mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-17 04:01:05 +00:00
196 lines
5.3 KiB
TypeScript
196 lines
5.3 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { buildOpenAISpeechProvider } from "./speech-provider.js";
|
|
|
|
function isSpeechRequestBody(value: unknown): value is { response_format?: string } {
|
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function parseRequestBody(init: RequestInit | undefined): { response_format?: string } {
|
|
if (typeof init?.body !== "string") {
|
|
throw new Error("expected string request body");
|
|
}
|
|
const body: unknown = JSON.parse(init.body);
|
|
if (!isSpeechRequestBody(body)) {
|
|
throw new Error("expected OpenAI speech request body");
|
|
}
|
|
return body;
|
|
}
|
|
|
|
describe("buildOpenAISpeechProvider", () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("normalizes provider-owned speech config from raw provider config", () => {
|
|
const provider = buildOpenAISpeechProvider();
|
|
const resolved = provider.resolveConfig?.({
|
|
cfg: {} as never,
|
|
timeoutMs: 30_000,
|
|
rawConfig: {
|
|
providers: {
|
|
openai: {
|
|
apiKey: "sk-test",
|
|
baseUrl: "https://example.com/v1/",
|
|
model: "tts-1",
|
|
voice: "alloy",
|
|
speed: 1.25,
|
|
instructions: " Speak warmly ",
|
|
responseFormat: " WAV ",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(resolved).toEqual({
|
|
apiKey: "sk-test",
|
|
baseUrl: "https://example.com/v1",
|
|
model: "tts-1",
|
|
voice: "alloy",
|
|
speed: 1.25,
|
|
instructions: "Speak warmly",
|
|
responseFormat: "wav",
|
|
});
|
|
});
|
|
|
|
it("parses OpenAI directive tokens against the resolved base url", () => {
|
|
const provider = buildOpenAISpeechProvider();
|
|
|
|
expect(
|
|
provider.parseDirectiveToken?.({
|
|
key: "voice",
|
|
value: "alloy",
|
|
policy: {
|
|
allowVoice: true,
|
|
allowModelId: true,
|
|
},
|
|
providerConfig: {
|
|
baseUrl: "https://api.openai.com/v1/",
|
|
},
|
|
} as never),
|
|
).toEqual({
|
|
handled: true,
|
|
overrides: { voice: "alloy" },
|
|
});
|
|
|
|
expect(
|
|
provider.parseDirectiveToken?.({
|
|
key: "model",
|
|
value: "kokoro-custom-model",
|
|
policy: {
|
|
allowVoice: true,
|
|
allowModelId: true,
|
|
},
|
|
providerConfig: {
|
|
baseUrl: "https://api.openai.com/v1/",
|
|
},
|
|
} as never),
|
|
).toEqual({
|
|
handled: false,
|
|
});
|
|
});
|
|
|
|
it("preserves talk responseFormat overrides", () => {
|
|
const provider = buildOpenAISpeechProvider();
|
|
|
|
expect(
|
|
provider.resolveTalkConfig?.({
|
|
cfg: {} as never,
|
|
timeoutMs: 30_000,
|
|
baseTtsConfig: {
|
|
providers: {
|
|
openai: {
|
|
apiKey: "sk-base",
|
|
responseFormat: "mp3",
|
|
},
|
|
},
|
|
},
|
|
talkProviderConfig: {
|
|
apiKey: "sk-talk",
|
|
responseFormat: " WAV ",
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
apiKey: "sk-talk",
|
|
responseFormat: "wav",
|
|
});
|
|
});
|
|
|
|
it("maps Talk speak params onto OpenAI speech overrides", () => {
|
|
const provider = buildOpenAISpeechProvider();
|
|
|
|
expect(
|
|
provider.resolveTalkOverrides?.({
|
|
talkProviderConfig: {},
|
|
params: {
|
|
text: "Hello from talk mode.",
|
|
voiceId: "nova",
|
|
modelId: "tts-1",
|
|
speed: 218 / 175,
|
|
},
|
|
}),
|
|
).toEqual({
|
|
voice: "nova",
|
|
model: "tts-1",
|
|
speed: 218 / 175,
|
|
});
|
|
});
|
|
|
|
it("uses wav for Groq-compatible OpenAI TTS endpoints", async () => {
|
|
const provider = buildOpenAISpeechProvider();
|
|
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
|
|
const body = parseRequestBody(init);
|
|
expect(body.response_format).toBe("wav");
|
|
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
|
|
});
|
|
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
|
|
const result = await provider.synthesize({
|
|
text: "hello",
|
|
cfg: {} as never,
|
|
providerConfig: {
|
|
apiKey: "sk-test",
|
|
baseUrl: "https://api.groq.com/openai/v1",
|
|
model: "canopylabs/orpheus-v1-english",
|
|
voice: "daniel",
|
|
},
|
|
target: "audio-file",
|
|
timeoutMs: 1_000,
|
|
});
|
|
|
|
expect(result.outputFormat).toBe("wav");
|
|
expect(result.fileExtension).toBe(".wav");
|
|
expect(result.voiceCompatible).toBe(false);
|
|
});
|
|
|
|
it("honors explicit responseFormat overrides and clears voice-note compatibility when not opus", async () => {
|
|
const provider = buildOpenAISpeechProvider();
|
|
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
|
|
const body = parseRequestBody(init);
|
|
expect(body.response_format).toBe("wav");
|
|
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
|
|
});
|
|
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
|
|
const result = await provider.synthesize({
|
|
text: "hello",
|
|
cfg: {} as never,
|
|
providerConfig: {
|
|
apiKey: "sk-test",
|
|
baseUrl: "https://proxy.example.com/openai/v1",
|
|
model: "canopylabs/orpheus-v1-english",
|
|
voice: "daniel",
|
|
responseFormat: "wav",
|
|
},
|
|
target: "voice-note",
|
|
timeoutMs: 1_000,
|
|
});
|
|
|
|
expect(result.outputFormat).toBe("wav");
|
|
expect(result.fileExtension).toBe(".wav");
|
|
expect(result.voiceCompatible).toBe(false);
|
|
});
|
|
});
|