projects
This commit is contained in:
@@ -54,7 +54,7 @@ ttsRouter.put('/', async (ctx) => {
|
||||
});
|
||||
|
||||
ttsRouter.post('/voices', async (ctx) => {
|
||||
const body = await ctx.req.json<{ provider: string; url?: string; apiKey?: string }>();
|
||||
const body = await ctx.req.json<{ provider: string; url?: string; apiKey?: string; model?: string }>();
|
||||
|
||||
try {
|
||||
if (body.provider === 'elevenlabs') {
|
||||
@@ -67,20 +67,88 @@ ttsRouter.post('/voices', async (ctx) => {
|
||||
return ctx.json({ voices: json.voices.map((v) => v.voice_id) });
|
||||
}
|
||||
|
||||
// OpenAI-compatible
|
||||
// OpenAI-compatible: try local server first, fallback to HuggingFace
|
||||
if (!body.url) return ctx.json({ error: 'URL required' }, 400);
|
||||
const headers: Record<string, string> = {};
|
||||
if (body.apiKey) headers['Authorization'] = `Bearer ${body.apiKey}`;
|
||||
const res = await fetch(`${body.url.replace(/\/+$/, '')}/v1/audio/voices`, { headers });
|
||||
if (!res.ok) return ctx.json({ error: `Voices fetch failed: ${res.status}` }, 500);
|
||||
const json = (await res.json()) as { voices: string[] };
|
||||
return ctx.json({ voices: json.voices });
|
||||
|
||||
// Try /v1/audio/voices on the local server
|
||||
const localRes = await fetch(`${body.url.replace(/\/+$/, '')}/v1/audio/voices`, { headers }).catch(() => null);
|
||||
if (localRes?.ok) {
|
||||
const json = (await localRes.json()) as { voices: string[] };
|
||||
return ctx.json({ voices: json.voices });
|
||||
}
|
||||
|
||||
// Fallback: get model repo from /v1/models, then list voices from HuggingFace
|
||||
const modelsRes = await fetch(`${body.url.replace(/\/+$/, '')}/v1/models`, { headers }).catch(() => null);
|
||||
if (modelsRes?.ok) {
|
||||
const modelsJson = (await modelsRes.json()) as { data?: { id: string }[] };
|
||||
const repoId = modelsJson.data?.[0]?.id;
|
||||
if (repoId) {
|
||||
const result = await fetchHuggingFaceVoices(repoId);
|
||||
if (result.flat.length > 0) return ctx.json({ voices: result.flat, groups: result.groups });
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: try model field as HuggingFace repo ID directly
|
||||
if (body.model && body.model.includes('/')) {
|
||||
const result = await fetchHuggingFaceVoices(body.model);
|
||||
if (result.flat.length > 0) return ctx.json({ voices: result.flat, groups: result.groups });
|
||||
}
|
||||
|
||||
return ctx.json({ error: 'Could not fetch voices from server or HuggingFace' }, 500);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return ctx.json({ error: message }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
const VOICE_GROUP_LABELS: Record<string, string> = {
|
||||
af: 'American Female',
|
||||
am: 'American Male',
|
||||
bf: 'British Female',
|
||||
bm: 'British Male',
|
||||
ef: 'Spanish Female',
|
||||
em: 'Spanish Male',
|
||||
ff: 'French Female',
|
||||
hf: 'Hindi Female',
|
||||
hm: 'Hindi Male',
|
||||
if: 'Italian Female',
|
||||
im: 'Italian Male',
|
||||
jf: 'Japanese Female',
|
||||
jm: 'Japanese Male',
|
||||
pf: 'Brazilian Portuguese Female',
|
||||
pm: 'Brazilian Portuguese Male',
|
||||
zf: 'Mandarin Chinese Female',
|
||||
zm: 'Mandarin Chinese Male',
|
||||
};
|
||||
|
||||
type VoiceGroup = { label: string; voices: string[] };
|
||||
|
||||
async function fetchHuggingFaceVoices(repoId: string): Promise<{ flat: string[]; groups: VoiceGroup[] }> {
|
||||
const res = await fetch(`https://huggingface.co/api/models/${repoId}/tree/main/voices`);
|
||||
if (!res.ok) return { flat: [], groups: [] };
|
||||
const files = (await res.json()) as { path: string; type: string }[];
|
||||
const names = new Set<string>();
|
||||
for (const f of files) {
|
||||
if (f.type !== 'file') continue;
|
||||
const name = f.path.replace('voices/', '').replace(/\.(pt|safetensors)$/, '');
|
||||
names.add(name);
|
||||
}
|
||||
const sorted = [...names].sort();
|
||||
const groupMap = new Map<string, string[]>();
|
||||
for (const name of sorted) {
|
||||
const prefix = name.slice(0, 2);
|
||||
if (!groupMap.has(prefix)) groupMap.set(prefix, []);
|
||||
groupMap.get(prefix)!.push(name);
|
||||
}
|
||||
const groups: VoiceGroup[] = [];
|
||||
for (const [prefix, voices] of groupMap) {
|
||||
groups.push({ label: VOICE_GROUP_LABELS[prefix] ?? prefix, voices });
|
||||
}
|
||||
return { flat: sorted, groups };
|
||||
}
|
||||
|
||||
ttsRouter.post('/test', async (ctx) => {
|
||||
const body = await ctx.req.json<TtsConfig>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user