diff --git a/extensions/elevenlabs/speech-provider.test.ts b/extensions/elevenlabs/speech-provider.test.ts index 060d44a5f36f..0d0af78dddab 100644 --- a/extensions/elevenlabs/speech-provider.test.ts +++ b/extensions/elevenlabs/speech-provider.test.ts @@ -45,6 +45,17 @@ const OUTPUT_FORMAT_CASES = [ { outputFormat: "future_123", fileExtension: ".bin", voiceCompatible: false }, ] as const; +const DIRECTIVE_POLICY = { + enabled: true, + allowText: true, + allowProvider: true, + allowVoice: true, + allowModelId: true, + allowVoiceSettings: true, + allowNormalization: true, + allowSeed: true, +}; + describe("elevenlabs speech provider", () => { const originalFetch = globalThis.fetch; @@ -73,6 +84,49 @@ describe("elevenlabs speech provider", () => { ]); }); + it.each([ + ["stability", "0", { stability: 0 }], + ["similarity", "1", { similarityBoost: 1 }], + ["similarityboost", "0.25", { similarityBoost: 0.25 }], + ["similarity_boost", "5e-1", { similarityBoost: 0.5 }], + ["style", "1", { style: 1 }], + ["speed", ".5", { speed: 0.5 }], + ["speed", "2", { speed: 2 }], + ["stability", "-0.1", "stability must be between 0 and 1"], + ["similarity", "Infinity", "invalid similarityBoost value"], + ["similarity_boost", "1.1", "similarityBoost must be between 0 and 1"], + ["style", "0x1", "invalid style value"], + ["speed", ".49", "speed must be between 0.5 and 2"], + ["speed", "2.01", "speed must be between 0.5 and 2"], + ["speed", "invalid", undefined, false], + ] as const)( + "preserves the %s=%s voice-setting directive", + (key, value, expected, allowed?: boolean) => { + const currentOverrides = { voiceId: "existing-voice", voiceSettings: { style: 0.25 } }; + const allowVoiceSettings = allowed ?? true; + const parsed = buildElevenLabsSpeechProvider().parseDirectiveToken?.({ + key, + value, + policy: { ...DIRECTIVE_POLICY, allowVoiceSettings }, + currentOverrides, + }); + + expect(parsed).toEqual( + !allowVoiceSettings + ? { handled: true } + : typeof expected === "string" + ? { handled: true, warnings: [expected] } + : { + handled: true, + overrides: { + ...currentOverrides, + voiceSettings: { ...currentOverrides.voiceSettings, ...expected }, + }, + }, + ); + }, + ); + it("forwards the core-resolved voice-list timeout", async () => { globalThis.fetch = vi.fn(async () => Response.json({ voices: [] })) as unknown as typeof fetch; const provider = buildElevenLabsSpeechProvider(); diff --git a/extensions/elevenlabs/speech-provider.ts b/extensions/elevenlabs/speech-provider.ts index d78e3f4a8b55..6f227c69dba5 100644 --- a/extensions/elevenlabs/speech-provider.ts +++ b/extensions/elevenlabs/speech-provider.ts @@ -269,54 +269,25 @@ function parseDirectiveToken(ctx: SpeechDirectiveTokenParseContext) { handled: true, overrides: { ...ctx.currentOverrides, modelId: normalizeElevenLabsTtsModelId(ctx.value) }, }; - case "stability": { - if (!ctx.policy.allowVoiceSettings) { - return { handled: true }; - } - const value = parseNumberValue(ctx.value); - if (value == null) { - return { handled: true, warnings: ["invalid stability value"] }; - } - requireInRange(value, 0, 1, "stability"); - return { handled: true, overrides: mergeVoiceSettingsOverride(ctx, { stability: value }) }; - } + case "stability": case "similarity": case "similarityboost": - case "similarity_boost": { - if (!ctx.policy.allowVoiceSettings) { - return { handled: true }; - } - const value = parseNumberValue(ctx.value); - if (value == null) { - return { handled: true, warnings: ["invalid similarityBoost value"] }; - } - requireInRange(value, 0, 1, "similarityBoost"); - return { - handled: true, - overrides: mergeVoiceSettingsOverride(ctx, { similarityBoost: value }), - }; - } - case "style": { - if (!ctx.policy.allowVoiceSettings) { - return { handled: true }; - } - const value = parseNumberValue(ctx.value); - if (value == null) { - return { handled: true, warnings: ["invalid style value"] }; - } - requireInRange(value, 0, 1, "style"); - return { handled: true, overrides: mergeVoiceSettingsOverride(ctx, { style: value }) }; - } + case "similarity_boost": + case "style": case "speed": { if (!ctx.policy.allowVoiceSettings) { return { handled: true }; } + const setting = ctx.key.startsWith("similarity") ? "similarityBoost" : ctx.key; const value = parseNumberValue(ctx.value); if (value == null) { - return { handled: true, warnings: ["invalid speed value"] }; + return { handled: true, warnings: [`invalid ${setting} value`] }; } - requireInRange(value, 0.5, 2, "speed"); - return { handled: true, overrides: mergeVoiceSettingsOverride(ctx, { speed: value }) }; + requireInRange(value, setting === "speed" ? 0.5 : 0, setting === "speed" ? 2 : 1, setting); + return { + handled: true, + overrides: mergeVoiceSettingsOverride(ctx, { [setting]: value }), + }; } case "speakerboost": case "speaker_boost":