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 });
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,17 @@ export const THINKING_CONFIG = {
|
||||
}
|
||||
};
|
||||
|
||||
const MINIMAX_TTS_MODELS = [
|
||||
{ id: "speech-2.8-hd", name: "Speech 2.8 HD" },
|
||||
{ id: "speech-2.8-turbo", name: "Speech 2.8 Turbo" },
|
||||
{ id: "speech-2.6-hd", name: "Speech 2.6 HD" },
|
||||
{ id: "speech-2.6-turbo", name: "Speech 2.6 Turbo" },
|
||||
{ id: "speech-02-hd", name: "Speech 02 HD" },
|
||||
{ id: "speech-02-turbo", name: "Speech 02 Turbo" },
|
||||
{ id: "speech-01-hd", name: "Speech 01 HD" },
|
||||
{ id: "speech-01-turbo", name: "Speech 01 Turbo" },
|
||||
];
|
||||
|
||||
// OAuth Providers
|
||||
export const OAUTH_PROVIDERS = {
|
||||
claude: { id: "claude", alias: "cc", name: "Claude Code", icon: "smart_toy", color: "#D97757", website: "https://claude.ai", notice: { signupUrl: "https://claude.ai" } },
|
||||
@@ -57,8 +68,8 @@ export const APIKEY_PROVIDERS = {
|
||||
glm: { id: "glm", alias: "glm", name: "GLM Coding", icon: "code", color: "#2563EB", textIcon: "GL", website: "https://open.bigmodel.cn", notice: { apiKeyUrl: "https://open.bigmodel.cn/usercenter/apikeys" } },
|
||||
"glm-cn": { id: "glm-cn", alias: "glm-cn", name: "GLM (China)", icon: "code", color: "#DC2626", textIcon: "GC", website: "https://open.bigmodel.cn", notice: { apiKeyUrl: "https://open.bigmodel.cn/usercenter/apikeys" } },
|
||||
kimi: { id: "kimi", alias: "kimi", name: "Kimi", icon: "psychology", color: "#1E3A8A", textIcon: "KM", website: "https://kimi.moonshot.cn", notice: { apiKeyUrl: "https://platform.moonshot.ai/console/api-keys" }, serviceKinds: ["llm", "webSearch"], searchViaChat: { defaultModel: "kimi-k2.5", pricingUrl: "https://platform.moonshot.ai/docs/pricing/chat" } },
|
||||
minimax: { id: "minimax", alias: "minimax", name: "Minimax Coding", icon: "memory", color: "#7C3AED", textIcon: "MM", website: "https://www.minimaxi.com", notice: { apiKeyUrl: "https://platform.minimaxi.com/user-center/basic-information/interface-key" }, serviceKinds: ["llm", "image", "imageToText", "webSearch"], searchViaChat: { defaultModel: "MiniMax-M2.7", pricingUrl: "https://www.minimaxi.com/document/price" } },
|
||||
"minimax-cn": { id: "minimax-cn", alias: "minimax-cn", name: "Minimax (China)", icon: "memory", color: "#DC2626", textIcon: "MC", website: "https://www.minimaxi.com", notice: { apiKeyUrl: "https://platform.minimaxi.com/user-center/basic-information/interface-key" } },
|
||||
minimax: { id: "minimax", alias: "minimax", name: "Minimax Coding", icon: "memory", color: "#7C3AED", textIcon: "MM", website: "https://www.minimaxi.com", notice: { apiKeyUrl: "https://platform.minimaxi.com/user-center/basic-information/interface-key" }, serviceKinds: ["llm", "image", "imageToText", "webSearch", "tts"], searchViaChat: { defaultModel: "MiniMax-M2.7", pricingUrl: "https://www.minimaxi.com/document/price" }, ttsConfig: { baseUrl: "https://api.minimax.io/v1/t2a_v2", authType: "apikey", authHeader: "bearer", format: "minimax-tts", models: MINIMAX_TTS_MODELS } },
|
||||
"minimax-cn": { id: "minimax-cn", alias: "minimax-cn", name: "Minimax (China)", icon: "memory", color: "#DC2626", textIcon: "MC", website: "https://www.minimaxi.com", notice: { apiKeyUrl: "https://platform.minimaxi.com/user-center/basic-information/interface-key" }, serviceKinds: ["llm", "tts"], ttsConfig: { baseUrl: "https://api.minimaxi.com/v1/t2a_v2", authType: "apikey", authHeader: "bearer", format: "minimax-tts", models: MINIMAX_TTS_MODELS } },
|
||||
alicode: { id: "alicode", alias: "alicode", name: "Alibaba", icon: "cloud", color: "#FF6A00", textIcon: "ALi", website: "https://bailian.console.aliyun.com", notice: { apiKeyUrl: "https://bailian.console.aliyun.com/?apiKey=1" } },
|
||||
"alicode-intl": { id: "alicode-intl", alias: "alicode-intl", name: "Alibaba Intl", icon: "cloud", color: "#FF6A00", textIcon: "ALi", website: "https://modelstudio.console.alibabacloud.com", notice: { apiKeyUrl: "https://modelstudio.console.alibabacloud.com/?apiKey=1" } },
|
||||
"xiaomi-mimo": { id: "xiaomi-mimo", alias: "mimo", name: "Xiaomi MiMo", icon: "smart_toy", color: "#FF6900", textIcon: "XM", website: "https://xiaomimimo.com", notice: { apiKeyUrl: "https://xiaomimimo.com" } },
|
||||
|
||||
@@ -109,6 +109,22 @@ export const TTS_PROVIDER_CONFIG = {
|
||||
hasVoiceIdInput: true,
|
||||
voiceSource: "config",
|
||||
},
|
||||
"minimax": {
|
||||
hasModelSelector: true,
|
||||
hasBrowseButton: true,
|
||||
hasVoiceIdInput: true,
|
||||
voiceSource: "api-language",
|
||||
apiEndpoint: "/api/media-providers/tts/minimax/voices",
|
||||
defaultVoiceId: "English_expressive_narrator",
|
||||
},
|
||||
"minimax-cn": {
|
||||
hasModelSelector: true,
|
||||
hasBrowseButton: true,
|
||||
hasVoiceIdInput: true,
|
||||
voiceSource: "api-language",
|
||||
apiEndpoint: "/api/media-providers/tts/minimax/voices?provider=minimax-cn",
|
||||
defaultVoiceId: "English_expressive_narrator",
|
||||
},
|
||||
"gemini": {
|
||||
hasLanguageDropdown: false,
|
||||
hasLanguageHint: true, // sends body.language to guide TTS pronunciation
|
||||
|
||||
Reference in New Issue
Block a user