Feat : tts
This commit is contained in:
1 parent
39545cf4c8
commit
3c96e8d6d1
40 files changed
+1896
-147
No files matched your search
@@ -0,0 +1,71 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnections } from "@/lib/localDb";
|
||||
import { fetchElevenLabsVoices } from "open-sse/handlers/ttsCore.js";
|
||||
|
||||
const langNames = new Intl.DisplayNames(["en"], { type: "language" });
|
||||
|
||||
/**
|
||||
* GET /api/media-providers/tts/elevenlabs/voices[?lang=en]
|
||||
* Returns { languages, byLang } grouped by language - same format as edge-tts
|
||||
* Uses direct DB read (no mutex) to avoid blocking on concurrent TTS requests
|
||||
*/
|
||||
export async function GET(request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const langFilter = searchParams.get("lang");
|
||||
|
||||
// Direct DB read - bypass auth mutex used for TTS inference
|
||||
const connections = await getProviderConnections({ provider: "elevenlabs", isActive: true });
|
||||
const apiKey = connections[0]?.apiKey;
|
||||
if (!apiKey) {
|
||||
return NextResponse.json({ error: "No ElevenLabs connection found" }, { status: 400 });
|
||||
}
|
||||
|
||||
const voices = await fetchElevenLabsVoices(apiKey);
|
||||
|
||||
// Group by all supported languages (verified_languages + labels.language)
|
||||
const byLang = {};
|
||||
const addToLang = (code, voice) => {
|
||||
if (!byLang[code]) {
|
||||
byLang[code] = {
|
||||
code,
|
||||
name: (() => { try { return langNames.of(code); } catch { return code; } })(),
|
||||
voices: [],
|
||||
};
|
||||
}
|
||||
// Avoid duplicate voice in same lang
|
||||
if (!byLang[code].voices.find((v) => v.id === voice.voice_id)) {
|
||||
byLang[code].voices.push({
|
||||
id: voice.voice_id,
|
||||
name: voice.name,
|
||||
gender: voice.labels?.gender || "",
|
||||
lang: code,
|
||||
// premade voices are free; professional library voices added to account may require paid plan
|
||||
free_users_allowed: voice.category === "premade" || voice.is_owner === true
|
||||
});
|
||||
}
|
||||
};
|
||||
for (const v of voices) {
|
||||
// Add to primary language
|
||||
const primaryLang = v.labels?.language || "en";
|
||||
addToLang(primaryLang, v);
|
||||
// Add to all verified languages
|
||||
for (const vl of v.verified_languages || []) {
|
||||
if (vl.language && vl.language !== primaryLang) {
|
||||
addToLang(vl.language, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const languages = Object.values(byLang).sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// If lang filter requested, return only that group's voices
|
||||
if (langFilter) {
|
||||
return NextResponse.json({ voices: byLang[langFilter]?.voices || [] });
|
||||
}
|
||||
|
||||
return NextResponse.json({ languages, byLang });
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: err.message || "Failed to fetch voices" }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { VOICE_FETCHERS } from "open-sse/handlers/ttsCore.js";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// Map locale code → country name
|
||||
const LOCALE_NAMES = new Intl.DisplayNames(["en"], { type: "region" });
|
||||
const LANG_NAMES = new Intl.DisplayNames(["en"], { type: "language" });
|
||||
|
||||
function countryName(code) {
|
||||
try { return LOCALE_NAMES.of(code); } catch { return code; }
|
||||
}
|
||||
function langName(code) {
|
||||
try { return LANG_NAMES.of(code); } catch { return code; }
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/media-providers/tts/voices
|
||||
* Query:
|
||||
* ?provider=edge-tts | local-device | elevenlabs (default: edge-tts)
|
||||
* ?lang=en (optional filter by lang code)
|
||||
* ?apiKey=xxx (required for elevenlabs)
|
||||
*/
|
||||
export async function GET(request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const provider = searchParams.get("provider") || "edge-tts";
|
||||
const langFilter = searchParams.get("lang");
|
||||
const apiKey = searchParams.get("apiKey");
|
||||
|
||||
const fetcher = VOICE_FETCHERS[provider];
|
||||
if (!fetcher) {
|
||||
return NextResponse.json({ error: `Provider '${provider}' does not support voice listing` }, { status: 400 });
|
||||
}
|
||||
|
||||
// ElevenLabs requires API key
|
||||
const raw = provider === "elevenlabs" ? await fetcher(apiKey) : await fetcher();
|
||||
let voices;
|
||||
|
||||
if (provider === "local-device") {
|
||||
voices = raw.map((v) => ({
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
locale: v.locale.replace("_", "-"),
|
||||
lang: v.lang,
|
||||
country: v.country,
|
||||
countryName: countryName(v.country),
|
||||
langName: langName(v.lang),
|
||||
gender: v.gender,
|
||||
}));
|
||||
} else if (provider === "elevenlabs") {
|
||||
voices = raw.map((v) => ({
|
||||
id: v.voice_id,
|
||||
name: v.name,
|
||||
locale: v.labels?.language || "en",
|
||||
lang: (v.labels?.language || "en").split("-")[0],
|
||||
country: "",
|
||||
countryName: "",
|
||||
langName: langName((v.labels?.language || "en").split("-")[0]),
|
||||
gender: v.labels?.gender || "",
|
||||
category: v.category,
|
||||
}));
|
||||
} else {
|
||||
// edge-tts (default)
|
||||
voices = raw.map((v) => {
|
||||
const [lang, country] = v.Locale.split("-");
|
||||
return {
|
||||
id: v.ShortName,
|
||||
name: (v.FriendlyName || v.ShortName)
|
||||
.replace("Microsoft ", "")
|
||||
.replace(/ Online \(Natural\) - /g, " ("),
|
||||
locale: v.Locale,
|
||||
lang,
|
||||
country: country || "",
|
||||
countryName: countryName(country || lang),
|
||||
langName: langName(lang),
|
||||
gender: v.Gender,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Apply filter
|
||||
if (langFilter) voices = voices.filter((v) => v.lang === langFilter);
|
||||
|
||||
// Group by language
|
||||
const byLang = {};
|
||||
for (const v of voices) {
|
||||
const key = v.lang;
|
||||
if (!byLang[key]) byLang[key] = { code: key, name: v.langName, voices: [] };
|
||||
byLang[key].voices.push(v);
|
||||
}
|
||||
|
||||
// Sorted language list
|
||||
const languages = Object.values(byLang).sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return NextResponse.json({ voices, languages, byLang });
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: err.message || "Failed to fetch voices" }, { status: 502 });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user