Route repo Face SAY lines through Weksa speech

This commit is contained in:
Meta Vens 2026-06-12 17:15:49 +01:00
commit 0f1aba036f
5 changed files with 229 additions and 0 deletions

View file

@ -67,6 +67,9 @@ REPO_FACE_RUMINATION_PASSES=3
REPO_FACE_BIRTH_MODE=plan
REPO_FACE_BIRTH_EXECUTOR=codex-exec
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
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

@ -62,8 +62,13 @@ import {
parseRepoIdentityPostIntents,
type RepoIdentityPostIntent,
} from "./repo-face-speech.js";
import { WeksaSpeechClient, type WeksaMimoReceipt } from "./weksa-speech.js";
const config = loadConfig();
const weksaSpeechClient = new WeksaSpeechClient({
baseUrl: config.repoFaceWeksaSpeech.daemonBaseUrl,
timeoutMs: config.repoFaceWeksaSpeech.timeoutMs,
});
const REPO_IDENTITY_PROPOSAL_PR_SENTINEL = "VOIDBOT_REPO_IDENTITY_PROPOSAL_PR:";
const REPO_IDENTITY_PR_COMMENT_SENTINEL = "VOIDBOT_REPO_IDENTITY_PR_COMMENT:";
const REPO_IDENTITY_UPDATE_REQUEST_SENTINEL = "VOIDBOT_REPO_IDENTITY_UPDATE_REQUEST:";
@ -1394,6 +1399,14 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
const message = error instanceof Error ? error.message : String(error);
console.warn(`Could not record repo identity delivery receipt for job ${job.id}: ${message}`);
});
await maybeRenderRepoIdentityWeksaSpeech({
job,
identity,
channelId,
content,
replyToMessageId: intent.replyToMessageId,
messageId: posted.messageId,
});
console.log(
`Posted repo identity ${identity.id} to Discord channel ${channelId} from job ${job.id} via ${posted.transport} message ${posted.messageId}.`,
);
@ -1431,6 +1444,90 @@ async function resolveRepoIdentityForJobIntent(
return findRepoDiscordIdentity(registry, identityId);
}
async function maybeRenderRepoIdentityWeksaSpeech(input: {
job: JobRecord;
identity: NonNullable<ReturnType<typeof findRepoDiscordIdentity>>;
channelId: string;
content: string;
replyToMessageId?: string;
messageId: string;
}): Promise<void> {
if (!config.repoFaceWeksaSpeech.enabled) {
return;
}
if (!input.identity.faceStatePath) {
console.warn(
`Skipped Weksa speech for repo identity ${input.identity.id}: no faceStatePath is registered.`,
);
return;
}
try {
const receipt = await weksaSpeechClient.renderRepoFaceSpeech({
jobId: input.job.id,
identityId: input.identity.id,
displayName: input.identity.displayName,
repoName: input.identity.repoName,
personaStatePath: input.identity.faceStatePath,
channelId: input.channelId,
messageId: input.messageId,
replyToMessageId: input.replyToMessageId,
content: input.content,
});
await recordRepoIdentityWeksaSpeechReceipt({
...input,
receipt,
});
console.log(
`Rendered Weksa/MiMo speech for repo identity ${input.identity.id} message ${input.messageId}: ${receipt.artifacts?.audio ?? "audio artifact not reported"}.`,
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
await auditLog.record({
type: "repo_identity.weksa_speech_failed",
actorId: input.job.requester.id,
jobId: input.job.id,
provider: input.job.provider,
details: {
identityId: input.identity.id,
channelId: input.channelId,
messageId: input.messageId,
error: message,
},
});
console.warn(`Weksa speech failed for repo identity ${input.identity.id} job ${input.job.id}: ${message}`);
}
}
async function recordRepoIdentityWeksaSpeechReceipt(input: {
job: JobRecord;
identity: NonNullable<ReturnType<typeof findRepoDiscordIdentity>>;
channelId: string;
content: string;
replyToMessageId?: string;
messageId: string;
receipt: WeksaMimoReceipt;
}): Promise<void> {
await auditLog.record({
type: "repo_identity.weksa_speech_rendered",
actorId: input.job.requester.id,
jobId: input.job.id,
provider: input.job.provider,
details: {
identityId: input.identity.id,
channelId: input.channelId,
messageId: input.messageId,
replyToMessageId: input.replyToMessageId,
requestId: input.receipt.request_id,
receiptSchema: input.receipt.schema_version,
audioArtifact: input.receipt.artifacts?.audio,
receiptArtifact: input.receipt.artifacts?.receipt,
audioBytes: input.receipt.provider_response?.audio_bytes,
spokenContentLength: input.content.length,
},
});
}
async function loadRegisteredFaceRepoRegistry() {
return faceRegistryAsRepoDiscordRegistry(
await loadFaceIdentityRegistry(config.repoDiscordIdentitiesPath),

View file

@ -0,0 +1,115 @@
export interface WeksaSpeechClientOptions {
baseUrl: string;
timeoutMs: number;
}
export interface WeksaRepoFaceSpeechInput {
jobId: string;
identityId: string;
displayName: string;
repoName: string;
personaStatePath: string;
channelId: string;
messageId: string;
content: string;
replyToMessageId?: string;
}
export interface WeksaMimoReceipt {
schema_version?: string;
request_id?: string;
ok?: boolean;
provider?: string;
model?: string;
artifacts?: {
audio?: string;
receipt?: string;
interlingua_packet?: string;
mimo_request?: string;
};
provider_response?: {
audio_bytes?: number;
};
}
export class WeksaSpeechClient {
private readonly baseUrl: string;
private readonly timeoutMs: number;
constructor(options: WeksaSpeechClientOptions) {
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
this.timeoutMs = options.timeoutMs;
}
async renderRepoFaceSpeech(input: WeksaRepoFaceSpeechInput): Promise<WeksaMimoReceipt> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const response = await fetch(`${this.baseUrl}/speech-provider/mimo/voicedesign`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
signal: controller.signal,
body: JSON.stringify({
request_id: buildRequestId(input),
persona_state_path: input.personaStatePath,
speaker_agent_id: input.identityId,
thought_text: input.content,
spoken_text: input.content,
scene: `Discord channel ${input.channelId}`,
addressee: "Discord room",
intent: input.content,
performance_register: {
label: `${input.displayName} Discord voice`,
medium: "Discord voice channel playback",
delivery_archetype: `${input.displayName} speaks an approved VoidBot SAY line`,
},
delivery_controls: [
"Discord-room conversational delivery",
"preserve the approved text exactly",
`repo Face for ${input.repoName}`,
],
forbidden_traits: [
"generic assistant voice",
"rewriting source meaning",
"changing the approved Discord text",
],
private_interpretation:
"VoidBot already approved this SAY line for public text delivery; Weksa is only projecting it into spoken audio.",
intended_effect: "Make the approved public Discord line audible without changing its meaning.",
}),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`Weksa speech request failed with ${response.status}: ${text}`);
}
return JSON.parse(text) as WeksaMimoReceipt;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new Error(`Weksa speech request timed out after ${this.timeoutMs}ms.`);
}
throw error;
} finally {
clearTimeout(timeout);
}
}
}
function buildRequestId(input: WeksaRepoFaceSpeechInput): string {
return [
"voidbot",
sanitizeId(input.identityId),
sanitizeId(input.jobId).slice(0, 16),
sanitizeId(input.messageId).slice(0, 24),
].filter(Boolean).join("-");
}
function sanitizeId(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}

