Route repo Face SAY lines through Weksa speech
This commit is contained in:
parent
626fdee05e
commit
0f1aba036f
5 changed files with 229 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
115
apps/worker/src/weksa-speech.ts
Normal file
115
apps/worker/src/weksa-speech.ts
Normal 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, "");
|
||||
}
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue