mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
feat: add minimax tts support (#1043)
This commit is contained in:
@@ -372,9 +372,9 @@ function TtsExampleCard({ providerId }) {
|
||||
const config = TTS_PROVIDER_CONFIG[providerId] || TTS_PROVIDER_CONFIG["edge-tts"];
|
||||
|
||||
// Voice state
|
||||
const [selectedVoice, setSelectedVoice] = useState("");
|
||||
const [selectedVoice, setSelectedVoice] = useState(config.defaultVoiceId || "");
|
||||
const [selectedVoiceName, setSelectedVoiceName] = useState("");
|
||||
const [voiceId, setVoiceId] = useState(""); // editable voice id (elevenlabs)
|
||||
const [voiceId, setVoiceId] = useState(config.defaultVoiceId || ""); // editable voice id (elevenlabs/config providers)
|
||||
// Voices shown below Voice row after language selected
|
||||
const [countryVoices, setCountryVoices] = useState([]);
|
||||
const [selectedLang, setSelectedLang] = useState("");
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnections } from "@/lib/localDb";
|
||||
|
||||
const MINIMAX_VOICE_ENDPOINTS = {
|
||||
minimax: "https://api.minimax.io/v1/get_voice",
|
||||
"minimax-cn": "https://api.minimaxi.com/v1/get_voice",
|
||||
};
|
||||
|
||||
const VOICE_GROUPS = [
|
||||
{ key: "system_voice", label: "System" },
|
||||
{ key: "voice_cloning", label: "Cloned" },
|
||||
{ key: "voice_generation", label: "Generated" },
|
||||
{ key: "music_generation", label: "Music" },
|
||||
];
|
||||
|
||||
function inferLanguage(voiceId) {
|
||||
const value = typeof voiceId === "string" ? voiceId.trim() : "";
|
||||
if (!value.includes("_")) return "Custom";
|
||||
return value.split("_")[0] || "Custom";
|
||||
}
|
||||
|
||||
function addVoice(byLang, code, voice) {
|
||||
if (!byLang[code]) byLang[code] = { code, name: code, voices: [] };
|
||||
if (byLang[code].voices.some((v) => v.id === voice.id)) return;
|
||||
byLang[code].voices.push(voice);
|
||||
}
|
||||
|
||||
function normalizeMiniMaxVoices(data) {
|
||||
const byLang = {};
|
||||
|
||||
for (const group of VOICE_GROUPS) {
|
||||
const voices = Array.isArray(data?.[group.key]) ? data[group.key] : [];
|
||||
for (const item of voices) {
|
||||
const voiceId = item?.voice_id || item?.voiceId;
|
||||
if (!voiceId) continue;
|
||||
|
||||
const voiceName = item?.voice_name || item?.voiceName || voiceId;
|
||||
const lang = group.key === "system_voice" ? inferLanguage(voiceId) : "Custom";
|
||||
addVoice(byLang, lang, {
|
||||
id: voiceId,
|
||||
name: group.key === "system_voice" ? voiceName : `${voiceName} · ${group.label}`,
|
||||
lang,
|
||||
category: group.key,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const languages = Object.values(byLang).sort((a, b) => {
|
||||
if (a.code === "Custom") return 1;
|
||||
if (b.code === "Custom") return -1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
for (const lang of languages) {
|
||||
lang.voices.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
return { languages, byLang };
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/media-providers/tts/minimax/voices[?provider=minimax|minimax-cn&voice_type=all]
|
||||
* Returns { languages, byLang } grouped for the shared TTS voice picker.
|
||||
*/
|
||||
export async function GET(request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const provider = searchParams.get("provider") === "minimax-cn" ? "minimax-cn" : "minimax";
|
||||
const voiceType = searchParams.get("voice_type") || "all";
|
||||
const langFilter = searchParams.get("lang");
|
||||
|
||||
const connections = await getProviderConnections({ provider, isActive: true });
|
||||
const apiKey = connections[0]?.apiKey;
|
||||
if (!apiKey) {
|
||||
return NextResponse.json({ error: `No ${provider} connection found` }, { status: 400 });
|
||||
}
|
||||
|
||||
const res = await fetch(MINIMAX_VOICE_ENDPOINTS[provider], {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ voice_type: voiceType }),
|
||||
});
|
||||
|
||||
const rawText = await res.text();
|
||||
let data = {};
|
||||
if (rawText) {
|
||||
try { data = JSON.parse(rawText); } catch { data = {}; }
|
||||
}
|
||||
|
||||
const baseResp = data.base_resp || data.baseResp || {};
|
||||
const statusCode = Number(baseResp.status_code ?? baseResp.statusCode ?? 0);
|
||||
const statusMessage = baseResp.status_msg || baseResp.statusMsg || data.message || "";
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json({ error: `MiniMax API ${res.status}: ${statusMessage || rawText || "Failed"}` }, { status: 502 });
|
||||
}
|
||||
if (statusCode !== 0) {
|
||||
return NextResponse.json({ error: statusMessage || "MiniMax voice API error" }, { status: 502 });
|
||||
}
|
||||
|
||||
const normalized = normalizeMiniMaxVoices(data);
|
||||
if (langFilter) {
|
||||
return NextResponse.json({ voices: normalized.byLang[langFilter]?.voices || [] });
|
||||
}
|
||||
|
||||
return NextResponse.json(normalized);
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: err.message || "Failed to fetch MiniMax voices" }, { status: 502 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user