- index.ts: plugin entry with definePluginEntry + registerSpeechProvider - speech-provider.ts: full SpeechProviderPlugin implementation - resolveConfig from messages.tts.providers.fish-audio - parseDirectiveToken for voice, model, speed, latency, temperature, top_p - listVoices merging official + user's own voices - synthesize with format-aware output (opus for voice-note, mp3 otherwise) - stub Talk Mode (resolveTalkConfig/resolveTalkOverrides) - tts.ts: raw fishAudioTTS() fetch + listFishAudioVoices() - streaming chunked → buffer, error body included in exceptions - parallel voice listing with graceful partial failure - speech-provider.test.ts: voice ID validation tests - openclaw.plugin.json: speechProviders contract - package.json: peer dep on openclaw >=2026.3.0
40 lines
1.4 KiB
TypeScript
40 lines
1.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { isValidFishAudioVoiceId } from "./speech-provider.js";
|
|
|
|
describe("fish-audio speech provider", () => {
|
|
describe("isValidFishAudioVoiceId", () => {
|
|
it("accepts valid Fish Audio ref IDs (24-40 char hex)", () => {
|
|
const valid = [
|
|
"8a2d42279389471993460b85340235c5", // 32 char - standard
|
|
"0dad9e24630447cf97803f4beee10481", // 32 char
|
|
"5796fe24630447cf97803f4beee10481", // 32 char
|
|
"d8b0991f96b44e489422ca2ddf0bd31d", // 32 char - author id
|
|
"aabbccddee112233445566778899", // 28 char
|
|
"aabbccddee11223344556677", // 24 char (minimum)
|
|
];
|
|
for (const v of valid) {
|
|
expect(isValidFishAudioVoiceId(v), `expected valid: ${v}`).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("rejects invalid voice IDs", () => {
|
|
const invalid = [
|
|
"", // empty
|
|
"abc123", // too short
|
|
"12345678901234567890123", // 23 chars - below minimum
|
|
"a".repeat(41), // too long
|
|
"8a2d4227-9389-4719-9346-0b85340235c5", // UUID with dashes
|
|
"../../../etc/passwd", // path traversal
|
|
"voice?param=value", // query string
|
|
"pMsXgVXv3BLzUgSXRplE", // ElevenLabs-style (mixed case, 20 chars)
|
|
"ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ", // non-hex chars
|
|
];
|
|
for (const v of invalid) {
|
|
expect(isValidFishAudioVoiceId(v), `expected invalid: ${v}`).toBe(
|
|
false,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
});
|