Play Weksa speech artifacts in Discord voice

This commit is contained in:
Meta Vens 2026-06-12 18:16:09 +01:00
commit 0e0e1d3044
10 changed files with 857 additions and 2 deletions

View file

@ -70,6 +70,12 @@ REPO_FACE_GITHUB_ACTIONS_ENABLED=false
REPO_FACE_WEKSA_SPEECH_ENABLED=false
WEKSA_DAEMON_BASE_URL=http://127.0.0.1:8813
WEKSA_DAEMON_TIMEOUT_MS=120000
WEKSA_REPO_ROOT=E:/Projects/weksa
REPO_FACE_DISCORD_VOICE_ENABLED=false
REPO_FACE_DISCORD_VOICE_CHANNEL_ID=
REPO_FACE_DISCORD_VOICE_OUTBOX_PATH=.voidbot/status/repo-face-voice-outbox.jsonl
REPO_FACE_DISCORD_VOICE_PLAYED_PATH=.voidbot/status/repo-face-voice-played.jsonl
REPO_FACE_DISCORD_VOICE_POLL_INTERVAL_MS=5000
REPO_FACE_HEARTBEATS_ENABLED=false
REPO_FACE_HEARTBEAT_STATE_PATH=.voidbot/status/repo-face-heartbeats.json
REPO_FACE_HEARTBEAT_TASK_NAME=VoidBot Repo Face Heartbeats

View file

@ -5,12 +5,14 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"@discordjs/voice": "^0.19.2",
"@voidbot/config": "0.1.0",
"@voidbot/core": "0.1.0",
"@voidbot/providers": "0.1.0",
"@voidbot/rag": "0.1.0",
"@voidbot/shared": "0.1.0",
"discord.js": "^14.19.0",
"dotenv": "^16.4.0"
"dotenv": "^16.4.0",
"ffmpeg-static": "^5.3.0"
}
}

View file

