Feat : tts

This commit is contained in:
decolua committed 2026-04-10 10:17:53 +07:00
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 });
}
}