feat: add minimax tts support (#1043)

This commit is contained in:
Thiên Toán
2026-05-13 15:34:10 +07:00
committed by GitHub
parent 3c2503c4b4
commit 74c9879e8e
10 changed files with 578 additions and 32 deletions
@@ -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 });
}
}
+13 -2
View File
@@ -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" } },
+16
View File
@@ -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