fix: Opus review pass — harden before building
- Widen voice ID validation to 20-64 alphanumeric (future-proof) - Remove hardcoded default voiceId (SJ personal clone) - Require voiceId in isConfigured + synthesize guard with clear error - Add model header comment explaining Fish Audio's non-standard API - Truncate error bodies to 500 chars to prevent log pollution - Update tests and README to match
This commit is contained in:
@@ -11,7 +11,10 @@ import { fishAudioTTS, listFishAudioVoices } from "./tts.js";
|
||||
// ── Defaults ────────────────────────────────────────────────────────────────
|
||||
|
||||
const DEFAULT_FISH_AUDIO_BASE_URL = "https://api.fish.audio";
|
||||
const DEFAULT_VOICE_ID = "8a2d42279389471993460b85340235c5"; // SJ voice
|
||||
// No default voice — users must configure one. Fish Audio has no universal
|
||||
// "default" voice like ElevenLabs does, and shipping a personal clone ID
|
||||
// as default would be wrong for community users.
|
||||
const DEFAULT_VOICE_ID = "";
|
||||
const DEFAULT_MODEL = "s2-pro";
|
||||
const DEFAULT_LATENCY = "normal" as const;
|
||||
|
||||
@@ -67,9 +70,11 @@ function normalizeModel(value: unknown): string {
|
||||
return s || DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
/** Fish Audio ref IDs are 32-char hex strings */
|
||||
/** Fish Audio voice ref IDs — alphanumeric, 20-64 chars. Permissive enough
|
||||
* to handle future ID format changes while still rejecting path traversal
|
||||
* and injection attempts. */
|
||||
export function isValidFishAudioVoiceId(voiceId: string): boolean {
|
||||
return /^[a-f0-9]{24,40}$/i.test(voiceId);
|
||||
return /^[a-zA-Z0-9]{20,64}$/.test(voiceId);
|
||||
}
|
||||
|
||||
// ── Config resolution ───────────────────────────────────────────────────────
|
||||
@@ -270,11 +275,12 @@ export function buildFishAudioSpeechProvider(): SpeechProviderPlugin {
|
||||
return raw as SpeechVoiceOption[];
|
||||
},
|
||||
|
||||
isConfigured: ({ providerConfig }) =>
|
||||
Boolean(
|
||||
readFishAudioProviderConfig(providerConfig).apiKey ||
|
||||
process.env.FISH_AUDIO_API_KEY,
|
||||
),
|
||||
isConfigured: ({ providerConfig }) => {
|
||||
const config = readFishAudioProviderConfig(providerConfig);
|
||||
const hasKey = Boolean(config.apiKey || process.env.FISH_AUDIO_API_KEY);
|
||||
const hasVoice = Boolean(config.voiceId);
|
||||
return hasKey && hasVoice;
|
||||
},
|
||||
|
||||
synthesize: async (req) => {
|
||||
const config = readFishAudioProviderConfig(req.providerConfig);
|
||||
@@ -285,6 +291,13 @@ export function buildFishAudioSpeechProvider(): SpeechProviderPlugin {
|
||||
throw new Error("Fish Audio API key missing");
|
||||
}
|
||||
|
||||
const voiceId = trimToUndefined(overrides.voiceId) ?? config.voiceId;
|
||||
if (!voiceId) {
|
||||
throw new Error(
|
||||
"Fish Audio: no voiceId configured. Set messages.tts.providers.fish-audio.voiceId",
|
||||
);
|
||||
}
|
||||
|
||||
// Pick format based on target channel
|
||||
const useOpus = req.target === "voice-note";
|
||||
const format = useOpus ? "opus" : "mp3";
|
||||
@@ -298,7 +311,7 @@ export function buildFishAudioSpeechProvider(): SpeechProviderPlugin {
|
||||
text: req.text,
|
||||
apiKey,
|
||||
baseUrl: config.baseUrl,
|
||||
referenceId: trimToUndefined(overrides.voiceId) ?? config.voiceId,
|
||||
referenceId: voiceId,
|
||||
model: trimToUndefined(overrides.model) ?? config.model,
|
||||
format,
|
||||
latency:
|
||||
|
||||
Reference in New Issue
Block a user