fix(tts): compose personas with agent config

This commit is contained in:
Ayaan Zaidi
2026-04-26 09:03:11 +05:30
parent f801fe7d27
commit cc2044633c
2 changed files with 257 additions and 0 deletions

View File

@@ -506,6 +506,84 @@ describe("speech-core per-agent TTS config", () => {
});
});
it("composes per-agent TTS overrides with active persona bindings", async () => {
const cfg = {
messages: {
tts: {
enabled: true,
provider: "mock",
providers: {
mock: {
model: "base-model",
voice: "base-voice",
},
},
persona: "alfred",
personas: {
alfred: {
provider: "mock",
providers: {
mock: {
voice: "alfred-voice",
},
},
},
jarvis: {
provider: "mock",
providers: {
mock: {
style: "jarvis-style",
},
},
},
},
},
},
agents: {
list: [
{
id: "reader",
tts: {
persona: "jarvis",
providers: {
mock: {
voice: "agent-voice",
},
},
},
},
],
},
} satisfies OpenClawConfig;
let mediaDir: string | undefined;
try {
const result = await maybeApplyTtsToPayload({
payload: { text: "This agent reply should use the composed persona config." },
cfg,
channel: "slack",
kind: "final",
agentId: "reader",
});
expect(synthesizeMock).toHaveBeenCalledWith(
expect.objectContaining({
providerConfig: expect.objectContaining({
model: "base-model",
voice: "agent-voice",
style: "jarvis-style",
}),
}),
);
expect(result.mediaUrl).toMatch(/voice-\d+\.ogg$/);
mediaDir = result.mediaUrl ? path.dirname(result.mediaUrl) : undefined;
} finally {
if (mediaDir) {
rmSync(mediaDir, { recursive: true, force: true });
}
}
});
it("ignores prototype-pollution keys in agent TTS overrides", () => {
const cfg = {
messages: {

View File

@@ -6549,6 +6549,181 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
type: "string",
minLength: 1,
},
persona: {
type: "string",
},
personas: {
type: "object",
propertyNames: {
type: "string",
},
additionalProperties: {
type: "object",
properties: {
label: {
type: "string",
},
description: {
type: "string",
},
provider: {
type: "string",
minLength: 1,
},
fallbackPolicy: {
anyOf: [
{
type: "string",
const: "preserve-persona",
},
{
type: "string",
const: "provider-defaults",
},
{
type: "string",
const: "fail",
},
],
},
prompt: {
type: "object",
properties: {
profile: {
type: "string",
},
scene: {
type: "string",
},
sampleContext: {
type: "string",
},
style: {
type: "string",
},
accent: {
type: "string",
},
pacing: {
type: "string",
},
constraints: {
type: "array",
items: {
type: "string",
},
},
},
additionalProperties: false,
},
providers: {
type: "object",
propertyNames: {
type: "string",
},
additionalProperties: {
type: "object",
properties: {
apiKey: {
anyOf: [
{
type: "string",
},
{
oneOf: [
{
type: "object",
properties: {
source: {
type: "string",
const: "env",
},
provider: {
type: "string",
pattern: "^[a-z][a-z0-9_-]{0,63}$",
},
id: {
type: "string",
pattern: "^[A-Z][A-Z0-9_]{0,127}$",
},
},
required: ["source", "provider", "id"],
additionalProperties: false,
},
{
type: "object",
properties: {
source: {
type: "string",
const: "file",
},
provider: {
type: "string",
pattern: "^[a-z][a-z0-9_-]{0,63}$",
},
id: {
type: "string",
},
},
required: ["source", "provider", "id"],
additionalProperties: false,
},
{
type: "object",
properties: {
source: {
type: "string",
const: "exec",
},
provider: {
type: "string",
pattern: "^[a-z][a-z0-9_-]{0,63}$",
},
id: {
type: "string",
},
},
required: ["source", "provider", "id"],
additionalProperties: false,
},
],
},
],
},
},
additionalProperties: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
{
type: "boolean",
},
{
type: "null",
},
{
type: "array",
items: {},
},
{
type: "object",
propertyNames: {
type: "string",
},
additionalProperties: {},
},
],
},
},
},
},
additionalProperties: false,
},
},
summaryModel: {
type: "string",
},
@@ -27972,6 +28147,10 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
sensitive: true,
tags: ["security", "auth"],
},
"agents.list[].tts.personas.*.providers.*.apiKey": {
sensitive: true,
tags: ["security", "auth", "media"],
},
"agents.list[].tts.providers.*.apiKey": {
sensitive: true,
tags: ["security", "auth", "media"],