feat(tts): add Xiaomi MiMo text-to-speech support

Adds mimo-v2.5-tts as a Media Provider TTS through the existing
OpenAI-compatible chat-completions endpoint. Voice is selected via the
top-level audio.voice field, and an optional style/language hint is
threaded through tts.js -> ttsCore.js -> the new adapter.
This commit is contained in:
MiQieR 2026-08-05 11:45:27 +07:00 committed by decolua
parent d0751bcff7
commit c570fe33ae
44 changed files with 396 additions and 20 deletions

View file

@ -1,6 +1,7 @@
# v0.5.45 (2026-07-30)
## Features
- **TTS**: add Xiaomi MiMo text-to-speech (preset voices 冰糖/茉莉/苏打/白桦/Mia/Chloe/Milo/Dean, style control, language hint dropdown with Auto-detect, i18n for Style label/placeholder)
- **Providers**: add Poolside (OpenAI-compatible)
- **Providers**: add api-airforce, baidu, bazaarlink, bluesminds, kilo-gateway, llm7, morph, sambanova, tencent
- **OAuth**: zed / trae / windsurf providers + harden callback proxies

View file

@ -33,6 +33,21 @@ const GEMINI_VOICES = [
"Vindemiatrix", "Sadachbia", "Sadaltager", "Sulafat",
].map((id) => ({ id, name: id, type: "tts" }));
// Xiaomi MiMo preset voices (from https://mimo.mi.com/docs/zh-CN/quick-start/usage-guide/audio/speech-synthesis-v2.5).
// Voice id is passed via `audio.voice`; `mimo_default` = default (冰糖 on CN cluster, Mia elsewhere).
// Voices are language-independent — the spoken language is a separate hint, not bound to the voice.
const MIMO_VOICES = [
{ id: "mimo_default", name: "mimo_default" },
{ id: "冰糖", name: "冰糖" },
{ id: "茉莉", name: "茉莉" },
{ id: "苏打", name: "苏打" },
{ id: "白桦", name: "白桦" },
{ id: "Mia", name: "Mia" },
{ id: "Chloe", name: "Chloe" },
{ id: "Milo", name: "Milo" },
{ id: "Dean", name: "Dean" },
].map((v) => ({ type: "tts", ...v }));
// ── TTS Config (config-driven, single source of truth) ─────────────────────
export const TTS_MODELS_CONFIG = {
openai: {
@ -107,6 +122,14 @@ export const TTS_MODELS_CONFIG = {
},
allVoices: GEMINI_VOICES,
},
"xiaomi-mimo": {
models: [
{ id: "mimo-v2.5-tts", name: "MiMo V2.5 TTS", type: "tts" },
],
voices: {
"mimo-v2.5-tts": MIMO_VOICES,
},
},
};
// ── Helper: get voices for a specific model ────────────────────────────────

View file