@ -61,6 +61,7 @@ import {
parseProviderOverride,
replyEphemeral,
} from "./discord-bot-handlers";
import { startRepoFaceVoicePlayback } from "./repo-face-voice-playback";
import {
buildActorFromInteraction,
buildChannelIndexingTarget,
@ -92,6 +93,7 @@ export async function startBot(): Promise<void> {
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.MessageContent,
],
});
@ -313,6 +315,7 @@ export async function startBot(): Promise<void> {
client.once(Events.ClientReady, async (readyClient) => {
console.log(`VoidBot connected as ${readyClient.user.tag}.`);
await maybeRegisterCommands(config.botToken!, config.applicationId, config.developmentGuildId);
startRepoFaceVoicePlayback(readyClient, config.repoFaceDiscordVoice);
});
client.on(Events.MessageCreate, async (message) => {

View file

@ -0,0 +1,142 @@
import { existsSync } from "node:fs";
import {
AudioPlayerStatus,
VoiceConnectionStatus,
createAudioPlayer,
createAudioResource,
entersState,
joinVoiceChannel,
type AudioPlayer,
type VoiceConnection,
} from "@discordjs/voice";
import { ChannelType, type Client, type GuildBasedChannel } from "discord.js";
import {
appendRepoFaceVoicePlayedEntry,
loadRepoFaceVoiceOutboxEntries,
loadRepoFaceVoicePlayedIds,
type RepoFaceVoiceOutboxEntry,
} from "@voidbot/core";
export interface RepoFaceVoicePlaybackOptions {
enabled: boolean;
channelId?: string;
outboxPath: string;
playedPath: string;
pollIntervalMs: number;
}
export function startRepoFaceVoicePlayback(
client: Client,
options: RepoFaceVoicePlaybackOptions,
): void {
if (!options.enabled) {
return;
}
if (!options.channelId) {
console.warn("Repo Face Discord voice playback enabled without REPO_FACE_DISCORD_VOICE_CHANNEL_ID.");
return;
}
const runtime = new RepoFaceVoicePlaybackRuntime(client, options);
void runtime.poll();
setInterval(() => {
void runtime.poll();
}, options.pollIntervalMs);
}
class RepoFaceVoicePlaybackRuntime {
private readonly player: AudioPlayer;
private connection?: VoiceConnection;
private active = false;
constructor(
private readonly client: Client,
private readonly options: RepoFaceVoicePlaybackOptions,
) {
this.player = createAudioPlayer();
this.player.on("error", (error) => {
console.warn(`Repo Face voice player error: ${error.message}`);
this.active = false;
});
}
async poll(): Promise<void> {
if (this.active) {
return;
}
const entries = await loadRepoFaceVoiceOutboxEntries(this.options.outboxPath);
if (entries.length === 0) {
return;
}
const played = await loadRepoFaceVoicePlayedIds(this.options.playedPath);
const entry = entries.find((candidate) => !played.has(candidate.id));
if (!entry) {
return;
}
await this.play(entry);
}
private async play(entry: RepoFaceVoiceOutboxEntry): Promise<void> {
if (!existsSync(entry.audioPath)) {
console.warn(`Repo Face voice audio is missing for ${entry.id}: ${entry.audioPath}`);
return;
}
this.active = true;
try {
const channel = await this.resolveVoiceChannel();
const connection = await this.ensureConnection(channel);
const resource = createAudioResource(entry.audioPath, {
metadata: entry,
});
this.player.play(resource);
connection.subscribe(this.player);
await entersState(this.player, AudioPlayerStatus.Playing, 15_000);
await entersState(this.player, AudioPlayerStatus.Idle, 180_000);
await appendRepoFaceVoicePlayedEntry(this.options.playedPath, entry);
console.log(
`Played repo Face voice ${entry.id} for ${entry.displayName} in channel ${this.options.channelId}.`,
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(`Repo Face voice playback failed for ${entry.id}: ${message}`);
} finally {
this.active = false;
}
}
private async resolveVoiceChannel(): Promise<GuildBasedChannel> {
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.`);
}
if (channel.type !== ChannelType.GuildVoice && channel.type !== ChannelType.GuildStageVoice) {
throw new Error(`Configured channel ${this.options.channelId} is not a Discord voice/stage channel.`);
}
return channel;
}
private async ensureConnection(channel: GuildBasedChannel): Promise<VoiceConnection> {
if (
this.connection &&
this.connection.joinConfig.channelId === channel.id &&
this.connection.state.status !== VoiceConnectionStatus.Destroyed
) {
return this.connection;
}
if (!("voiceAdapterCreator" in channel.guild)) {
throw new Error(`Guild for voice channel ${channel.id} does not expose a voice adapter.`);
}
this.connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
adapterCreator: channel.guild.voiceAdapterCreator,
selfDeaf: false,
});
await entersState(this.connection, VoiceConnectionStatus.Ready, 30_000);
return this.connection;
}
}

View file

@ -8,6 +8,7 @@ import { isAbsolute, join, relative, resolve } from "node:path";
import { loadConfig } from "@voidbot/config";
import {
applyRepoFacePostFatigueAfterSpeech,
appendRepoFaceVoiceOutboxEntry,
type AuditLog,
buildVoidMcpServerConfig,
createStateStorage,
@ -19,6 +20,7 @@ import {
loadFaceIdentityRegistry,
loadSystemMessageCatalog,
resolveRepoFaceStatePath,
resolveWeksaArtifactPath,
type SystemMessageCatalog,
} from "@voidbot/core";
import {
@ -1508,6 +1510,28 @@ async function recordRepoIdentityWeksaSpeechReceipt(input: {
messageId: string;
receipt: WeksaMimoReceipt;
}): Promise<void> {
const audioPath = resolveWeksaArtifactPath(
config.repoFaceWeksaSpeech.repoRoot,
input.receipt.artifacts?.audio,
);
if (config.repoFaceDiscordVoice.enabled && audioPath) {
await appendRepoFaceVoiceOutboxEntry(config.repoFaceDiscordVoice.outboxPath, {
schemaVersion: "voidbot.repo_face_voice_outbox.v1",
id: `repo-face:${input.identity.id}:${input.messageId}`,
createdAt: new Date().toISOString(),
identityId: input.identity.id,
displayName: input.identity.displayName,
repoName: input.identity.repoName,
textChannelId: input.channelId,
textMessageId: input.messageId,
replyToMessageId: input.replyToMessageId,
contentPreview: input.content.slice(0, 500),
weksaRequestId: input.receipt.request_id,
weksaReceiptArtifact: input.receipt.artifacts?.receipt,
audioPath,
audioBytes: input.receipt.provider_response?.audio_bytes,
});
}
await auditLog.record({
type: "repo_identity.weksa_speech_rendered",
actorId: input.job.requester.id,

552
package-lock.json generated
View file

@ -22,13 +22,15 @@
"name": "@voidbot/bot",
"version": "0.1.0",
"dependencies": {
"@discordjs/voice": "^0.19.2",
"@voidbot/config": "0.1.0",
"@voidbot/core": "0.1.0",
"@voidbot/providers": "0.1.0",
"@voidbot/rag": "0.1.0",
"@voidbot/shared": "0.1.0",
"discord.js": "^14.19.0",
"dotenv": "^16.4.0"
"dotenv": "^16.4.0",
"ffmpeg-static": "^5.3.0"
}
},
"apps/worker": {
@ -45,6 +47,21 @@
"zod": "^3.25.0"
}
},
"node_modules/@derhuerst/http-basic": {
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz",
"integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==",
"license": "MIT",
"dependencies": {
"caseless": "^0.12.0",
"concat-stream": "^2.0.0",
"http-response-object": "^3.0.1",
"parse-cache-control": "^1.0.1"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@discordjs/builders": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
@ -150,6 +167,26 @@
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/voice": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.19.2.tgz",
"integrity": "sha512-3yJ255e4ag3wfZu/DSxeOZK1UtnqNxnspmLaQetGT0pDkThNZoHs+Zg6dgZZ19JEVomXygvfHn9lNpICZuYtEA==",
"license": "Apache-2.0",
"dependencies": {
"@snazzah/davey": "^0.1.9",
"@types/ws": "^8.18.1",
"discord-api-types": "^0.38.41",
"prism-media": "^1.3.5",
"tslib": "^2.8.1",
"ws": "^8.19.0"
},
"engines": {
"node": ">=22.12.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/ws": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
@ -185,6 +222,40 @@
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@emnapi/core": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz",
"integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz",
"integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
@ -688,6 +759,24 @@
"node": ">= 18"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1"
}
},
"node_modules/@qdrant/js-client-rest": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/@qdrant/js-client-rest/-/js-client-rest-1.17.0.tgz",
@ -748,6 +837,280 @@
"npm": ">=7.0.0"
}
},
"node_modules/@snazzah/davey": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey/-/davey-0.1.11.tgz",
"integrity": "sha512-oBN+msHzPnm1M5DDx3wVD7iBwpNXFUtkh2MrAbUJu0OhKjliLChi28hq++mu1+qdMpAVQO5JKAvQQxYVbyneiw==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
"funding": {
"url": "https://github.com/sponsors/Snazzah"
},
"optionalDependencies": {
"@snazzah/davey-android-arm-eabi": "0.1.11",
"@snazzah/davey-android-arm64": "0.1.11",
"@snazzah/davey-darwin-arm64": "0.1.11",
"@snazzah/davey-darwin-x64": "0.1.11",
"@snazzah/davey-freebsd-x64": "0.1.11",
"@snazzah/davey-linux-arm-gnueabihf": "0.1.11",
"@snazzah/davey-linux-arm64-gnu": "0.1.11",
"@snazzah/davey-linux-arm64-musl": "0.1.11",
"@snazzah/davey-linux-x64-gnu": "0.1.11",
"@snazzah/davey-linux-x64-musl": "0.1.11",
"@snazzah/davey-wasm32-wasi": "0.1.11",
"@snazzah/davey-win32-arm64-msvc": "0.1.11",
"@snazzah/davey-win32-ia32-msvc": "0.1.11",
"@snazzah/davey-win32-x64-msvc": "0.1.11"
}
},
"node_modules/@snazzah/davey-android-arm-eabi": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-android-arm-eabi/-/davey-android-arm-eabi-0.1.11.tgz",
"integrity": "sha512-T1RYbNYKN6tLOcGIDKJd8OI6FBSEemwL7DOYdTMmhqfhhMr3YVN8WOhfoxGg63OcnpTN2e2c5tdY2bAx25RmQQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@snazzah/davey-android-arm64": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-android-arm64/-/davey-android-arm64-0.1.11.tgz",
"integrity": "sha512-ksJn/x2VU8h6w9eku1HT96ugSRZ7lKVkKNKbFleaFN+U99DJaPM+gMu2YvnFU4V54HR06ZBnRihnVG6VLXQpDw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@snazzah/davey-darwin-arm64": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-darwin-arm64/-/davey-darwin-arm64-0.1.11.tgz",
"integrity": "sha512-E1d7PbaaVMO3Lj9EiAPqOVbuV0xg5+PsHzHH097DDXiD1+zUDXvJaTnUWsnm5z50pJniHpi4GtaYmk+ieB/guA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@snazzah/davey-darwin-x64": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-darwin-x64/-/davey-darwin-x64-0.1.11.tgz",
"integrity": "sha512-Tl4TI/LTmgJZepgbgVMYDi8RqlAkPtPg1OEBPl7a9Tn3AwR36Vs6lyIT1cs/lGy/ds/+B+mKI4rPObN1cyILTw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@snazzah/davey-freebsd-x64": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-freebsd-x64/-/davey-freebsd-x64-0.1.11.tgz",
"integrity": "sha512-T8Iw9FXkuI1T+YBAFzh9v/TXf9IOTOSqnd/BFpTRTrlW72PR2lhIidzSmg027VxO7r5pX47iFwiOkb9I/NU/EA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@snazzah/davey-linux-arm-gnueabihf": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-linux-arm-gnueabihf/-/davey-linux-arm-gnueabihf-0.1.11.tgz",
"integrity": "sha512-1Txj+8pqA8uq/OGtaUaBFWAPnNMQzFgIywj0iA7EI4xZl+mab48/pv+YZ1pNb/suC6ynsW44oB9efiXSdcUAgA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@snazzah/davey-linux-arm64-gnu": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-linux-arm64-gnu/-/davey-linux-arm64-gnu-0.1.11.tgz",
"integrity": "sha512-ERzF5nM/IYW1BcN3wLXpEwBCGLFf0kGJUVhaV6yfiInz0tkU8UmvrrgpaMaACfMjIhfWdq5CcX+aTkXo/saNcg==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@snazzah/davey-linux-arm64-musl": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-linux-arm64-musl/-/davey-linux-arm64-musl-0.1.11.tgz",
"integrity": "sha512-e6pX6Hiabtz99q+H/YHNkm9JVlpqN8HGh0qPib8G2+UY4/SSH8WvqWipk3v581dMy2oyCHt7MOoY1aU1P1N/xA==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@snazzah/davey-linux-x64-gnu": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-linux-x64-gnu/-/davey-linux-x64-gnu-0.1.11.tgz",
"integrity": "sha512-TW5bSoqChOJMbvsDb4wAATYrxmAXuNnse7wFNVSAJUaZKSeRfZbu3UAiPWSNn7GwLwSfU6hg322KZUn8IWCuvg==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@snazzah/davey-linux-x64-musl": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-linux-x64-musl/-/davey-linux-x64-musl-0.1.11.tgz",
"integrity": "sha512-5j6Pmc+Wzv5lSxVP6quA7teYRJXibkZqQyYGfTDnTsUOO5dPpcojpqlXlkhyvsA1OAQTj4uxbOCciN3cVWwzug==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@snazzah/davey-wasm32-wasi": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-wasm32-wasi/-/davey-wasm32-wasi-0.1.11.tgz",
"integrity": "sha512-rKOwZ/0J8lp+4VEyOdMDBRP9KR+PksZpa9V1Qn0veMzy4FqTVKthkxwGqewheFe0SFg9fdvt798l/PBFrfDeZw==",
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "^1.1.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@snazzah/davey-win32-arm64-msvc": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-win32-arm64-msvc/-/davey-win32-arm64-msvc-0.1.11.tgz",
"integrity": "sha512-5fptJU4tX901m3mj0SHiBljMrPT4ZEsynbBhR7bK1yn9TY1jjyhN8EFi7QF5IWtUEni+0mia2BCMHZ5ZkmFZqQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@snazzah/davey-win32-ia32-msvc": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-win32-ia32-msvc/-/davey-win32-ia32-msvc-0.1.11.tgz",
"integrity": "sha512-ualexn8SeLsiMHhWfzVrzRcjHgcBapg++FPaVgJJxoh2S/jCRiklXOu3luqIZdJdNKvhe2V9SwO/cImPeIIBKw==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@snazzah/davey-win32-x64-msvc": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@snazzah/davey-win32-x64-msvc/-/davey-win32-x64-msvc-0.1.11.tgz",
"integrity": "sha512-muNhc8UKXtknzsH/w4AIkbPR2I8BuvApn0pDXar0IEvY8PCjqU/M8MPbOOEYwQVvQRMwVTgExtxzrkBPSXB4nA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/node": {
"version": "24.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
@ -833,6 +1196,18 @@
"node": ">= 0.6"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
@ -890,6 +1265,12 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -928,6 +1309,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
"license": "Apache-2.0"
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/content-disposition": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
@ -1106,6 +1508,15 @@
"node": ">= 0.8"
}
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -1297,6 +1708,22 @@
],
"license": "BSD-3-Clause"
},
"node_modules/ffmpeg-static": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz",
"integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@derhuerst/http-basic": "^8.2.0",
"env-paths": "^2.2.0",
"https-proxy-agent": "^5.0.0",
"progress": "^2.0.3"
},
"engines": {
"node": ">=16"
}
},
"node_modules/finalhandler": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
@ -1475,6 +1902,34 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/http-response-object": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
"integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
"license": "MIT",
"dependencies": {
"@types/node": "^10.0.3"
}
},
"node_modules/http-response-object/node_modules/@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
"license": "MIT"
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
@ -1678,6 +2133,11 @@
"wrappy": "1"
}
},
"node_modules/parse-cache-control": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
"integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -1843,6 +2303,41 @@
"node": ">=0.10.0"
}
},
"node_modules/prism-media": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz",
"integrity": "sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==",
"license": "Apache-2.0",
"peerDependencies": {
"@discordjs/opus": ">=0.8.0 <1.0.0",
"ffmpeg-static": "^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0",
"node-opus": "^0.3.3",
"opusscript": "^0.0.8"
},
"peerDependenciesMeta": {
"@discordjs/opus": {
"optional": true
},
"ffmpeg-static": {
"optional": true
},
"node-opus": {
"optional": true
},
"opusscript": {
"optional": true
}
}
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -1895,6 +2390,20 @@
"node": ">= 0.10"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@ -1930,6 +2439,26 @@
"node": ">= 18"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@ -2098,6 +2627,15 @@
"node": ">= 0.8"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -2153,6 +2691,12 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@ -2190,6 +2734,12 @@
"node": ">= 0.8"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View file

@ -111,6 +111,12 @@ const envSchema = z.object({
REPO_FACE_WEKSA_SPEECH_ENABLED: booleanFromEnv.default(false),
WEKSA_DAEMON_BASE_URL: z.string().url().default("http://127.0.0.1:8813"),
WEKSA_DAEMON_TIMEOUT_MS: z.coerce.number().int().positive().default(120000),
WEKSA_REPO_ROOT: z.string().min(1).default("E:/Projects/weksa"),
REPO_FACE_DISCORD_VOICE_ENABLED: booleanFromEnv.default(false),
REPO_FACE_DISCORD_VOICE_CHANNEL_ID: optionalNonEmptyString,
REPO_FACE_DISCORD_VOICE_OUTBOX_PATH: z.string().min(1).default(".voidbot/status/repo-face-voice-outbox.jsonl"),
REPO_FACE_DISCORD_VOICE_PLAYED_PATH: z.string().min(1).default(".voidbot/status/repo-face-voice-played.jsonl"),
REPO_FACE_DISCORD_VOICE_POLL_INTERVAL_MS: z.coerce.number().int().positive().default(5000),
BIFROST_ROOT: z.string().min(1).default("E:/Projects/Bifrost"),
BIFROST_DISCORD_CHANNEL_ID: optionalNonEmptyString,
REPO_FACE_HEARTBEATS_ENABLED: booleanFromEnv.default(false),
@ -190,6 +196,14 @@ export interface AppConfig {
enabled: boolean;
daemonBaseUrl: string;
timeoutMs: number;
repoRoot: string;
};
repoFaceDiscordVoice: {
enabled: boolean;
channelId?: string;
outboxPath: string;
playedPath: string;
pollIntervalMs: number;
};
bifrostRoot: string;
bifrostDiscordChannelId?: string;
@ -436,6 +450,14 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
enabled: parsed.REPO_FACE_WEKSA_SPEECH_ENABLED,
daemonBaseUrl: parsed.WEKSA_DAEMON_BASE_URL.replace(/\/+$/, ""),
timeoutMs: parsed.WEKSA_DAEMON_TIMEOUT_MS,
repoRoot: resolve(parsed.WEKSA_REPO_ROOT),
},
repoFaceDiscordVoice: {
enabled: parsed.REPO_FACE_DISCORD_VOICE_ENABLED,
channelId: parsed.REPO_FACE_DISCORD_VOICE_CHANNEL_ID,
outboxPath: resolve(parsed.REPO_FACE_DISCORD_VOICE_OUTBOX_PATH),
playedPath: resolve(parsed.REPO_FACE_DISCORD_VOICE_PLAYED_PATH),
pollIntervalMs: parsed.REPO_FACE_DISCORD_VOICE_POLL_INTERVAL_MS,
},
bifrostRoot: resolve(parsed.BIFROST_ROOT),
bifrostDiscordChannelId: parsed.BIFROST_DISCORD_CHANNEL_ID,

View file

@ -10,6 +10,7 @@ export * from "./repo-discord-identities";
export * from "./repo-face-heartbeat-queue";
export * from "./repo-face-initializer";
export * from "./repo-face-rest";
export * from "./repo-face-voice-outbox";
export * from "./situational-social-read";
export * from "./state-storage";
export * from "./style-pack-loader";

View file

@ -0,0 +1,104 @@
import { appendFile, mkdir, readFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
export interface RepoFaceVoiceOutboxEntry {
schemaVersion: "voidbot.repo_face_voice_outbox.v1";
id: string;
createdAt: string;
identityId: string;
displayName: string;
repoName: string;
textChannelId: string;
textMessageId: string;
replyToMessageId?: string;
contentPreview: string;
weksaRequestId?: string;
weksaReceiptArtifact?: string;
audioPath: string;
audioBytes?: number;
}
export async function appendRepoFaceVoiceOutboxEntry(
outboxPath: string,
entry: RepoFaceVoiceOutboxEntry,
): Promise<void> {
await mkdir(dirname(outboxPath), { recursive: true });
await appendFile(outboxPath, `${JSON.stringify(entry)}\n`, "utf8");
}
export async function loadRepoFaceVoiceOutboxEntries(
outboxPath: string,
): Promise<RepoFaceVoiceOutboxEntry[]> {
const content = await readFile(outboxPath, "utf8").catch((error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") {
return "";
}
throw error;
});
return content
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.flatMap((line) => {
try {
const parsed = JSON.parse(line) as RepoFaceVoiceOutboxEntry;
return parsed.schemaVersion === "voidbot.repo_face_voice_outbox.v1" &&
typeof parsed.id === "string" &&
typeof parsed.audioPath === "string"
? [parsed]
: [];
} catch {
return [];
}
});
}
export async function appendRepoFaceVoicePlayedEntry(
playedPath: string,
entry: RepoFaceVoiceOutboxEntry,
): Promise<void> {
await mkdir(dirname(playedPath), { recursive: true });
await appendFile(
playedPath,
`${JSON.stringify({
schemaVersion: "voidbot.repo_face_voice_played.v1",
id: entry.id,
playedAt: new Date().toISOString(),
identityId: entry.identityId,
textMessageId: entry.textMessageId,
audioPath: entry.audioPath,
})}\n`,
"utf8",
);
}
export async function loadRepoFaceVoicePlayedIds(playedPath: string): Promise<Set<string>> {
const content = await readFile(playedPath, "utf8").catch((error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") {
return "";
}
throw error;
});
const ids = new Set<string>();
for (const line of content.split(/\r?\n/)) {
if (!line.trim()) {
continue;
}
try {
const parsed = JSON.parse(line) as { id?: string };
if (typeof parsed.id === "string") {
ids.add(parsed.id);
}
} catch {
continue;
}
}
return ids;
}
export function resolveWeksaArtifactPath(weksaRepoRoot: string, artifactPath: string | undefined): string | undefined {
if (!artifactPath) {
return undefined;
}
return resolve(weksaRepoRoot, artifactPath);
}

View file

@ -353,3 +353,4 @@
{"ts":"2026-06-12T13:07:40Z","type":"implementation","status":"ok","note":"Raven Hugging Face token from gamecult-ops/huggingface-bifrost was installed into Ubuntu-24.04 at /root/.cache/huggingface/token, Vili now mounts/passes it into Kimodo containers, /health reports huggingFaceToken: present, Starfire smoke passes, and a Kimodo container verified gated metadata access to meta-llama/Meta-Llama-3-8B-Instruct. Token value was not logged."}
{"ts":"2026-06-12T14:12:05Z","type":"verification","status":"ok","note":"Kimodo real generation now works on Raven through Vili. Direct smoke generated E:\\Projects\\Vili\\.vili\\artifacts\\kimodo-real-smoke-live\\motion.npz after first-run cache warmup. Vili HTTP POST /motion/generate succeeded for prompt 'A person gives a small friendly wave.' with duration=1, diffusionSteps=2, seed=10, producing E:\\Projects\\Vili\\.vili\\artifacts\\vili-1781273290152\\motion.npz. Key fixes: writable Hugging Face cache mount and file-based Kimodo input_folder/meta.json prompt handoff."}
{"ts":"2026-06-12T16:01:29Z","type":"implementation","status":"ok","note":"Added gated repo Face Weksa speech projection after parent-approved Discord SAY delivery. Weksa remains utterance/MiMo artifact owner; VoidBot records audit receipts and text delivery stays authoritative if speech rendering fails."}
{"ts":"2026-06-12T17:06:36Z","type":"verification","status":"ok","note":"Discord voice playback organ is live for Weksa MiMo artifacts. Bot consumed repo-face voice outbox entry manual-voice-smoke-1781283564, joined Aquarium voice channel 1505916699831046224, played E:\\\\Projects\\\\weksa\\\\.weksa\\\\speech-provider\\\\mimo\\\\metame-1513599037692182589-smoke-003-auto-feminine\\\\metame-1513599037692182589-smoke-003-auto-feminine.wav, and wrote repo-face-voice-played receipt."}