feat: add minimax tts support (#1043)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// Generic config-driven TTS handlers — dispatched by ttsConfig.format.
|
||||
// Each handler accepts { baseUrl, apiKey, text, modelId, voiceId } and returns { base64, format }.
|
||||
import { responseToBase64, throwUpstreamError } from "./_base.js";
|
||||
import minimaxTts from "./minimax.js";
|
||||
|
||||
// Hyperbolic: POST { text } → { audio: base64 }
|
||||
async function hyperbolic({ baseUrl, apiKey, text }) {
|
||||
@@ -164,4 +165,5 @@ export const FORMAT_HANDLERS = {
|
||||
coqui,
|
||||
tortoise,
|
||||
openai: openaiCompat,
|
||||
"minimax-tts": minimaxTts,
|
||||
};
|
||||
|
||||
59
open-sse/handlers/ttsProviders/minimax.js
Normal file
59
open-sse/handlers/ttsProviders/minimax.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
function hexToBase64(audioHex) {
|
||||
const clean = typeof audioHex === "string" ? audioHex.trim() : "";
|
||||
if (!clean) throw new Error("MiniMax TTS returned no audio");
|
||||
if (clean.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(clean)) {
|
||||
throw new Error("MiniMax TTS returned invalid audio");
|
||||
}
|
||||
return Buffer.from(clean, "hex").toString("base64");
|
||||
}
|
||||
|
||||
// MiniMax T2A HTTP: returns hex-encoded audio in non-streaming mode.
|
||||
export default async function minimaxTts({ baseUrl, apiKey, text, modelId, voiceId }) {
|
||||
const res = await fetch(baseUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
|
||||
body: JSON.stringify({
|
||||
model: modelId || "speech-2.8-hd",
|
||||
text,
|
||||
stream: false,
|
||||
language_boost: "auto",
|
||||
output_format: "hex",
|
||||
voice_setting: {
|
||||
voice_id: voiceId || "English_expressive_narrator",
|
||||
speed: 1,
|
||||
vol: 1,
|
||||
pitch: 0,
|
||||
},
|
||||
audio_setting: {
|
||||
sample_rate: 32000,
|
||||
bitrate: 128000,
|
||||
format: "mp3",
|
||||
channel: 1,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
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) {
|
||||
throw new Error(statusMessage || rawText || `MiniMax TTS error (${res.status})`);
|
||||
}
|
||||
if (statusCode !== 0) {
|
||||
throw new Error(statusMessage || "MiniMax TTS upstream error");
|
||||
}
|
||||
|
||||
return {
|
||||
base64: hexToBase64(data.data?.audio),
|
||||
format: data.extra_info?.audio_format || data.extraInfo?.audioFormat || "mp3",
|
||||
};
|
||||
}
|
||||
@@ -966,16 +966,29 @@ async function getGlmUsage(apiKey, provider, proxyOptions = null) {
|
||||
}
|
||||
|
||||
// ── MiniMax helpers ──────────────────────────────────────────────────────
|
||||
function isMiniMaxTextQuotaModel(modelName) {
|
||||
const normalized = (modelName || "").trim().toLowerCase();
|
||||
return normalized.startsWith("minimax-m") || normalized.startsWith("coding-plan");
|
||||
}
|
||||
|
||||
function getMiniMaxField(model, snakeKey, camelKey) {
|
||||
if (!model || typeof model !== "object") return null;
|
||||
return model[snakeKey] ?? model[camelKey] ?? null;
|
||||
}
|
||||
|
||||
function getMiniMaxModelName(model) {
|
||||
return String(getMiniMaxField(model, "model_name", "modelName") || "").trim();
|
||||
}
|
||||
|
||||
function formatMiniMaxQuotaName(model) {
|
||||
const rawName = getMiniMaxModelName(model);
|
||||
if (!rawName) return "MiniMax";
|
||||
|
||||
return rawName
|
||||
.replace(/[_-]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.replace(/\b\w/g, (ch) => ch.toUpperCase())
|
||||
.replace(/\bTo\b/g, "to")
|
||||
.replace(/\bTts\b/g, "TTS")
|
||||
.replace(/\bHd\b/g, "HD");
|
||||
}
|
||||
|
||||
function getMiniMaxSessionTotal(model) {
|
||||
return Math.max(0, Number(getMiniMaxField(model, "current_interval_total_count", "currentIntervalTotalCount")) || 0);
|
||||
}
|
||||
@@ -984,11 +997,8 @@ function getMiniMaxWeeklyTotal(model) {
|
||||
return Math.max(0, Number(getMiniMaxField(model, "current_weekly_total_count", "currentWeeklyTotalCount")) || 0);
|
||||
}
|
||||
|
||||
function pickMiniMaxRepresentativeModel(models, getTotal) {
|
||||
const withQuota = models.filter((m) => getTotal(m) > 0);
|
||||
const pool = withQuota.length > 0 ? withQuota : models;
|
||||
if (pool.length === 0) return null;
|
||||
return pool.reduce((best, current) => (getTotal(current) > getTotal(best) ? current : best));
|
||||
function hasMiniMaxQuota(model) {
|
||||
return getMiniMaxSessionTotal(model) > 0 || getMiniMaxWeeklyTotal(model) > 0;
|
||||
}
|
||||
|
||||
function getMiniMaxResetAt(model, capturedAtMs, remainsSnake, remainsCamel, endSnake, endCamel) {
|
||||
@@ -1011,6 +1021,19 @@ function buildMiniMaxQuota(total, count, resetAt, countMeansRemaining) {
|
||||
};
|
||||
}
|
||||
|
||||
function addMiniMaxQuota(quotas, key, model, getTotal, countSnake, countCamel, resetArgs, countMeansRemaining) {
|
||||
const total = getTotal(model);
|
||||
if (total <= 0) return;
|
||||
|
||||
const count = Math.max(0, Number(getMiniMaxField(model, countSnake, countCamel)) || 0);
|
||||
quotas[key] = buildMiniMaxQuota(
|
||||
total,
|
||||
count,
|
||||
getMiniMaxResetAt(model, ...resetArgs),
|
||||
countMeansRemaining
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* MiniMax Token Plan / Coding Plan usage
|
||||
*/
|
||||
@@ -1064,34 +1087,37 @@ async function getMiniMaxUsage(apiKey, provider, proxyOptions = null) {
|
||||
|
||||
const modelRemains = payload?.model_remains ?? payload?.modelRemains;
|
||||
const allModels = Array.isArray(modelRemains) ? modelRemains : [];
|
||||
const textModels = allModels.filter((m) => isMiniMaxTextQuotaModel(String(getMiniMaxField(m, "model_name", "modelName"))));
|
||||
const quotaModels = allModels.filter(hasMiniMaxQuota);
|
||||
|
||||
if (textModels.length === 0) {
|
||||
return { message: "MiniMax connected. No text quota data was returned." };
|
||||
if (quotaModels.length === 0) {
|
||||
return { message: "MiniMax connected. No quota data was returned." };
|
||||
}
|
||||
|
||||
const capturedAtMs = Date.now();
|
||||
const countMeansRemaining = usageUrl.includes("/coding_plan/remains");
|
||||
const quotas = {};
|
||||
|
||||
const sessionModel = pickMiniMaxRepresentativeModel(textModels, getMiniMaxSessionTotal);
|
||||
if (sessionModel) {
|
||||
const total = getMiniMaxSessionTotal(sessionModel);
|
||||
const count = Math.max(0, Number(getMiniMaxField(sessionModel, "current_interval_usage_count", "currentIntervalUsageCount")) || 0);
|
||||
quotas["session (5h)"] = buildMiniMaxQuota(
|
||||
total, count,
|
||||
getMiniMaxResetAt(sessionModel, capturedAtMs, "remains_time", "remainsTime", "end_time", "endTime"),
|
||||
for (const model of quotaModels) {
|
||||
const displayName = formatMiniMaxQuotaName(model);
|
||||
addMiniMaxQuota(
|
||||
quotas,
|
||||
`${displayName} (5h)`,
|
||||
model,
|
||||
getMiniMaxSessionTotal,
|
||||
"current_interval_usage_count",
|
||||
"currentIntervalUsageCount",
|
||||
[capturedAtMs, "remains_time", "remainsTime", "end_time", "endTime"],
|
||||
countMeansRemaining
|
||||
);
|
||||
}
|
||||
|
||||
const weeklyModel = pickMiniMaxRepresentativeModel(textModels, getMiniMaxWeeklyTotal);
|
||||
if (weeklyModel && getMiniMaxWeeklyTotal(weeklyModel) > 0) {
|
||||
const total = getMiniMaxWeeklyTotal(weeklyModel);
|
||||
const count = Math.max(0, Number(getMiniMaxField(weeklyModel, "current_weekly_usage_count", "currentWeeklyUsageCount")) || 0);
|
||||
quotas["weekly (7d)"] = buildMiniMaxQuota(
|
||||
total, count,
|
||||
getMiniMaxResetAt(weeklyModel, capturedAtMs, "weekly_remains_time", "weeklyRemainsTime", "weekly_end_time", "weeklyEndTime"),
|
||||
addMiniMaxQuota(
|
||||
quotas,
|
||||
`${displayName} (7d)`,
|
||||
model,
|
||||
getMiniMaxWeeklyTotal,
|
||||
"current_weekly_usage_count",
|
||||
"currentWeeklyUsageCount",
|
||||
[capturedAtMs, "weekly_remains_time", "weeklyRemainsTime", "weekly_end_time", "weeklyEndTime"],
|
||||
countMeansRemaining
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user