@ -48,16 +48,16 @@ function createTtsResponse(base64Audio, format, responseFormat) {
*
* @returns {Promise<{success, response, status?, error?}>}
*/
export async function handleTtsCore({ provider, model, input, credentials, responseFormat = "mp3", language }) {
export async function handleTtsCore({ provider, model, input, credentials, responseFormat = "mp3", language, style }) {
if (!input?.trim()) {
return createErrorResult(HTTP_STATUS.BAD_REQUEST, "Missing required field: input");
}
try {
// Special-case adapters (google-tts, edge-tts, local-device, elevenlabs, openai, openrouter, gemini)
// Special-case adapters (google-tts, edge-tts, local-device, elevenlabs, openai, openrouter, gemini, xiaomi-mimo)
const adapter = getTtsAdapter(provider);
if (adapter) {
const result = await adapter.synthesize(input.trim(), model, credentials, responseFormat, { language });
const result = await adapter.synthesize(input.trim(), model, credentials, responseFormat, { language, style });
// Adapter may return a full {success, response} (legacy) or {base64, format}
if (result.success !== undefined) return result;
return createTtsResponse(result.base64, result.format, responseFormat);

View file

@ -6,6 +6,7 @@ import elevenlabs, { fetchElevenLabsVoices } from "./elevenlabs.js";
import openai from "./openai.js";
import openrouter from "./openrouter.js";
import gemini, { fetchGeminiVoices } from "./gemini.js";
import xiaomiMimo from "./xiaomi-mimo.js";
import { FORMAT_HANDLERS } from "./genericFormats.js";
import { parseModelVoice } from "./_base.js";
@ -18,6 +19,7 @@ const SPECIAL_ADAPTERS = {
openai,
openrouter,
gemini,
"xiaomi-mimo": xiaomiMimo,
};
export function getTtsAdapter(provider) {

View file

@ -0,0 +1,65 @@
// Xiaomi MiMo TTS — via OpenAI-compatible chat completions (non-streaming).
// Docs: https://mimo.mi.com/docs/zh-CN/quick-start/usage-guide/audio/speech-synthesis-v2.5
// Message contract: target text in `role: assistant` content, style/voice
// instructions in `role: user` content. Voice is selected via the top-level
// `audio.voice` field (NOT embedded in the model name).
import { parseModelVoice } from "./_base.js";
const DEFAULT_MODEL = "mimo-v2.5-tts";
const DEFAULT_VOICE = "mimo_default";
export default {
synthesize(text, model, credentials, responseFormat, { style, language } = {}) {
if (!credentials?.apiKey) throw new Error("xiaomi-mimo API key required");
return synthesizeMiMo(text, model, credentials.apiKey, style, language);
},
};
export async function synthesizeMiMo(text, model, apiKey, style, language) {
const { modelId, voiceId } = parseModelVoice(model, DEFAULT_MODEL, DEFAULT_VOICE, [DEFAULT_MODEL]);
// Language and style are soft instructions → prepend as a role:user message.
// MiMo auto-detects the spoken language of the text; the hint only nudges it
// (e.g. "Speak in English.") and is independent of the chosen voice.
const instructions = [];
if (language) instructions.push(`Speak in ${language}.`);
if (style) instructions.push(style);
const messages = [{ role: "assistant", content: text }];
if (instructions.length) messages.unshift({ role: "user", content: instructions.join(" ") });
const res = await fetch("https://api.xiaomimimo.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: modelId,
stream: false,
messages,
audio: {
format: "wav",
voice: voiceId || DEFAULT_VOICE,
},
}),
});
const rawText = await res.text();
let data = {};
if (rawText) {
try { data = JSON.parse(rawText); } catch { data = {}; }
}
if (!res.ok) {
throw new Error(data?.error?.message || rawText || `MiMo TTS error (${res.status})`);
}
const audio = data?.choices?.[0]?.message?.audio?.data;
if (!audio) throw new Error(data?.error?.message || "MiMo TTS returned no audio");
return {
base64: audio,
format: data?.choices?.[0]?.message?.audio?.format || "wav",
};
}

View file

@ -15,10 +15,11 @@ export default {
textIcon: "XM",
website: "https://xiaomimimo.com",
notice: {
apiKeyUrl: "https://xiaomimimo.com",
apiKeyUrl: "https://platform.xiaomimimo.com/console/api-keys",
},
},
category: "apikey",
serviceKinds: ["llm", "tts"],
transport: {
baseUrl: "https://api.xiaomimimo.com/v1/chat/completions",
validateUrl: "https://api.xiaomimimo.com/v1/models",
@ -42,5 +43,12 @@ export default {
{ id: "mimo-v2.5", name: "MiMo V2.5" },
{ id: "mimo-v2-omni", name: "MiMo V2 Omni" },
{ id: "mimo-v2-flash", name: "MiMo V2 Flash" },
{ id: "mimo-v2.5-tts", name: "MiMo V2.5 TTS", kind: "tts" },
],
ttsConfig: {
baseUrl: "https://api.xiaomimimo.com/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
format: "xiaomi-mimo-tts",
},
};

View file

@ -12,6 +12,7 @@
"Logout": "تسجيل الخروج",
"Login": "تسجيل الدخول",
"Providers": "الموفرون",
"Style": "النمط",
"Usage": "الإحصائيات",
"API Key": "مفتاح API",
"Connected": "متصل",
@ -176,6 +177,7 @@
"How it works:": "كيف يعمل:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "طلب Antigravity/Copilot IDE → إعادة توجيه DNS إلى localhost:443 → يعترض وكيل MITM → 9Router → الرد إلى Antigravity/Copilot",
"No API keys — create one in Keys page": "لا توجد مفاتيح API — قم بإنشاء واحدة في صفحة المفاتيح",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "مثال: صوت دافئ ولطيف، يتحدث ببطء بلهجة بريطانية",
"sk_9router (default)": "sk_9router (افتراضي)",
"Server started": "تم بدء الخادم",
"Failed to start server": "فشل في بدء الخادم",

View file

@ -12,6 +12,7 @@
"Logout": "লগ আউট",
"Login": "লগ ইন",
"Providers": "সরবরাহকারী",
"Style": "শৈলী",
"Usage": "ব্যবহারের পরিসংখ্যান",
"API Key": "API কী",
"Connected": "সংযুক্ত",
@ -176,6 +177,7 @@
"How it works:": "এটি কীভাবে কাজ করে:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE অনুরোধ → DNS কে localhost:443 তে রিডিরেক্ট করুন → MITM প্রক্সি ইন্টারসেপ্ট করে → 9Router → Antigravity/Copilot এ প্রতিক্রিয়া",
"No API keys — create one in Keys page": "কোন API কী নেই — Keys পৃষ্ঠায় একটি তৈরি করুন",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "যেমন: উষ্ণ, মৃদু কণ্ঠস্বর, ব্রিটিশ উচ্চারণে ধীরে ধীরে কথা বলা",
"sk_9router (default)": "sk_9router (ডিফল্ট)",
"Server started": "সার্ভার শুরু হয়েছে",
"Failed to start server": "সার্ভার শুরু করতে ব্যর্থ",

View file

@ -12,6 +12,7 @@
"Logout": "Odhlásit se",
"Login": "Přihlásit se",
"Providers": "Poskytovatelé",
"Style": "Styl",
"Usage": "Statistika",
"API Key": "Klíč API",
"Connected": "Připojeno",
@ -176,6 +177,7 @@
"How it works:": "Jak to funguje:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Požadavek Antigravity/Copilot IDE → Přesměrování DNS na localhost:443 → Proxy MITM zachycuje → 9Router → odpověď na Antigravity/Copilot",
"No API keys — create one in Keys page": "Žádné klíče API — vytvořte jeden na stránce Klíče",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "např.: teplý, jemný hlas, mluvící pomalu s britským přízvukem",
"sk_9router (default)": "sk_9router (výchozí)",
"Server started": "Server spuštěn",
"Failed to start server": "Spuštění serveru se nezdařilo",

View file

@ -12,6 +12,7 @@
"Logout": "Log ud",
"Login": "Log ind",
"Providers": "Udbydere",
"Style": "Stil",
"Usage": "Forbrugsstatistik",
"API Key": "API-nøgle",
"Connected": "Forbundet",
@ -176,6 +177,7 @@
"How it works:": "Sådan virker det:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE-anmodning → DNS-omdirigering til localhost:443 → MITM-proxy aflytter → 9Router → svar til Antigravity/Copilot",
"No API keys — create one in Keys page": "Ingen API-nøgler — opret en på Keys-siden",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "fx.: en varm, blød stemme, der taler langsomt med britisk accent",
"sk_9router (default)": "sk_9router (standard)",
"Server started": "Server startet",
"Failed to start server": "Fejl ved start af server",

View file

@ -12,6 +12,7 @@
"Logout": "Abmelden",
"Login": "Anmelden",
"Providers": "Anbieter",
"Style": "Stil",
"Usage": "Statistiken",
"API Key": "API-Schlüssel",
"Connected": "Verbunden",
@ -176,6 +177,7 @@
"How it works:": "So funktioniert es:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE-Anforderung → DNS-Umleitung auf localhost:443 → MITM-Proxy abfangen → 9Router → Antwort auf Antigravity/Copilot",
"No API keys — create one in Keys page": "Keine API-Schlüssel — erstellen Sie einen auf der Seite Schlüssel",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "z. B.: eine warme, sanfte Stimme, die langsam mit britischem Akzent spricht",
"sk_9router (default)": "sk_9router (Standard)",
"Server started": "Server gestartet",
"Failed to start server": "Server konnte nicht gestartet werden",

View file

@ -12,6 +12,7 @@
"Logout": "Έξοδος",
"Login": "Σύνδεση",
"Providers": "Παρόχοι",
"Style": "Στυλ",
"Usage": "Στατιστικά χρήσης",
"API Key": "Κλειδί API",
"Connected": "Συνδεδεμένο",
@ -176,6 +177,7 @@
"How it works:": "Πώς λειτουργεί:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Αίτημα Antigravity/Copilot IDE → Ανακατεύθυνση DNS στο localhost:443 → Ο διακομιστής μεσολάβησης MITM παρεμβαίνει → 9Router → απάντηση στο Antigravity/Copilot",
"No API keys — create one in Keys page": "Δεν υπάρχουν κλειδιά API — δημιουργήστε ένα στη σελίδα Keys",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "π.χ.: μια ζεστή, απαλή φωνή, που μιλάει αργά με βρετανική προφορά",
"sk_9router (default)": "sk_9router (προεπιλεγμένο)",
"Server started": "Ο διακομιστής ξεκίνησε",
"Failed to start server": "Αποτυχία εκκίνησης διακομιστή",

View file

@ -12,6 +12,7 @@
"Logout": "Cerrar sesión",
"Login": "Iniciar sesión",
"Providers": "Proveedores",
"Style": "Estilo",
"Usage": "Estadísticas",
"API Key": "Clave API",
"Connected": "Conectado",
@ -176,6 +177,7 @@
"How it works:": "Cómo funciona:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Solicitud de Antigravity/Copilot IDE → Redireccionamiento DNS a localhost:443 → El proxy MITM intercepta → 9Router → respuesta a Antigravity/Copilot",
"No API keys — create one in Keys page": "Sin claves API — cree una en la página Claves",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "p. ej.: voz cálida y suave, hablando lentamente con acento británico",
"sk_9router (default)": "sk_9router (predeterminado)",
"Server started": "Servidor iniciado",
"Failed to start server": "Error al iniciar el servidor",

View file

@ -1119,6 +1119,7 @@
"Stop Server": "توقف سرور",
"Stopped": "متوقف شد",
"Strict Proxy": "پروکسی سختگیرانه",
"Style": "سبک",
"Subagent Model": "مدل زیرعامل",
"Sudo Password Required": "رمز عبور sudo الزامی است",
"Sudo password is required": "رمز عبور sudo الزامی است",
@ -1312,6 +1313,7 @@
"disabled": "غیرفعال",
"dollars per million tokens": "دلار به ازای هر میلیون توکن",
"e.g. CwhRBWXzGAHq8TQ4Fs17": "مثلاً CwhRBWXzGAHq8TQ4Fs17",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "مثلاً: صدای گرم و ملایم که با لهجه بریتانیایی به‌آرامی صحبت می‌کند",
"e.g. claude-opus-4-5": "مثلاً claude-opus-4-5",
"e.g. my-model-id": "مثلاً my-model-id",
"e.g. tts-1-hd": "مثلاً tts-1-hd",

View file

@ -12,6 +12,7 @@
"Logout": "Kirjaudu ulos",
"Login": "Kirjaudu sisään",
"Providers": "Palveluntarjoajat",
"Style": "Tyyli",
"Usage": "Käyttötilastot",
"API Key": "API-avain",
"Connected": "Yhdistetty",
@ -176,6 +177,7 @@
"How it works:": "Kuinka se toimii:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE-pyyntö → DNS-uudelleenohjaus localhost:443:iin → MITM-välityspalvelin sieppaa → 9Router → vastaus Antigravity/Copilot:ille",
"No API keys — create one in Keys page": "Ei API-avaimia — luo yksi Keys-sivulla",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "esim.: lämmin, pehmeä ääni, joka puhuu hitaasti brittiaksentilla",
"sk_9router (default)": "sk_9router (oletus)",
"Server started": "Palvelin käynnistetty",
"Failed to start server": "Palvelimen käynnistäminen epäonnistui",

View file

@ -12,6 +12,7 @@
"Logout": "Déconnexion",
"Login": "Connexion",
"Providers": "Fournisseurs",
"Style": "Style",
"Usage": "Statistiques",
"API Key": "Clé API",
"Connected": "Connecté",
@ -176,6 +177,7 @@
"How it works:": "Comment ça marche :",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Demande Antigravity/Copilot IDE → Redirection DNS vers localhost:443 → Le proxy MITM intercepte → 9Router → réponse à Antigravity/Copilot",
"No API keys — create one in Keys page": "Aucune clé API — créez-en une dans la page Clés",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "ex. : voix chaleureuse et douce, parlant lentement avec un accent britannique",
"sk_9router (default)": "sk_9router (par défaut)",
"Server started": "Serveur démarré",
"Failed to start server": "Impossible de démarrer le serveur",

View file

@ -12,6 +12,7 @@
"Logout": "התנתקות",
"Login": "כניסה",
"Providers": "ספקים",
"Style": "סגנון",
"Usage": "סטטיסטיקה",
"API Key": "מפתח API",
"Connected": "מחובר",
@ -176,6 +177,7 @@
"How it works:": "איך זה עובד:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "בקשת Antigravity/Copilot IDE → הפניה DNS ל-localhost:443 → פרוקסי MITM חוטף → 9Router → תגובה ל-Antigravity/Copilot",
"No API keys — create one in Keys page": "אין מפתחות API — צור אחד בעמוד Keys",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "למשל: קול חם ועדין, מדבר לאט במבטא בריטי",
"sk_9router (default)": "sk_9router (ברירת מחדל)",
"Server started": "השרת התחיל",
"Failed to start server": "הפעלת השרת נכשלה",

View file

@ -12,6 +12,7 @@
"Logout": "लॉग आउट",
"Login": "लॉगिन",
"Providers": "प्रदाता",
"Style": "शैली",
"Usage": "उपयोग के आंकड़े",
"API Key": "API कुंजी",
"Connected": "जुड़ा हुआ",
@ -176,6 +177,7 @@
"How it works:": "यह कैसे काम करता है:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE अनुरोध → DNS को localhost:443 में पुनर्निर्देशित करें → MITM प्रॉक्सी इंटरसेप्ट करता है → 9Router → Antigravity/Copilot को प्रतिक्रिया",
"No API keys — create one in Keys page": "कोई API कुंजी नहीं — Keys पृष्ठ में एक बनाएं",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "जैसे: गर्म, कोमल आवाज़, ब्रिटिश लहजे में धीरे-धीरे बोलते हुए",
"sk_9router (default)": "sk_9router (डिफ़ॉल्ट)",
"Server started": "सर्वर शुरू किया गया",
"Failed to start server": "सर्वर शुरू करने में विफल",

View file

@ -12,6 +12,7 @@
"Logout": "Kijelentkezés",
"Login": "Bejelentkezés",
"Providers": "Szolgáltatók",
"Style": "Stílus",
"Usage": "Használati statisztika",
"API Key": "API-kulcs",
"Connected": "Csatlakoztatva",
@ -176,6 +177,7 @@
"How it works:": "Hogyan működik:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE kérés → DNS átirányítás a localhost:443-ra → MITM proxy elfogja → 9Router → válasz Antigravity/Copilot-nak",
"No API keys — create one in Keys page": "Nincsenek API-kulcsok — hozzon létre egyet a Keys oldalon",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "pl.: meleg, lágy hang, lassan beszél brit akcentussal",
"sk_9router (default)": "sk_9router (alapértelmezett)",
"Server started": "Szerver elindult",
"Failed to start server": "Nem sikerült elindítani a szervert",

View file

@ -12,6 +12,7 @@
"Logout": "Keluar",
"Login": "Masuk",
"Providers": "Penyedia",
"Style": "Gaya",
"Usage": "Statistik Penggunaan",
"API Key": "Kunci API",
"Connected": "Terhubung",
@ -176,6 +177,7 @@
"How it works:": "Cara kerjanya:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Permintaan Antigravity/Copilot IDE → Pengalihan DNS ke localhost:443 → Proxy MITM mengintersep → 9Router → respons ke Antigravity/Copilot",
"No API keys — create one in Keys page": "Tidak ada kunci API — buat satu di halaman Keys",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "mis.: suara hangat dan lembut, berbicara pelan dengan aksen Inggris",
"sk_9router (default)": "sk_9router (bawaan)",
"Server started": "Server dimulai",
"Failed to start server": "Gagal memulai server",

View file

@ -12,6 +12,7 @@
"Logout": "Esci",
"Login": "Accedi",
"Providers": "Provider",
"Style": "Stile",
"Usage": "Statistiche di utilizzo",
"API Key": "Chiave API",
"Connected": "Connesso",
@ -176,6 +177,7 @@
"How it works:": "Come funziona:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Richiesta Antigravity/Copilot IDE → Reindirizzamento DNS a localhost:443 → Il proxy MITM intercetta → 9Router → Risposta a Antigravity/Copilot",
"No API keys — create one in Keys page": "Nessuna chiave API — crearne una nella pagina Chiavi",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "es.: voce calda e morbida, che parla lentamente con accento britannico",
"sk_9router (default)": "sk_9router (predefinito)",
"Server started": "Server avviato",
"Failed to start server": "Impossibile avviare il server",

View file

@ -12,6 +12,7 @@
"Logout": "ログアウト",
"Login": "ログイン",
"Providers": "プロバイダー",
"Style": "スタイル",
"Usage": "統計",
"API Key": "APIキー",
"Connected": "接続済み",
@ -176,6 +177,7 @@
"How it works:": "しくみ:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE リクエスト → localhost:443 への DNS リダイレクト → MITM プロキシが傍受 → 9Router → Antigravity/Copilot への応答",
"No API keys — create one in Keys page": "APIキーがありません — キーページで1つ作成してください",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "例:温かく穏やかな声で、イギリス英語のアクセントでゆっくり話す",
"sk_9router (default)": "sk_9router(デフォルト)",
"Server started": "サーバーが開始されました",
"Failed to start server": "サーバーの開始に失敗しました",

View file

@ -1121,6 +1121,7 @@
"Stop Server": "បញ្ឈប់ម៉ាស៊ីនមេ",
"Stopped": "បានបញ្ឈប់",
"Strict Proxy": "Strict Proxy",
"Style": "រចនាបទ",
"Subagent Model": "ម៉ូដែល Subagent",
"Sudo Password Required": "ត្រូវការពាក្យសម្ងាត់ Sudo",
"Sudo password is required": "ត្រូវការពាក្យសម្ងាត់ Sudo",
@ -1317,6 +1318,7 @@
"disabled": "បានបិទ",
"dollars per million tokens": "ដុល្លារក្នុងមួយលាន Tokens",
"e.g. CwhRBWXzGAHq8TQ4Fs17": "ឧ. CwhRBWXzGAHq8TQ4Fs17",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "ឧទាហរណ៍៖ សំឡេងកក់ក្តៅ និងទន់ភ្លន់ និយាយយឺតៗដោយសង្កត់សំឡេងបែបអង់គ្លេស",
"e.g. claude-opus-4-5": "ឧ. claude-opus-4-5",
"e.g. my-model-id": "ឧ. my-model-id",
"e.g. tts-1-hd": "ឧ. tts-1-hd",

View file

@ -12,6 +12,7 @@
"Logout": "로그아웃",
"Login": "로그인",
"Providers": "제공자",
"Style": "스타일",
"Usage": "통계",
"API Key": "API 키",
"Connected": "연결됨",
@ -176,6 +177,7 @@
"How it works:": "작동 방식:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE 요청 → localhost:443로 DNS 리디렉션 → MITM 프록시가 가로챔 → 9Router → Antigravity/Copilot으로 응답",
"No API keys — create one in Keys page": "API 키 없음 — 키 페이지에서 만들기",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "예: 따뜻하고 부드러운 목소리로 영국식 억양을 쓰며 천천히 말하기",
"sk_9router (default)": "sk_9router (기본값)",
"Server started": "서버 시작됨",
"Failed to start server": "서버 시작 실패",

View file

@ -12,6 +12,7 @@
"Logout": "Afmelden",
"Login": "Aanmelden",
"Providers": "Providers",
"Style": "Stijl",
"Usage": "Statistieken",
"API Key": "API-sleutel",
"Connected": "Verbonden",
@ -176,6 +177,7 @@
"How it works:": "Hoe het werkt:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE-aanvraag → DNS-omleiding naar localhost:443 → MITM-proxy onderschept → 9Router → antwoord naar Antigravity/Copilot",
"No API keys — create one in Keys page": "Geen API-sleutels — maak er één aan op de pagina Sleutels",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "bijv.: een warme, zachte stem, die langzaam praat met een Brits accent",
"sk_9router (default)": "sk_9router (standaard)",
"Server started": "Server gestart",
"Failed to start server": "Server starten mislukt",

View file

@ -12,6 +12,7 @@
"Logout": "Logg ut",
"Login": "Logg inn",
"Providers": "Leverandører",
"Style": "Stil",
"Usage": "Bruksstatistikk",
"API Key": "API-nøkkel",
"Connected": "Tilkoblet",
@ -176,6 +177,7 @@
"How it works:": "Slik fungerer det:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE-forespørsel → DNS-omdirigering til localhost:443 → MITM-proxy avlytt → 9Router → svar til Antigravity/Copilot",
"No API keys — create one in Keys page": "Ingen API-nøkler — lag en på Keys-siden",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "f.eks.: en varm, myk stemme, som snakker sakte med britisk aksent",
"sk_9router (default)": "sk_9router (standard)",
"Server started": "Server startet",
"Failed to start server": "Klarte ikke å starte server",

View file

@ -12,6 +12,7 @@
"Logout": "Wyloguj się",
"Login": "Zaloguj się",
"Providers": "Dostawcy",
"Style": "Styl",
"Usage": "Statystyka",
"API Key": "Klucz API",
"Connected": "Połączony",
@ -176,6 +177,7 @@
"How it works:": "Jak to działa:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Żądanie Antigravity/Copilot IDE → Przekierowanie DNS na localhost:443 → Serwer proxy MITM przechwytuje → 9Router → odpowiedź do Antigravity/Copilot",
"No API keys — create one in Keys page": "Brak kluczy API — utwórz jeden na stronie Klucze",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "np.: ciepły, delikatny głos, mówiący powoli z brytyjskim akcentem",
"sk_9router (default)": "sk_9router (domyślnie)",
"Server started": "Serwer uruchomiony",
"Failed to start server": "Nie udało się uruchomić serwera",

View file

@ -864,6 +864,7 @@
"Stopped": "Parado",
"Stopping…": "Parando…",
"Strict Proxy": "Proxy Estrito",
"Style": "Estilo",
"Subagent Model": "Modelo de Subagente",
"Subagent model overrides": "Substituições de modelo de subagente",
"Submit": "Enviar",
@ -978,6 +979,7 @@
"Your OAuth application client ID": "ID do cliente do seu aplicativo OAuth",
"Your requests start from your favorite tools or our unified SDK.": "Suas requisições começam de suas ferramentas favoritas ou do nosso SDK unificado.",
"[ml] downloads ~1 GB (torch + huggingface-hub). Continue?": "[ml] baixa ~1 GB (torch + huggingface-hub). Continuar?",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "ex.: voz quente e suave, falando devagar com sotaque britânico",
"extras status failed": "falha no status dos extras",
"git/grep/ls/tree/logs → 60-90% fewer input tokens": "git/grep/ls/tree/logs → 60-90% menos tokens de entrada",
"not installed": "não instalado",

View file

@ -12,6 +12,7 @@
"Logout": "Terminar sessão",
"Login": "Iniciar sessão",
"Providers": "Fornecedores",
"Style": "Estilo",
"Usage": "Estatísticas",
"API Key": "Chave API",
"Connected": "Ligado",
@ -176,6 +177,7 @@
"How it works:": "Como funciona:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Pedido do Antigravity/Copilot IDE → Redirecionamento DNS para localhost:443 → Proxy MITM interceta → 9Router → resposta para Antigravity/Copilot",
"No API keys — create one in Keys page": "Sem chaves de API — crie uma na página Chaves",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "ex.: voz quente e suave, a falar devagar com sotaque britânico",
"sk_9router (default)": "sk_9router (predefinição)",
"Server started": "Servidor iniciado",
"Failed to start server": "Falha ao iniciar o servidor",

View file

@ -12,6 +12,7 @@
"Logout": "Ieșire",
"Login": "Conectare",
"Providers": "Furnizori",
"Style": "Stil",
"Usage": "Statistici de utilizare",
"API Key": "Cheie API",
"Connected": "Conectat",
@ -176,6 +177,7 @@
"How it works:": "Cum funcționează:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Solicitare Antigravity/Copilot IDE → Redirecționare DNS la localhost:443 → Proxy MITM interceptează → 9Router → răspuns la Antigravity/Copilot",
"No API keys — create one in Keys page": "Nicio cheie API — creați una în pagina Chei",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "ex.: voce caldă și blândă, vorbind încet cu accent britanic",
"sk_9router (default)": "sk_9router (implicit)",
"Server started": "Server pornit",
"Failed to start server": "Nu s-a putut porni serverul",

View file

@ -12,6 +12,7 @@
"Logout": "Выход",
"Login": "Вход",
"Providers": "Провайдеры",
"Style": "Стиль",
"Usage": "Статистика",
"API Key": "Ключ API",
"Connected": "Подключено",
@ -176,6 +177,7 @@
"How it works:": "Как это работает:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Запрос Antigravity/Copilot IDE → Перенаправление DNS на localhost:443 → Прокси MITM перехватывает → 9Router → ответ для Antigravity/Copilot",
"No API keys — create one in Keys page": "Нет ключей API — создайте один на странице ключей",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "напр.: тёплый мягкий голос, медленно говорящий с британским акцентом",
"sk_9router (default)": "sk_9router (по умолчанию)",
"Server started": "Сервер запущен",
"Failed to start server": "Ошибка при запуске сервера",

View file

@ -12,6 +12,7 @@
"Logout": "Logga ut",
"Login": "Logga in",
"Providers": "Leverantörer",
"Style": "Stil",
"Usage": "Användarstatistik",
"API Key": "API-nyckel",
"Connected": "Ansluten",
@ -176,6 +177,7 @@
"How it works:": "Hur det fungerar:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE-begäran → DNS-omdirigering till localhost:443 → MITM-proxy avlyssnar → 9Router → svar till Antigravity/Copilot",
"No API keys — create one in Keys page": "Inga API-nycklar — skapa en på nyckelsidan",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "t.ex.: en varm, mjuk röst som talar långsamt med brittisk accent",
"sk_9router (default)": "sk_9router (standard)",
"Server started": "Servern startad",
"Failed to start server": "Misslyckades att starta servern",

View file

@ -1119,6 +1119,7 @@
"Stop Server": "หยุดเซิร์ฟเวอร์",
"Stopped": "หยุดแล้ว",
"Strict Proxy": "Strict Proxy",
"Style": "สไตล์",
"Subagent Model": "Subagent Model",
"Sudo Password Required": "ต้องใช้ Sudo Password",
"Sudo password is required": "ต้องใช้ Sudo password",
@ -1312,6 +1313,7 @@
"disabled": "ปิดใช้งานแล้ว",
"dollars per million tokens": "ดอลลาร์ต่อล้าน tokens",
"e.g. CwhRBWXzGAHq8TQ4Fs17": "เช่น CwhRBWXzGAHq8TQ4Fs17",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "เช่น เสียงอบอุ่นนุ่มนวล พูดช้าๆ ด้วยสำเนียงอังกฤษ",
"e.g. claude-opus-4-5": "เช่น claude-opus-4-5",
"e.g. my-model-id": "เช่น my-model-id",
"e.g. tts-1-hd": "เช่น tts-1-hd",

View file

@ -12,6 +12,7 @@
"Logout": "Maglog out",
"Login": "Magsimula ng sesyon",
"Providers": "Mga Provider",
"Style": "Estilo",
"Usage": "Mga Istatistika ng Paggamit",
"API Key": "Susi ng API",
"Connected": "Konektado",
@ -176,6 +177,7 @@
"How it works:": "Paano ito gumagana:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE request → DNS redirect sa localhost:443 → MITM proxy intercepts → 9Router → response sa Antigravity/Copilot",
"No API keys — create one in Keys page": "Walang API keys — lumikha ng isa sa Keys page",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "hal.: mainit at malumanay na boses, dahan-dahang nagsasalita nang may British accent",
"sk_9router (default)": "sk_9router (default)",
"Server started": "Ang server ay nagsimula",
"Failed to start server": "Nabigo na magsimula ang server",

View file

@ -12,6 +12,7 @@
"Logout": "Çıkış Yap",
"Login": "Giriş Yap",
"Providers": "Sağlayıcılar",
"Style": "Stil",
"Usage": "Kullanım İstatistikleri",
"API Key": "API Anahtarı",
"Connected": "Bağlı",
@ -176,6 +177,7 @@
"How it works:": "Nasıl çalışır:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE isteği → DNS'i localhost:443'e yönlendir → MITM proxy yakalar → 9Router → Antigravity/Copilot'a yanıt",
"No API keys — create one in Keys page": "API anahtarı yok — Keys sayfasında bir tane oluşturun",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "örn.: sıcak ve yumuşak bir ses, İngiliz aksanıyla yavaş konuşuyor",
"sk_9router (default)": "sk_9router (varsayılan)",
"Server started": "Sunucu başlatıldı",
"Failed to start server": "Sunucu başlatılamadı",

View file

@ -12,6 +12,7 @@
"Logout": "Вийти",
"Login": "Увійти",
"Providers": "Постачальники",
"Style": "Стиль",
"Usage": "Статистика використання",
"API Key": "Ключ API",
"Connected": "Підключено",
@ -176,6 +177,7 @@
"How it works:": "Як це працює:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Запит Antigravity/Copilot IDE → Перенаправлення DNS на localhost:443 → MITM проксі перехопити → 9Router → відповідь на Antigravity/Copilot",
"No API keys — create one in Keys page": "Немає ключів API — створіть один на сторінці ключів",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "напр.: теплий м'який голос, що повільно говорить з британським акцентом",
"sk_9router (default)": "sk_9router (за замовчуванням)",
"Server started": "Сервер запущено",
"Failed to start server": "Не вдалося запустити сервер",

View file

@ -12,6 +12,7 @@
"Logout": "لاگ آؤٹ",
"Login": "لاگ ان",
"Providers": "فراہم کنندگان",
"Style": "انداز",
"Usage": "استعمال کے اعدادوشمار",
"API Key": "API کلید",
"Connected": "منسلک",
@ -176,6 +177,7 @@
"How it works:": "یہ کیسے کام کرتا ہے:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE درخواست → DNS کو localhost:443 کی طرف ری ڈائریکٹ کریں → MITM پروکسی روکے → 9Router → Antigravity/Copilot کو جواب",
"No API keys — create one in Keys page": "کوئی API کلید نہیں — Keys صفحہ میں ایک بنائیں",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "مثلاً: گرم، نرم آواز، برطانوی لہجے کے ساتھ آہستہ بولتی ہوئی",
"sk_9router (default)": "sk_9router (ڈیفالٹ)",
"Server started": "سرور شروع ہوگیا",
"Failed to start server": "سرور شروع کرنے میں ناکام",

View file

@ -12,6 +12,7 @@
"Logout": "Đăng xuất",
"Login": "Đăng nhập",
"Providers": "Nhà cung cấp",
"Style": "Phong cách",
"Usage": "Thống kê",
"API Key": "Khóa API",
"Connected": " Đã kết nối",
@ -176,6 +177,7 @@
"How it works:": "Cách hoạt động:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Yêu cầu Antigravity/Copilot IDE → Chuyển hướng DNS đến localhost:443 → MITM proxy chặn → 9Router → phản hồi đến Antigravity/Copilot",
"No API keys — create one in Keys page": "Không có khóa API — tạo một khóa trong trang Keys",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "vd.: giọng ấm áp, nhẹ nhàng, nói chậm với giọng Anh-Anh",
"sk_9router (default)": "sk_9router (mặc định)",
"Server started": "Đã khởi động máy chủ",
"Failed to start server": "Không thể khởi động máy chủ",

View file

@ -1119,6 +1119,7 @@
"Stop Server": "停止服务器",
"Stopped": "已停止",
"Strict Proxy": "严格代理",
"Style": "风格",
"Subagent Model": "子代理模型",
"Sudo Password Required": "需要 sudo 密码",
"Sudo password is required": "需要 sudo 密码",
@ -1312,6 +1313,7 @@
"disabled": "已禁用",
"dollars per million tokens": "美元 / 百万 Token",
"e.g. CwhRBWXzGAHq8TQ4Fs17": "例如 CwhRBWXzGAHq8TQ4Fs17",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "例如:温暖柔和的嗓音,用英式口音缓慢说话",
"e.g. claude-opus-4-5": "例如 claude-opus-4-5",
"e.g. my-model-id": "例如 my-model-id",
"e.g. tts-1-hd": "例如 tts-1-hd",

View file

@ -12,6 +12,7 @@
"Logout": "登出",
"Login": "登錄",
"Providers": "提供者",
"Style": "風格",
"Usage": "統計",
"API Key": "API 金鑰",
"Connected": "已連接",
@ -176,6 +177,7 @@
"How it works:": "工作原理:",
"Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE 請求 → DNS 重定向到 localhost:443 → MITM 代理攔截 → 9Router → 响應到 Antigravity/Copilot",
"No API keys — create one in Keys page": "沒有 API 金鑰 — 在金鑰頁面中創建一個",
"e.g. a warm, gentle voice, speaking slowly with a British accent": "例如:溫暖柔和的嗓音,用英式口音緩慢說話",
"sk_9router (default)": "sk_9router(默認)",
"Server started": "服務器已啟動",
"Failed to start server": "啟動服務器失敗",

View file

@ -6,6 +6,7 @@ import { AI_PROVIDERS, getProviderAlias } from "@/shared/constants/providers";
import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { TTS_PROVIDER_CONFIG } from "@/shared/constants/ttsProviders";
import { translate } from "@/i18n/runtime";
import { getTtsVoicesForModel } from "open-sse/config/ttsModels.js";
import { GOOGLE_TTS_LANGUAGES } from "open-sse/config/googleTtsLanguages.js";
import { Row } from "./exampleShared";
@ -40,6 +41,7 @@ export function TtsExampleCard({ providerId }) {
// Form state
const [input, setInput] = useState("Hello, this is a text to speech test.");
const [style, setStyle] = useState(""); // style/voice instructions (e.g. MiMo voicedesign)
const [apiKey, setApiKey] = useState("");
const [useTunnel, setUseTunnel] = useState(false);
const [localEndpoint, setLocalEndpoint] = useState("");
@ -59,8 +61,10 @@ export function TtsExampleCard({ providerId }) {
const [modalSearch, setModalSearch] = useState("");
const [modalError, setModalError] = useState("");
const [byLang, setByLang] = useState({});
// Language hint (e.g. Gemini): controls the spoken language without affecting voice selection
// Language hint (e.g. Gemini/MiMo): guides the spoken language without affecting voice selection
const [languageHint, setLanguageHint] = useState("");
// Number of stored provider connections (shown when no dashboard API key)
const [connectionCount, setConnectionCount] = useState(0);
useEffect(() => {
setLocalEndpoint(window.location.origin);
@ -68,6 +72,10 @@ export function TtsExampleCard({ providerId }) {
.then((r) => r.json())
.then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); })
.catch(() => {});
fetch("/api/providers", { cache: "no-store" })
.then((r) => r.json())
.then((d) => { setConnectionCount((d.connections || []).filter((c) => c.provider === providerId && c.isActive !== false).length); })
.catch(() => {});
fetch("/api/tunnel/status")
.then((r) => r.json())
.then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); })
@ -111,6 +119,10 @@ export function TtsExampleCard({ providerId }) {
if (voices.length) {
setSelectedVoice(voices[0].id);
setSelectedVoiceName(voices[0].name || voices[0].id);
} else {
// Model has no preset voices (voicedesign/voiceclone) — drop stale voice
setSelectedVoice("");
setSelectedVoiceName("");
}
}, [selectedModel]);
@ -184,6 +196,7 @@ export function TtsExampleCard({ providerId }) {
const ttsBody = (() => {
const b = { model: modelFull, input };
if (config.hasLanguageHint && languageHint) b.language = languageHint;
if (config.hasStyleInput && style.trim()) b.style = style.trim();
return b;
})();
const curlSnippet = `curl -X POST ${endpoint}/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""} \\
@ -218,7 +231,8 @@ export function TtsExampleCard({ providerId }) {
if (responseFormat === "json") {
const data = await res.json();
setJsonResponse(data); // Store full JSON response
const audioBlob = await fetch(`data:audio/mp3;base64,${data.audio}`).then(r => r.blob());
const format = data.format || "mp3";
const audioBlob = await fetch(`data:audio/${format};base64,${data.audio}`).then(r => r.blob());
setAudioUrl(URL.createObjectURL(audioBlob));
} else {
const blob = await res.blob();
@ -259,7 +273,11 @@ export function TtsExampleCard({ providerId }) {
</Row>
<Row label="API Key">
<span className="px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate block">
{apiKey ? `${apiKey.slice(0, 8)}${"•".repeat(Math.min(20, apiKey.length - 8))}` : <span className="text-text-muted italic">No key configured</span>}
{apiKey
? `${apiKey.slice(0, 8)}${"•".repeat(Math.min(20, apiKey.length - 8))}`
: connectionCount > 0
? <span className="text-text-muted italic">Using stored key(s) · {connectionCount} connection{connectionCount > 1 ? "s" : ""}</span>
: <span className="text-text-muted italic">No key configured</span>}
</span>
</Row>
@ -281,7 +299,7 @@ export function TtsExampleCard({ providerId }) {
</Row>
)}
{/* Language hint dropdown (Gemini) — sends body.language to guide pronunciation */}
{/* Language hint dropdown (Gemini, Xiaomi MiMo) — sends body.language to guide pronunciation */}
{config.hasLanguageHint && (
<Row label="Language">
<select
@ -290,9 +308,11 @@ export function TtsExampleCard({ providerId }) {
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
<option value="">Auto-detect</option>
{GOOGLE_TTS_LANGUAGES.map((l) => (
<option key={l.id} value={l.name}>{l.name}</option>
))}
{(config.languageOptions || GOOGLE_TTS_LANGUAGES).map((l) =>
typeof l === "string"
? <option key={l} value={l}>{l}</option>
: <option key={l.id} value={l.name}>{l.name}</option>
)}
</select>
</Row>
)}
@ -320,7 +340,7 @@ export function TtsExampleCard({ providerId }) {
</Row>
)}
{/* Voice chips — shown after language picked (edge-tts, local-device) or always (OpenAI/ElevenLabs) */}
{/* Voice chips — shown after language picked (edge-tts, local-device) or always (OpenAI/ElevenLabs/MiMo) */}
{countryVoices.length > 0 && (
<Row label="Voice">
<div className="flex flex-wrap gap-1.5">
@ -338,7 +358,9 @@ export function TtsExampleCard({ providerId }) {
: "border-border text-text-muted hover:text-primary hover:border-primary/40"
}`}
>
{v.name}{v.gender ? ` · ${v.gender[0].toUpperCase()}` : ""}
{v.name}
{v.language ? ` · ${v.language}` : ""}
{v.gender ? ` · ${v.gender[0].toUpperCase()}` : ""}
{v.free_users_allowed === true && (
<span className="ml-1.5 px-1 py-0.5 text-[9px] font-semibold rounded bg-green-500/15 text-green-600 border border-green-500/20">Free</span>
)}
@ -418,6 +440,30 @@ export function TtsExampleCard({ providerId }) {
</div>
</Row>
{/* Style / voice instructions (Xiaomi MiMo) */}
{config.hasStyleInput && (
<Row label={translate("Style")}>
<div className="relative">
<textarea
value={style}
onChange={(e) => setStyle(e.target.value)}
placeholder={translate("e.g. a warm, gentle voice, speaking slowly with a British accent")}
rows={2}
className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary resize-none"
/>
{style && (
<button
type="button"
onClick={() => setStyle("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">close</span>
</button>
)}
</div>
</Row>
)}
{/* Output Format */}
<Row label="Output Format">
<select

View file

@ -135,4 +135,16 @@ export const TTS_PROVIDER_CONFIG = {
voiceKey: "gemini-tts-voices",
voicesPerModel: true,
},
"xiaomi-mimo": {
hasLanguageDropdown: false,
hasModelSelector: true,
hasBrowseButton: false,
hasVoiceIdInput: false,
hasStyleInput: true, // style/voice instructions (role: user)
hasLanguageHint: true, // language dropdown (Auto-detect default); voices are language-independent
languageOptions: ["Chinese", "English"],
voiceSource: "hardcoded",
modelKey: "xiaomi-mimo-tts-models",
voicesPerModel: true,
},
};

View file

@ -30,6 +30,7 @@ export async function handleTts(request) {
const modelStr = body.model;
const responseFormat = url.searchParams.get("response_format") || "mp3"; // mp3 (default) | json
const language = body.language || ""; // Optional language hint (currently used by Gemini)
const style = body.style || ""; // Optional style/voice instructions (e.g. Xiaomi MiMo)
log.request("POST", `${url.pathname} | ${modelStr} | format=${responseFormat}${language ? ` | lang=${language}` : ""}`);
const settings = await getSettings();
@ -53,7 +54,7 @@ export async function handleTts(request) {
return handleComboChat({
body,
models: comboModels,
handleSingleModel: (b, m) => handleSingleModelTts(b, m, responseFormat, language),
handleSingleModel: (b, m) => handleSingleModelTts(b, m, responseFormat, language, style),
log,
comboName: modelStr,
comboStrategy,
@ -61,10 +62,10 @@ export async function handleTts(request) {
});
}
return handleSingleModelTts(body, modelStr, responseFormat, language);
return handleSingleModelTts(body, modelStr, responseFormat, language, style);
}
async function handleSingleModelTts(body, modelStr, responseFormat, language) {
async function handleSingleModelTts(body, modelStr, responseFormat, language, style) {
const modelInfo = await getModelInfo(modelStr);
if (!modelInfo.provider) return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid model format");
@ -73,7 +74,7 @@ async function handleSingleModelTts(body, modelStr, responseFormat, language) {
// noAuth providers — no credential needed
if (!CREDENTIALED_PROVIDERS.has(provider)) {
const result = await handleTtsCore({ provider, model, input: body.input, responseFormat, language });
const result = await handleTtsCore({ provider, model, input: body.input, responseFormat, language, style });
if (result.success) return result.response;
return errorResponse(result.status || HTTP_STATUS.BAD_GATEWAY, result.error || "TTS failed");
}
@ -98,7 +99,7 @@ async function handleSingleModelTts(body, modelStr, responseFormat, language) {
log.info("AUTH", `\x1b[32mUsing ${provider} account: ${credentials.connectionName}\x1b[0m`);
const result = await handleTtsCore({ provider, model, input: body.input, credentials, responseFormat, language });
const result = await handleTtsCore({ provider, model, input: body.input, credentials, responseFormat, language, style });
if (result.success) return result.response;

View file

@ -0,0 +1,150 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { handleTtsCore } from "../../open-sse/handlers/ttsCore.js";
import { buildTtsProviderModels, getTtsVoicesForModel } from "../../open-sse/config/ttsModels.js";
import { AI_PROVIDERS } from "../../src/shared/constants/providers.js";
import { getTtsAdapter } from "../../open-sse/handlers/ttsProviders/index.js";
import { PROVIDER_MODELS } from "../../open-sse/config/providerModels.js";
import { TTS_PROVIDER_CONFIG } from "../../src/shared/constants/ttsProviders.js";
const originalFetch = global.fetch;
function mockMiMoAudioResponse() {
global.fetch.mockResolvedValueOnce(
new Response(
JSON.stringify({
choices: [
{
message: {
role: "assistant",
audio: {
data: Buffer.from([0, 1, 2, 3]).toString("base64"),
format: "wav",
transcript: "Hello from MiMo",
},
},
},
],
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
)
);
}
describe("Xiaomi MiMo TTS", () => {
beforeEach(() => {
global.fetch = vi.fn();
});
afterEach(() => {
global.fetch = originalFetch;
});
it("posts to chat completions with text as assistant message and voice in audio.voice", async () => {
mockMiMoAudioResponse();
const result = await handleTtsCore({
provider: "xiaomi-mimo",
model: "mimo-v2.5-tts/冰糖",
input: "Hello from MiMo",
credentials: { apiKey: "test-key" },
responseFormat: "json",
});
expect(result.success).toBe(true);
expect(global.fetch.mock.calls[0][0]).toBe("https://api.xiaomimimo.com/v1/chat/completions");
expect(global.fetch.mock.calls[0][1].headers.Authorization).toBe("Bearer test-key");
const sent = JSON.parse(global.fetch.mock.calls[0][1].body);
expect(sent.model).toBe("mimo-v2.5-tts");
expect(sent.stream).toBe(false);
expect(sent.audio.format).toBe("wav");
expect(sent.audio.voice).toBe("冰糖");
const assistant = sent.messages.find((m) => m.role === "assistant");
expect(assistant.content).toBe("Hello from MiMo");
const body = await result.response.json();
expect(body.format).toBe("wav");
expect(body.audio).toEqual(expect.any(String));
});
it("uses mimo_default when no voice is provided", async () => {
mockMiMoAudioResponse();
await handleTtsCore({
provider: "xiaomi-mimo",
model: "mimo-v2.5-tts",
input: "Hello from MiMo",
credentials: { apiKey: "test-key" },
responseFormat: "json",
});
const sent = JSON.parse(global.fetch.mock.calls[0][1].body);
expect(sent.model).toBe("mimo-v2.5-tts");
expect(sent.audio.voice).toBe("mimo_default");
});
it("threads the style field into the role:user message", async () => {
mockMiMoAudioResponse();
await handleTtsCore({
provider: "xiaomi-mimo",
model: "mimo-v2.5-tts/Chloe",
input: "Hello from MiMo",
style: "a calm, warm female voice speaking slowly",
credentials: { apiKey: "test-key" },
responseFormat: "json",
});
const sent = JSON.parse(global.fetch.mock.calls[0][1].body);
const user = sent.messages.find((m) => m.role === "user");
expect(user.content).toBe("a calm, warm female voice speaking slowly");
});
it("exposes MiMo TTS models + preset voices in the TTS catalog", () => {
const entries = buildTtsProviderModels();
expect(entries["xiaomi-mimo-tts-models"].map((model) => model.id)).toEqual(["mimo-v2.5-tts"]);
const voices = getTtsVoicesForModel("xiaomi-mimo", "mimo-v2.5-tts");
expect(voices.map((v) => v.id)).toEqual([
"mimo_default", "冰糖", "茉莉", "苏打", "白桦", "Mia", "Chloe", "Milo", "Dean",
]);
expect(voices.find((v) => v.id === "冰糖")).toMatchObject({ name: "冰糖" });
expect(voices.find((v) => v.id === "Dean")).toMatchObject({ name: "Dean" });
// Voices are language-independent and gender-neutral (no language/gender labels in the UI)
expect(voices[0]).not.toHaveProperty("language");
expect(voices[0]).not.toHaveProperty("gender");
expect(getTtsVoicesForModel("xiaomi-mimo", "mimo-v2.5-tts-voiceclone")).toBeNull();
});
it("threads the language hint into a role:user instruction", async () => {
mockMiMoAudioResponse();
await handleTtsCore({
provider: "xiaomi-mimo",
model: "mimo-v2.5-tts/冰糖",
input: "Hello from MiMo",
language: "English",
credentials: { apiKey: "test-key" },
responseFormat: "json",
});
const sent = JSON.parse(global.fetch.mock.calls[0][1].body);
const user = sent.messages.find((m) => m.role === "user");
expect(user.content).toBe("Speak in English.");
});
it("wires the provider into media-providers TTS (serviceKind, adapter, UI config)", () => {
expect(AI_PROVIDERS["xiaomi-mimo"].serviceKinds).toContain("tts");
expect(AI_PROVIDERS["xiaomi-mimo"].ttsConfig.baseUrl).toBe("https://api.xiaomimimo.com/v1/chat/completions");
expect(getTtsAdapter("xiaomi-mimo")).toBeTruthy();
const ttsModels = PROVIDER_MODELS["xiaomi-mimo"].filter((m) => (m.kind || m.type) === "tts").map((m) => m.id);
expect(ttsModels).toEqual(["mimo-v2.5-tts"]);
expect(TTS_PROVIDER_CONFIG["xiaomi-mimo"].hasStyleInput).toBe(true);
expect(TTS_PROVIDER_CONFIG["xiaomi-mimo"].hasLanguageHint).toBe(true);
expect(TTS_PROVIDER_CONFIG["xiaomi-mimo"].languageOptions).toEqual(["Chinese", "English"]);
expect(TTS_PROVIDER_CONFIG["xiaomi-mimo"].hasVoiceIdInput).toBe(false);
});
});