View file

@ -108,6 +108,9 @@ const envSchema = z.object({
REPO_FACE_BIRTH_EXECUTOR: z.enum(["model-runtime", "openai-runtime"]).default("model-runtime"),
REPO_FACE_GITHUB_ACTIONS_ENABLED: booleanFromEnv.default(false),
REPO_FACE_BIFROST_ENABLED: booleanFromEnv.default(false),
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),
BIFROST_ROOT: z.string().min(1).default("E:/Projects/Bifrost"),
BIFROST_DISCORD_CHANNEL_ID: optionalNonEmptyString,
REPO_FACE_HEARTBEATS_ENABLED: booleanFromEnv.default(false),
@ -183,6 +186,11 @@ export interface AppConfig {
repoFaceBirthExecutor: "model-runtime" | "openai-runtime";
repoFaceGithubActionsEnabled: boolean;
repoFaceBifrostEnabled: boolean;
repoFaceWeksaSpeech: {
enabled: boolean;
daemonBaseUrl: string;
timeoutMs: number;
};
bifrostRoot: string;
bifrostDiscordChannelId?: string;
repoFaceHeartbeats: {
@ -424,6 +432,11 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
repoFaceBirthExecutor: parsed.REPO_FACE_BIRTH_EXECUTOR,
repoFaceGithubActionsEnabled: parsed.REPO_FACE_GITHUB_ACTIONS_ENABLED,
repoFaceBifrostEnabled: parsed.REPO_FACE_BIFROST_ENABLED,
repoFaceWeksaSpeech: {
enabled: parsed.REPO_FACE_WEKSA_SPEECH_ENABLED,
daemonBaseUrl: parsed.WEKSA_DAEMON_BASE_URL.replace(/\/+$/, ""),
timeoutMs: parsed.WEKSA_DAEMON_TIMEOUT_MS,
},
bifrostRoot: resolve(parsed.BIFROST_ROOT),
bifrostDiscordChannelId: parsed.BIFROST_DISCORD_CHANNEL_ID,
repoFaceHeartbeats: {

View file

@ -352,3 +352,4 @@
{"ts":"2026-06-12T11:57:27Z","type":"implementation","status":"ok","note":"Vili animation daemon created and deployed: GameCult/Vili runs on Raven at http://10.77.0.4:8824 with Eve provider vili.animation, Raven scheduled task GameCult\\Vili, Odin seed/discovery wired, Idunn target vili active, and Starfire smoke verifies Kimodo image, NVIDIA runtime, RTX 4060 Ti visibility, and kimodo_gen --help. Full generation still needs a Raven Hugging Face token with gated Meta-Llama-3-8B-Instruct access."}
{"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."}