From 0e0e1d30446ab37c873fc2722c4b3587cc31db7a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 12 Jun 2026 18:16:09 +0100 Subject: [PATCH] Play Weksa speech artifacts in Discord voice --- .env.example | 6 + apps/bot/package.json | 4 +- apps/bot/src/discord-bot.ts | 3 + apps/bot/src/repo-face-voice-playback.ts | 142 +++++ apps/worker/src/index.ts | 24 + package-lock.json | 552 +++++++++++++++++++- packages/config/src/index.ts | 22 + packages/core/src/index.ts | 1 + packages/core/src/repo-face-voice-outbox.ts | 104 ++++ state/evidence.jsonl | 1 + 10 files changed, 857 insertions(+), 2 deletions(-) create mode 100644 apps/bot/src/repo-face-voice-playback.ts create mode 100644 packages/core/src/repo-face-voice-outbox.ts diff --git a/.env.example b/.env.example index 8a7aeaa..68dfad7 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/apps/bot/package.json b/apps/bot/package.json index 24971a5..4d8818d 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -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" } } diff --git a/apps/bot/src/discord-bot.ts b/apps/bot/src/discord-bot.ts index 6b84b8f..7458e21 100644 --- a/apps/bot/src/discord-bot.ts +++ b/apps/bot/src/discord-bot.ts @@ -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 { intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.MessageContent, ], }); @@ -313,6 +315,7 @@ export async function startBot(): Promise { 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) => { diff --git a/apps/bot/src/repo-face-voice-playback.ts b/apps/bot/src/repo-face-voice-playback.ts new file mode 100644 index 0000000..e980d2e --- /dev/null +++ b/apps/bot/src/repo-face-voice-playback.ts @@ -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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 4012fc5..d8df653 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -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 { + 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, diff --git a/package-lock.json b/package-lock.json index 1ddf10e..c5c7967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index e65baec..fab6b5c 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -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, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ad84ec2..99b49a3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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"; diff --git a/packages/core/src/repo-face-voice-outbox.ts b/packages/core/src/repo-face-voice-outbox.ts new file mode 100644 index 0000000..0403a8a --- /dev/null +++ b/packages/core/src/repo-face-voice-outbox.ts @@ -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 { + await mkdir(dirname(outboxPath), { recursive: true }); + await appendFile(outboxPath, `${JSON.stringify(entry)}\n`, "utf8"); +} + +export async function loadRepoFaceVoiceOutboxEntries( + outboxPath: string, +): Promise { + 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 { + 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> { + const content = await readFile(playedPath, "utf8").catch((error: NodeJS.ErrnoException) => { + if (error.code === "ENOENT") { + return ""; + } + throw error; + }); + const ids = new Set(); + 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); +} diff --git a/state/evidence.jsonl b/state/evidence.jsonl index fe0e3b7..1c260be 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -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."}