mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 07:04:53 +00:00
fix: validate minimax speech voice settings
This commit is contained in:
@@ -449,6 +449,32 @@ describe("buildMinimaxSpeechProvider", () => {
|
||||
expect(voiceSetting.pitch).toBe(0);
|
||||
});
|
||||
|
||||
it("drops malformed voice settings before synthesis", async () => {
|
||||
const hexAudio = Buffer.from("audio").toString("hex");
|
||||
const mockFetch = vi.mocked(globalThis.fetch);
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ data: { audio: hexAudio } }), { status: 200 }),
|
||||
);
|
||||
|
||||
await provider.synthesize({
|
||||
text: "Test",
|
||||
cfg: {} as never,
|
||||
providerConfig: {
|
||||
apiKey: "sk-test",
|
||||
speed: 3,
|
||||
vol: -1,
|
||||
pitch: 20,
|
||||
},
|
||||
target: "audio-file",
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
|
||||
const voiceSetting = firstFetchBody().voice_setting as Record<string, unknown>;
|
||||
expect(voiceSetting.speed).toBe(1);
|
||||
expect(voiceSetting.vol).toBe(1);
|
||||
expect(voiceSetting.pitch).toBe(0);
|
||||
});
|
||||
|
||||
it("uses a MiniMax Token Plan env var when no API key is configured", async () => {
|
||||
process.env.MINIMAX_CODING_API_KEY = "sk-cp-env";
|
||||
const hexAudio = Buffer.from("audio").toString("hex");
|
||||
|
||||
@@ -11,7 +11,8 @@ import type {
|
||||
SpeechProviderOverrides,
|
||||
SpeechProviderPlugin,
|
||||
} from "openclaw/plugin-sdk/speech-core";
|
||||
import { asFiniteNumber, asObject, trimToUndefined } from "openclaw/plugin-sdk/speech-core";
|
||||
import { asObject, trimToUndefined } from "openclaw/plugin-sdk/speech-core";
|
||||
import { asFiniteNumberInRange } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
DEFAULT_MINIMAX_TTS_BASE_URL,
|
||||
MINIMAX_TTS_MODELS,
|
||||
@@ -108,12 +109,25 @@ function normalizeMinimaxProviderConfig(
|
||||
trimToUndefined(raw?.voiceId) ??
|
||||
trimToUndefined(process.env.MINIMAX_TTS_VOICE_ID) ??
|
||||
"English_expressive_narrator",
|
||||
speed: asFiniteNumber(raw?.speed),
|
||||
vol: asFiniteNumber(raw?.vol),
|
||||
pitch: asFiniteNumber(raw?.pitch),
|
||||
speed: normalizeMinimaxSpeed(raw?.speed),
|
||||
vol: normalizeMinimaxVolume(raw?.vol),
|
||||
pitch: normalizeMinimaxPitch(raw?.pitch),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMinimaxSpeed(value: unknown): number | undefined {
|
||||
return asFiniteNumberInRange(value, { min: 0.5, max: 2 });
|
||||
}
|
||||
|
||||
function normalizeMinimaxVolume(value: unknown): number | undefined {
|
||||
return asFiniteNumberInRange(value, { min: 0, max: 10, minExclusive: true });
|
||||
}
|
||||
|
||||
function normalizeMinimaxPitch(value: unknown): number | undefined {
|
||||
const pitch = asFiniteNumberInRange(value, { min: -12, max: 12 });
|
||||
return pitch !== undefined ? Math.trunc(pitch) : undefined;
|
||||
}
|
||||
|
||||
function readMinimaxProviderConfig(
|
||||
config: SpeechProviderConfig,
|
||||
cfg?: OpenClawConfig,
|
||||
@@ -124,9 +138,9 @@ function readMinimaxProviderConfig(
|
||||
baseUrl: normalizeMinimaxTtsBaseUrl(trimToUndefined(config.baseUrl) ?? normalized.baseUrl),
|
||||
model: trimToUndefined(config.model) ?? normalized.model,
|
||||
voiceId: trimToUndefined(config.voiceId) ?? normalized.voiceId,
|
||||
speed: asFiniteNumber(config.speed) ?? normalized.speed,
|
||||
vol: asFiniteNumber(config.vol) ?? normalized.vol,
|
||||
pitch: asFiniteNumber(config.pitch) ?? normalized.pitch,
|
||||
speed: normalizeMinimaxSpeed(config.speed) ?? normalized.speed,
|
||||
vol: normalizeMinimaxVolume(config.vol) ?? normalized.vol,
|
||||
pitch: normalizeMinimaxPitch(config.pitch) ?? normalized.pitch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,9 +153,9 @@ function readMinimaxOverrides(
|
||||
return {
|
||||
model: trimToUndefined(overrides.model),
|
||||
voiceId: trimToUndefined(overrides.voiceId),
|
||||
speed: asFiniteNumber(overrides.speed),
|
||||
vol: asFiniteNumber(overrides.vol),
|
||||
pitch: asFiniteNumber(overrides.pitch),
|
||||
speed: normalizeMinimaxSpeed(overrides.speed),
|
||||
vol: normalizeMinimaxVolume(overrides.vol),
|
||||
pitch: normalizeMinimaxPitch(overrides.pitch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -236,15 +250,15 @@ export function buildMinimaxSpeechProvider(): SpeechProviderPlugin {
|
||||
...(trimToUndefined(talkProviderConfig.voiceId) == null
|
||||
? {}
|
||||
: { voiceId: trimToUndefined(talkProviderConfig.voiceId) }),
|
||||
...(asFiniteNumber(talkProviderConfig.speed) == null
|
||||
...(normalizeMinimaxSpeed(talkProviderConfig.speed) == null
|
||||
? {}
|
||||
: { speed: asFiniteNumber(talkProviderConfig.speed) }),
|
||||
...(asFiniteNumber(talkProviderConfig.vol) == null
|
||||
: { speed: normalizeMinimaxSpeed(talkProviderConfig.speed) }),
|
||||
...(normalizeMinimaxVolume(talkProviderConfig.vol) == null
|
||||
? {}
|
||||
: { vol: asFiniteNumber(talkProviderConfig.vol) }),
|
||||
...(asFiniteNumber(talkProviderConfig.pitch) == null
|
||||
: { vol: normalizeMinimaxVolume(talkProviderConfig.vol) }),
|
||||
...(normalizeMinimaxPitch(talkProviderConfig.pitch) == null
|
||||
? {}
|
||||
: { pitch: asFiniteNumber(talkProviderConfig.pitch) }),
|
||||
: { pitch: normalizeMinimaxPitch(talkProviderConfig.pitch) }),
|
||||
};
|
||||
},
|
||||
resolveTalkOverrides: ({ params }) => ({
|
||||
@@ -254,9 +268,15 @@ export function buildMinimaxSpeechProvider(): SpeechProviderPlugin {
|
||||
...(trimToUndefined(params.modelId) == null
|
||||
? {}
|
||||
: { model: trimToUndefined(params.modelId) }),
|
||||
...(asFiniteNumber(params.speed) == null ? {} : { speed: asFiniteNumber(params.speed) }),
|
||||
...(asFiniteNumber(params.vol) == null ? {} : { vol: asFiniteNumber(params.vol) }),
|
||||
...(asFiniteNumber(params.pitch) == null ? {} : { pitch: asFiniteNumber(params.pitch) }),
|
||||
...(normalizeMinimaxSpeed(params.speed) == null
|
||||
? {}
|
||||
: { speed: normalizeMinimaxSpeed(params.speed) }),
|
||||
...(normalizeMinimaxVolume(params.vol) == null
|
||||
? {}
|
||||
: { vol: normalizeMinimaxVolume(params.vol) }),
|
||||
...(normalizeMinimaxPitch(params.pitch) == null
|
||||
? {}
|
||||
: { pitch: normalizeMinimaxPitch(params.pitch) }),
|
||||
}),
|
||||
listVoices: async () => MINIMAX_TTS_VOICES.map((voice) => ({ id: voice, name: voice })),
|
||||
isConfigured: ({ cfg, providerConfig }) =>
|
||||
|
||||
@@ -15,6 +15,7 @@ export {
|
||||
readStringValue,
|
||||
} from "../shared/string-coerce.js";
|
||||
export {
|
||||
asFiniteNumberInRange,
|
||||
asFiniteNumber,
|
||||
asPositiveSafeInteger,
|
||||
parseFiniteNumber,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { asFiniteNumber, parseFiniteNumber } from "./number-coercion.js";
|
||||
import { asFiniteNumber, asFiniteNumberInRange, parseFiniteNumber } from "./number-coercion.js";
|
||||
|
||||
describe("number-coercion", () => {
|
||||
test("asFiniteNumber accepts only finite numbers", () => {
|
||||
@@ -9,6 +9,14 @@ describe("number-coercion", () => {
|
||||
expect(asFiniteNumber(Number.POSITIVE_INFINITY)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("asFiniteNumberInRange enforces inclusive and exclusive bounds", () => {
|
||||
expect(asFiniteNumberInRange(0.5, { min: 0.5, max: 2 })).toBe(0.5);
|
||||
expect(asFiniteNumberInRange(2, { min: 0.5, max: 2 })).toBe(2);
|
||||
expect(asFiniteNumberInRange(0.5, { min: 0.5, minExclusive: true })).toBeUndefined();
|
||||
expect(asFiniteNumberInRange(10, { max: 10, maxExclusive: true })).toBeUndefined();
|
||||
expect(asFiniteNumberInRange("1", { min: 0, max: 2 })).toBeUndefined();
|
||||
});
|
||||
|
||||
test("parseFiniteNumber accepts finite numbers and numeric strings", () => {
|
||||
expect(parseFiniteNumber(4)).toBe(4);
|
||||
expect(parseFiniteNumber("4.5")).toBe(4.5);
|
||||
|
||||
@@ -2,6 +2,32 @@ export function asFiniteNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
export function asFiniteNumberInRange(
|
||||
value: unknown,
|
||||
range: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
minExclusive?: boolean;
|
||||
maxExclusive?: boolean;
|
||||
},
|
||||
): number | undefined {
|
||||
const number = asFiniteNumber(value);
|
||||
if (number === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (range.min !== undefined) {
|
||||
if (range.minExclusive ? number <= range.min : number < range.min) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (range.max !== undefined) {
|
||||
if (range.maxExclusive ? number >= range.max : number > range.max) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
export function parseFiniteNumber(value: unknown): number | undefined {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? value : undefined;
|
||||
|
||||
Reference in New Issue
Block a user