sync: match upstream PR #56891 at rebase 2026-03-30
Brings Gitea mirror up to date with the current state of the openclaw/openclaw PR branch, including all fixes from Codex review: - Namespaced directive keys (fishaudio_*/fish_* prefixes only) - Strict latency directive validation with warnings - Code quality cleanup, s2 model removal - Contract and directive parsing tests - README updated with prefixed directive docs Source: Conan-Scott/openclaw@9787ef6e (feat/fish-audio-speech-provider)
This commit is contained in:
85
tts.ts
85
tts.ts
@@ -1,6 +1,6 @@
|
||||
const DEFAULT_FISH_AUDIO_BASE_URL = "https://api.fish.audio";
|
||||
export const DEFAULT_FISH_AUDIO_BASE_URL = "https://api.fish.audio";
|
||||
|
||||
function normalizeFishAudioBaseUrl(baseUrl?: string): string {
|
||||
export function normalizeFishAudioBaseUrl(baseUrl?: string): string {
|
||||
const trimmed = baseUrl?.trim();
|
||||
if (!trimmed) {
|
||||
return DEFAULT_FISH_AUDIO_BASE_URL;
|
||||
@@ -115,67 +115,32 @@ export async function listFishAudioVoices(params: {
|
||||
}): Promise<Array<{ id: string; name: string }>> {
|
||||
const base = normalizeFishAudioBaseUrl(params.baseUrl);
|
||||
|
||||
// Two parallel calls: official voices + user's own voices
|
||||
const [officialRes, selfRes] = await Promise.allSettled([
|
||||
fetch(`${base}/model?type=tts&author_id=d8b0991f96b44e489422ca2ddf0bd31d&page_size=100`, {
|
||||
headers: { Authorization: `Bearer ${params.apiKey}` },
|
||||
}),
|
||||
fetch(`${base}/model?type=tts&self=true&page_size=100`, {
|
||||
headers: { Authorization: `Bearer ${params.apiKey}` },
|
||||
}),
|
||||
]);
|
||||
// List the authenticated user's own voices (cloned/trained).
|
||||
// Fish Audio has no stable API for fetching a curated "official" voice
|
||||
// catalogue — the public model listing returns the entire community corpus
|
||||
// (1M+ entries) and filtering by undocumented author IDs would be fragile.
|
||||
// Users can browse and select voices at https://fish.audio and configure
|
||||
// their chosen voiceId directly.
|
||||
const res = await fetch(`${base}/model?type=tts&self=true&page_size=100`, {
|
||||
headers: { Authorization: `Bearer ${params.apiKey}` },
|
||||
});
|
||||
|
||||
const voices = new Map<string, string>();
|
||||
|
||||
// Process official voices first
|
||||
if (officialRes.status === "fulfilled" && officialRes.value.ok) {
|
||||
const json = (await officialRes.value.json()) as {
|
||||
items?: Array<{ _id?: string; title?: string }>;
|
||||
};
|
||||
if (Array.isArray(json.items)) {
|
||||
for (const v of json.items) {
|
||||
const id = v._id?.trim();
|
||||
const name = v.title?.trim();
|
||||
if (id) {
|
||||
voices.set(id, name || id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(`Fish Audio voices API error (${res.status})`);
|
||||
}
|
||||
|
||||
// User's own voices take precedence on conflict
|
||||
if (selfRes.status === "fulfilled" && selfRes.value.ok) {
|
||||
const json = (await selfRes.value.json()) as {
|
||||
items?: Array<{ _id?: string; title?: string }>;
|
||||
};
|
||||
if (Array.isArray(json.items)) {
|
||||
for (const v of json.items) {
|
||||
const id = v._id?.trim();
|
||||
const name = v.title?.trim();
|
||||
if (id) {
|
||||
voices.set(id, name ? `${name} (mine)` : id);
|
||||
}
|
||||
}
|
||||
}
|
||||
const json = (await res.json()) as {
|
||||
items?: Array<{ _id?: string; title?: string }>;
|
||||
};
|
||||
|
||||
if (!Array.isArray(json.items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// If both calls failed, throw
|
||||
if (voices.size === 0) {
|
||||
const errors: string[] = [];
|
||||
if (officialRes.status === "rejected") {
|
||||
errors.push(`official: ${officialRes.reason}`);
|
||||
} else if (!officialRes.value.ok) {
|
||||
errors.push(`official: HTTP ${officialRes.value.status}`);
|
||||
}
|
||||
if (selfRes.status === "rejected") {
|
||||
errors.push(`self: ${selfRes.reason}`);
|
||||
} else if (!selfRes.value.ok) {
|
||||
errors.push(`self: HTTP ${selfRes.value.status}`);
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Fish Audio voices API error: ${errors.join("; ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(voices.entries()).map(([id, name]) => ({ id, name }));
|
||||
return json.items
|
||||
.map((v) => ({
|
||||
id: v._id?.trim() ?? "",
|
||||
name: v.title?.trim() || v._id?.trim() || "",
|
||||
}))
|
||||
.filter((v) => v.id.length > 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user