feat: add MiMo TTS provider 语音TTS提供接入MiMo (#752)
* feat: add MiMo TTS provider with preset voices, voice design and voice clone * refactor: remove MiMo voice clone feature
This commit is contained in:
@@ -371,22 +371,22 @@ const canPlaySpeech = computed(() => {
|
||||
// 只有 assistant 消息可以播放
|
||||
if (props.message.role !== 'assistant') return false
|
||||
if (!copyableContent.value) return false
|
||||
// OpenAI / Custom / Edge 不依赖浏览器 Web Speech API
|
||||
if (voiceSettings.provider.value === 'openai' || voiceSettings.provider.value === 'custom' || voiceSettings.provider.value === 'edge') return true
|
||||
// OpenAI / Custom / Edge / MiMo 不依赖浏览器 Web Speech API
|
||||
if (voiceSettings.provider.value === 'openai' || voiceSettings.provider.value === 'custom' || voiceSettings.provider.value === 'edge' || voiceSettings.provider.value === 'mimo') return true
|
||||
return speech.isSupported
|
||||
})
|
||||
|
||||
const isPlayingThisMessage = computed(() => {
|
||||
// OpenAI / Custom / Edge 模式
|
||||
if (voiceSettings.provider.value === 'openai' || voiceSettings.provider.value === 'custom' || voiceSettings.provider.value === 'edge') {
|
||||
// OpenAI / Custom / Edge / MiMo 模式
|
||||
if (voiceSettings.provider.value === 'openai' || voiceSettings.provider.value === 'custom' || voiceSettings.provider.value === 'edge' || voiceSettings.provider.value === 'mimo') {
|
||||
return speech.currentCustomMessageId.value === props.message.id && speech.isCustomPlaying.value
|
||||
}
|
||||
return speech.currentMessageId.value === props.message.id && speech.isPlaying.value
|
||||
})
|
||||
|
||||
const isPausedThisMessage = computed(() => {
|
||||
// OpenAI / Custom / Edge 模式
|
||||
if (voiceSettings.provider.value === 'openai' || voiceSettings.provider.value === 'custom' || voiceSettings.provider.value === 'edge') {
|
||||
// OpenAI / Custom / Edge / MiMo 模式
|
||||
if (voiceSettings.provider.value === 'openai' || voiceSettings.provider.value === 'custom' || voiceSettings.provider.value === 'edge' || voiceSettings.provider.value === 'mimo') {
|
||||
return speech.currentCustomMessageId.value === props.message.id && speech.isCustomPaused.value
|
||||
}
|
||||
return speech.currentMessageId.value === props.message.id && speech.isPaused.value
|
||||
@@ -441,6 +441,24 @@ function handleSpeechToggle() {
|
||||
return
|
||||
}
|
||||
|
||||
// MiMo TTS 模式
|
||||
if (voiceSettings.provider.value === 'mimo') {
|
||||
const apiKey = voiceSettings.mimoApiKey.value
|
||||
if (!apiKey) {
|
||||
console.warn('[MessageItem] MiMo TTS API Key 为空')
|
||||
return
|
||||
}
|
||||
speech.mimoToggle(props.message.id, content, {
|
||||
baseUrl: voiceSettings.mimoBaseUrl.value,
|
||||
apiKey,
|
||||
model: voiceSettings.mimoModel.value,
|
||||
voice: voiceSettings.mimoVoice.value,
|
||||
voiceDesignDesc: voiceSettings.mimoVoiceDesignDesc.value || undefined,
|
||||
stylePrompt: voiceSettings.mimoStylePrompt.value || undefined,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Web Speech API 模式
|
||||
if (voiceSettings.provider.value === 'webspeech') {
|
||||
const text = speech.extractReadableText(content)
|
||||
@@ -486,6 +504,18 @@ onMounted(() => {
|
||||
rate: speedToEdgeRate(voiceSettings.edgeRate.value),
|
||||
pitch: hzToEdgePitch(voiceSettings.edgePitchHz.value),
|
||||
})
|
||||
} else if (voiceSettings.provider.value === 'mimo') {
|
||||
const apiKey = voiceSettings.mimoApiKey.value
|
||||
if (apiKey) {
|
||||
speech.mimoPlay(props.message.id, content, {
|
||||
baseUrl: voiceSettings.mimoBaseUrl.value,
|
||||
apiKey,
|
||||
model: voiceSettings.mimoModel.value,
|
||||
voice: voiceSettings.mimoVoice.value,
|
||||
voiceDesignDesc: voiceSettings.mimoVoiceDesignDesc.value || undefined,
|
||||
stylePrompt: voiceSettings.mimoStylePrompt.value || undefined,
|
||||
})
|
||||
}
|
||||
} else if (voiceSettings.provider.value === 'webspeech') {
|
||||
const text = speech.extractReadableText(content)
|
||||
if (text) {
|
||||
|
||||
@@ -19,6 +19,7 @@ const providerOptions = [
|
||||
{ label: t('settings.voice.providerOpenai'), value: 'openai' },
|
||||
{ label: t('settings.voice.providerCustom'), value: 'custom' },
|
||||
{ label: t('settings.voice.providerEdge'), value: 'edge' },
|
||||
{ label: t('settings.voice.providerMimo'), value: 'mimo' },
|
||||
]
|
||||
|
||||
const openaiModelOptions = [
|
||||
@@ -76,6 +77,28 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// ── MiMo TTS options ──
|
||||
const mimoBaseUrlOptions = [
|
||||
{ label: 'https://api.xiaomimimo.com/v1', value: 'https://api.xiaomimimo.com/v1' },
|
||||
{ label: 'https://token-plan-cn.xiaomimimo.com/v1', value: 'https://token-plan-cn.xiaomimimo.com/v1' },
|
||||
]
|
||||
|
||||
const mimoModelOptions = [
|
||||
{ label: t('settings.voice.mimoModelPreset'), value: 'mimo-v2.5-tts' },
|
||||
{ label: t('settings.voice.mimoModelVoiceDesign'), value: 'mimo-v2.5-tts-voicedesign' },
|
||||
]
|
||||
|
||||
const mimoVoiceOptions = [
|
||||
{ label: '冰糖 (中文·女)', value: '冰糖' },
|
||||
{ label: '茉莉 (中文·女)', value: '茉莉' },
|
||||
{ label: '苏打 (中文·男)', value: '苏打' },
|
||||
{ label: '白桦 (中文·男)', value: '白桦' },
|
||||
{ label: 'Mia (English·Female)', value: 'Mia' },
|
||||
{ label: 'Chloe (English·Female)', value: 'Chloe' },
|
||||
{ label: 'Milo (English·Male)', value: 'Milo' },
|
||||
{ label: 'Dean (English·Male)', value: 'Dean' },
|
||||
]
|
||||
|
||||
async function handleTest() {
|
||||
const text = testText.value.trim()
|
||||
if (!text) return
|
||||
@@ -113,6 +136,19 @@ async function handleTest() {
|
||||
rate: speedToEdgeRate(vs.edgeRate.value),
|
||||
pitch: hzToEdgePitch(vs.edgePitchHz.value),
|
||||
})
|
||||
} else if (vs.provider.value === 'mimo') {
|
||||
if (!vs.mimoApiKey.value) {
|
||||
console.warn('[VoiceSettings] MiMo API Key empty')
|
||||
return
|
||||
}
|
||||
await speech.mimoPlay('__test__', text, {
|
||||
baseUrl: vs.mimoBaseUrl.value,
|
||||
apiKey: vs.mimoApiKey.value,
|
||||
model: vs.mimoModel.value,
|
||||
voice: vs.mimoVoice.value,
|
||||
voiceDesignDesc: vs.mimoVoiceDesignDesc.value || undefined,
|
||||
stylePrompt: vs.mimoStylePrompt.value || undefined,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[VoiceSettings] Test failed:', err)
|
||||
@@ -312,6 +348,104 @@ async function handleTest() {
|
||||
|
||||
</template>
|
||||
|
||||
<!-- ════ MiMo TTS ════ -->
|
||||
<template v-if="vs.provider.value === 'mimo'">
|
||||
<div class="provider-hint">
|
||||
{{ t('settings.voice.mimoHint') }}
|
||||
</div>
|
||||
|
||||
<SettingRow
|
||||
:label="t('settings.voice.mimoApiKey')"
|
||||
:hint="t('settings.voice.mimoApiKeyHint')"
|
||||
>
|
||||
<NInput
|
||||
:value="vs.mimoApiKey.value"
|
||||
type="password"
|
||||
size="small"
|
||||
show-password-on="click"
|
||||
style="width: 360px"
|
||||
:placeholder="t('settings.voice.mimoApiKeyPlaceholder')"
|
||||
@update:value="vs.setMimoApiKey"
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
:label="t('settings.voice.mimoBaseUrl')"
|
||||
:hint="t('settings.voice.mimoBaseUrlHint')"
|
||||
>
|
||||
<NSelect
|
||||
:value="vs.mimoBaseUrl.value"
|
||||
:options="mimoBaseUrlOptions"
|
||||
size="small"
|
||||
filterable
|
||||
tag
|
||||
style="width: 360px"
|
||||
@update:value="vs.setMimoBaseUrl"
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
:label="t('settings.voice.mimoModel')"
|
||||
:hint="t('settings.voice.mimoModelHint')"
|
||||
>
|
||||
<NSelect
|
||||
:value="vs.mimoModel.value"
|
||||
:options="mimoModelOptions"
|
||||
size="small"
|
||||
style="width: 320px"
|
||||
@update:value="vs.setMimoModel"
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<!-- Preset voice mode -->
|
||||
<SettingRow
|
||||
v-if="vs.mimoModel.value === 'mimo-v2.5-tts'"
|
||||
:label="t('settings.voice.mimoVoice')"
|
||||
:hint="t('settings.voice.mimoVoiceHint')"
|
||||
>
|
||||
<NSelect
|
||||
:value="vs.mimoVoice.value"
|
||||
:options="mimoVoiceOptions"
|
||||
size="small"
|
||||
style="width: 200px"
|
||||
@update:value="vs.setMimoVoice"
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<!-- Voice design mode -->
|
||||
<SettingRow
|
||||
v-if="vs.mimoModel.value === 'mimo-v2.5-tts-voicedesign'"
|
||||
:label="t('settings.voice.mimoVoiceDesignPrompt')"
|
||||
:hint="t('settings.voice.mimoVoiceDesignPromptHint')"
|
||||
>
|
||||
<NInput
|
||||
:value="vs.mimoVoiceDesignDesc.value"
|
||||
type="textarea"
|
||||
size="small"
|
||||
style="width: 360px"
|
||||
:rows="3"
|
||||
:placeholder="t('settings.voice.mimoVoiceDesignPromptPlaceholder')"
|
||||
@update:value="vs.setMimoVoiceDesignDesc"
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<!-- Style prompt (available for all models) -->
|
||||
<SettingRow
|
||||
:label="t('settings.voice.mimoStylePrompt')"
|
||||
:hint="t('settings.voice.mimoStylePromptHint')"
|
||||
>
|
||||
<NInput
|
||||
:value="vs.mimoStylePrompt.value"
|
||||
type="textarea"
|
||||
size="small"
|
||||
style="width: 360px"
|
||||
:rows="2"
|
||||
:placeholder="t('settings.voice.mimoStylePromptPlaceholder')"
|
||||
@update:value="vs.setMimoStylePrompt"
|
||||
/>
|
||||
</SettingRow>
|
||||
</template>
|
||||
|
||||
<!-- ─── Test / Audition ─── -->
|
||||
<div class="test-section">
|
||||
<h4 class="test-title">{{ t('settings.voice.testTitle') }}</h4>
|
||||
|
||||
@@ -15,6 +15,15 @@ export interface OpenaiTtsOptions {
|
||||
pitch?: string // Edge TTS pitch format, e.g. "-8Hz"
|
||||
}
|
||||
|
||||
export interface MimoTtsOptions {
|
||||
baseUrl: string
|
||||
apiKey: string
|
||||
model: string
|
||||
voice: string // preset voice ID (preset mode) or data URI (clone mode)
|
||||
voiceDesignDesc?: string // voice design description text (voice design mode)
|
||||
stylePrompt?: string // natural language style instruction
|
||||
}
|
||||
|
||||
export interface SpeechState {
|
||||
isPlaying: boolean
|
||||
isPaused: boolean
|
||||
@@ -333,20 +342,17 @@ export function useSpeech() {
|
||||
function openaiToggle(messageId: string, content: string, opts: OpenaiTtsOptions) {
|
||||
if (currentCustomMessageId.value === messageId && isCustomPlaying.value) {
|
||||
if (isCustomPaused.value) {
|
||||
// Resume
|
||||
if (customAudio) {
|
||||
customAudio.play()
|
||||
}
|
||||
isCustomPaused.value = false
|
||||
} else {
|
||||
// Pause
|
||||
if (customAudio) {
|
||||
customAudio.pause()
|
||||
}
|
||||
isCustomPaused.value = true
|
||||
}
|
||||
} else {
|
||||
// Stop other speech and start new
|
||||
stop(false)
|
||||
if (customAudio) {
|
||||
customAudio.pause()
|
||||
@@ -356,6 +362,148 @@ export function useSpeech() {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── MiMo TTS Engine ──────────────────────────────────────────
|
||||
|
||||
async function mimoPlay(
|
||||
messageId: string,
|
||||
content: string,
|
||||
opts: MimoTtsOptions,
|
||||
) {
|
||||
const text = extractReadableText(content)
|
||||
if (!text) return
|
||||
|
||||
const token = ++playbackToken
|
||||
|
||||
isCustomPlaying.value = true
|
||||
isCustomPaused.value = false
|
||||
currentCustomMessageId.value = messageId
|
||||
|
||||
// Build messages based on model type
|
||||
const messages: Array<{ role: string; content: string }> = []
|
||||
|
||||
if (opts.model === 'mimo-v2.5-tts-voicedesign') {
|
||||
// Voice design: user message = voice description (+ appended style prompt)
|
||||
const desc = opts.voiceDesignDesc || ''
|
||||
const userContent = opts.stylePrompt
|
||||
? `${desc}\n风格指令:${opts.stylePrompt}`
|
||||
: desc
|
||||
messages.push({ role: 'user', content: userContent || '默认音色' })
|
||||
} else {
|
||||
// Preset voices: user message = style prompt or empty
|
||||
messages.push({ role: 'user', content: opts.stylePrompt || '' })
|
||||
}
|
||||
|
||||
// assistant message = synthesis text
|
||||
messages.push({ role: 'assistant', content: text })
|
||||
|
||||
const audio: Record<string, any> = { format: 'wav' }
|
||||
// Voice design model does not accept audio.voice
|
||||
if (opts.model !== 'mimo-v2.5-tts-voicedesign') {
|
||||
audio.voice = opts.voice
|
||||
}
|
||||
|
||||
const body: Record<string, any> = {
|
||||
model: opts.model,
|
||||
messages,
|
||||
audio,
|
||||
}
|
||||
|
||||
const url = `${opts.baseUrl.replace(/\/+$/, '')}/chat/completions`
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'api-key': opts.apiKey,
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (token !== playbackToken) return
|
||||
|
||||
if (!res.ok) {
|
||||
const errText = await res.text().catch(() => '')
|
||||
throw new Error(`MiMo TTS 返回 ${res.status}: ${errText || res.statusText}`)
|
||||
}
|
||||
|
||||
const json = await res.json()
|
||||
if (token !== playbackToken) return
|
||||
|
||||
const audioBase64 = json?.choices?.[0]?.message?.audio?.data
|
||||
if (!audioBase64) {
|
||||
throw new Error('MiMo TTS 响应中未找到音频数据')
|
||||
}
|
||||
|
||||
// base64 → binary → Blob
|
||||
const binaryStr = atob(audioBase64)
|
||||
const bytes = new Uint8Array(binaryStr.length)
|
||||
for (let i = 0; i < binaryStr.length; i++) {
|
||||
bytes[i] = binaryStr.charCodeAt(i)
|
||||
}
|
||||
const audioBlob = new Blob([bytes], { type: 'audio/wav' })
|
||||
|
||||
if (token !== playbackToken) return
|
||||
|
||||
const audioUrl = URL.createObjectURL(audioBlob)
|
||||
const audio = new Audio(audioUrl)
|
||||
customAudio = audio
|
||||
|
||||
audio.onended = () => {
|
||||
if (token !== playbackToken) return
|
||||
URL.revokeObjectURL(audioUrl)
|
||||
isCustomPlaying.value = false
|
||||
isCustomPaused.value = false
|
||||
currentCustomMessageId.value = null
|
||||
customAudio = null
|
||||
}
|
||||
|
||||
audio.onerror = () => {
|
||||
if (token !== playbackToken) return
|
||||
URL.revokeObjectURL(audioUrl)
|
||||
console.warn('[useSpeech] MiMo TTS audio playback error')
|
||||
isCustomPlaying.value = false
|
||||
isCustomPaused.value = false
|
||||
currentCustomMessageId.value = null
|
||||
customAudio = null
|
||||
}
|
||||
|
||||
await audio.play()
|
||||
} catch (err) {
|
||||
if (token !== playbackToken) return
|
||||
console.error('[useSpeech] MiMo TTS 请求失败:', err)
|
||||
isCustomPlaying.value = false
|
||||
isCustomPaused.value = false
|
||||
currentCustomMessageId.value = null
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function mimoToggle(messageId: string, content: string, opts: MimoTtsOptions) {
|
||||
if (currentCustomMessageId.value === messageId && isCustomPlaying.value) {
|
||||
if (isCustomPaused.value) {
|
||||
if (customAudio) {
|
||||
customAudio.play()
|
||||
}
|
||||
isCustomPaused.value = false
|
||||
} else {
|
||||
if (customAudio) {
|
||||
customAudio.pause()
|
||||
}
|
||||
isCustomPaused.value = true
|
||||
}
|
||||
} else {
|
||||
stop(false)
|
||||
if (customAudio) {
|
||||
customAudio.pause()
|
||||
customAudio = null
|
||||
}
|
||||
mimoPlay(messageId, content, opts)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Unified speak ──────────────────────────────────────────
|
||||
|
||||
function speak(messageId: string, text: string, options: SpeechOptions = {}) {
|
||||
@@ -473,6 +621,10 @@ export function useSpeech() {
|
||||
openaiPlay,
|
||||
openaiToggle,
|
||||
|
||||
// MiMo TTS
|
||||
mimoPlay,
|
||||
mimoToggle,
|
||||
|
||||
// Browser WebSpeech (直接调用避免 Rolldown 树摇)
|
||||
speakViaBrowser,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
export type TtsProvider = 'webspeech' | 'openai' | 'custom' | 'edge'
|
||||
export type TtsProvider = 'webspeech' | 'openai' | 'custom' | 'edge' | 'mimo'
|
||||
|
||||
export interface VoiceSettingsData {
|
||||
provider: TtsProvider
|
||||
@@ -23,6 +23,14 @@ export interface VoiceSettingsData {
|
||||
edgeVoice: string
|
||||
edgeRate: number // 语速倍率 0.5~2.0,1.0 = 正常
|
||||
edgePitchHz: number // 音调偏移 Hz,-20~20,0 = 正常
|
||||
|
||||
// MiMo TTS
|
||||
mimoApiKey: string
|
||||
mimoBaseUrl: string
|
||||
mimoModel: string // 'mimo-v2.5-tts' | 'mimo-v2.5-tts-voicedesign'
|
||||
mimoVoice: string // 预置音色 ID
|
||||
mimoVoiceDesignDesc: string // 音色设计描述文本
|
||||
mimoStylePrompt: string // 风格指令
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'hermes-tts-settings-v2'
|
||||
@@ -67,6 +75,13 @@ const DEFAULT: VoiceSettingsData = {
|
||||
edgeVoice: 'zh-CN-XiaoxiaoNeural',
|
||||
edgeRate: 1.0,
|
||||
edgePitchHz: 0,
|
||||
|
||||
mimoApiKey: '',
|
||||
mimoBaseUrl: 'https://api.xiaomimimo.com/v1',
|
||||
mimoModel: 'mimo-v2.5-tts',
|
||||
mimoVoice: '冰糖',
|
||||
mimoVoiceDesignDesc: '',
|
||||
mimoStylePrompt: '',
|
||||
}
|
||||
|
||||
function sanitize(data: VoiceSettingsData): VoiceSettingsData {
|
||||
@@ -110,10 +125,19 @@ const edgeVoice = ref<string>(load().edgeVoice)
|
||||
const edgeRate = ref<number>(load().edgeRate)
|
||||
const edgePitchHz = ref<number>(load().edgePitchHz)
|
||||
|
||||
// MiMo TTS
|
||||
const mimoApiKey = ref<string>(load().mimoApiKey)
|
||||
const mimoBaseUrl = ref<string>(load().mimoBaseUrl)
|
||||
const mimoModel = ref<string>(load().mimoModel)
|
||||
const mimoVoice = ref<string>(load().mimoVoice)
|
||||
const mimoVoiceDesignDesc = ref<string>(load().mimoVoiceDesignDesc)
|
||||
const mimoStylePrompt = ref<string>(load().mimoStylePrompt)
|
||||
|
||||
// Auto-persist on change
|
||||
watch(
|
||||
[provider, webspeechVoice, openaiApiKey, openaiBaseUrl, openaiModel, openaiVoice,
|
||||
customUrl, customApiKey, edgeUrl, edgeVoice, edgeRate, edgePitchHz],
|
||||
customUrl, customApiKey, edgeUrl, edgeVoice, edgeRate, edgePitchHz,
|
||||
mimoApiKey, mimoBaseUrl, mimoModel, mimoVoice, mimoVoiceDesignDesc, mimoStylePrompt],
|
||||
() => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||
provider: provider.value,
|
||||
@@ -128,6 +152,12 @@ watch(
|
||||
edgeVoice: edgeVoice.value,
|
||||
edgeRate: edgeRate.value,
|
||||
edgePitchHz: edgePitchHz.value,
|
||||
mimoApiKey: mimoApiKey.value,
|
||||
mimoBaseUrl: mimoBaseUrl.value,
|
||||
mimoModel: mimoModel.value,
|
||||
mimoVoice: mimoVoice.value,
|
||||
mimoVoiceDesignDesc: mimoVoiceDesignDesc.value,
|
||||
mimoStylePrompt: mimoStylePrompt.value,
|
||||
}))
|
||||
},
|
||||
)
|
||||
@@ -146,6 +176,12 @@ export function useVoiceSettings() {
|
||||
edgeVoice,
|
||||
edgeRate,
|
||||
edgePitchHz,
|
||||
mimoApiKey,
|
||||
mimoBaseUrl,
|
||||
mimoModel,
|
||||
mimoVoice,
|
||||
mimoVoiceDesignDesc,
|
||||
mimoStylePrompt,
|
||||
|
||||
setProvider(v: TtsProvider) { provider.value = v },
|
||||
setWebSpeechVoice(v: string) { webspeechVoice.value = v },
|
||||
@@ -159,6 +195,12 @@ export function useVoiceSettings() {
|
||||
setEdgeVoice(v: string) { edgeVoice.value = v },
|
||||
setEdgeRate(v: number) { edgeRate.value = v },
|
||||
setEdgePitchHz(v: number) { edgePitchHz.value = v },
|
||||
setMimoApiKey(v: string) { mimoApiKey.value = v },
|
||||
setMimoBaseUrl(v: string) { mimoBaseUrl.value = v },
|
||||
setMimoModel(v: string) { mimoModel.value = v },
|
||||
setMimoVoice(v: string) { mimoVoice.value = v },
|
||||
setMimoVoiceDesignDesc(v: string) { mimoVoiceDesignDesc.value = v },
|
||||
setMimoStylePrompt(v: string) { mimoStylePrompt.value = v },
|
||||
|
||||
reset() {
|
||||
provider.value = DEFAULT.provider
|
||||
@@ -173,6 +215,12 @@ export function useVoiceSettings() {
|
||||
edgeVoice.value = DEFAULT.edgeVoice
|
||||
edgeRate.value = DEFAULT.edgeRate
|
||||
edgePitchHz.value = DEFAULT.edgePitchHz
|
||||
mimoApiKey.value = DEFAULT.mimoApiKey
|
||||
mimoBaseUrl.value = DEFAULT.mimoBaseUrl
|
||||
mimoModel.value = DEFAULT.mimoModel
|
||||
mimoVoice.value = DEFAULT.mimoVoice
|
||||
mimoVoiceDesignDesc.value = DEFAULT.mimoVoiceDesignDesc
|
||||
mimoStylePrompt.value = DEFAULT.mimoStylePrompt
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -670,6 +670,32 @@ jobTriggered: 'Job ausgelost',
|
||||
testButton: 'Testen',
|
||||
testButtonPlaying: 'Wiedergabe...',
|
||||
testFailed: 'Test fehlgeschlagen: {error}',
|
||||
|
||||
// MiMo TTS
|
||||
providerMimo: 'MiMo TTS',
|
||||
mimoHint: 'Xiaomi MiMo TTS — unterstützt Voreingestellte Stimmen, Stimmdesign und Stimmklonung',
|
||||
mimoApiKey: 'API-Schluessel',
|
||||
mimoApiKeyHint: 'Holen Sie sich Ihren Schluessel auf platform.xiaomimimo.com',
|
||||
mimoApiKeyPlaceholder: 'MiMo API-Schluessel',
|
||||
mimoBaseUrl: 'Basis-URL',
|
||||
mimoBaseUrlHint: 'MiMo API-Endpunkt-URL',
|
||||
mimoModel: 'Modell',
|
||||
mimoModelHint: 'Sprachsynthesemodell auswählen',
|
||||
mimoModelPreset: 'Voreingestellte Stimmen',
|
||||
mimoModelVoiceDesign: 'Stimmdesign',
|
||||
mimoModelVoiceClone: 'Stimmklonung',
|
||||
mimoVoice: 'Stimme',
|
||||
mimoVoiceHint: 'Voreingestellte Stimme auswählen',
|
||||
mimoVoiceDesignPrompt: 'Stimmbeschreibung',
|
||||
mimoVoiceDesignPromptHint: 'Beschreiben Sie die gewünschten Stimmmerkmale',
|
||||
mimoVoiceDesignPromptPlaceholder: 'Z.B.: Eine warme junge Frauenstimme, etwas langsam, mit magnetischem Ton',
|
||||
mimoCloneAudio: 'Audio hochladen',
|
||||
mimoCloneAudioHint: 'Audio-Beispiel für Stimmklonung hochladen (mp3/wav, max. 10 MB)',
|
||||
mimoCloneAudioUpload: 'Datei auswählen',
|
||||
mimoCloneAudioClear: 'Löschen',
|
||||
mimoStylePrompt: 'Stil-Eingabe',
|
||||
mimoStylePromptHint: 'Optional — beschreiben Sie den Sprechstil in natürlicher Sprache',
|
||||
mimoStylePromptPlaceholder: 'Z.B.: Heller, lebhafter Ton, schnelles Tempo',
|
||||
},
|
||||
lockedIps: {
|
||||
title: 'Gesperrte IPs',
|
||||
|
||||
@@ -847,6 +847,32 @@ export default {
|
||||
testButton: 'Test',
|
||||
testButtonPlaying: 'Playing...',
|
||||
testFailed: 'Test failed: {error}',
|
||||
|
||||
// MiMo TTS
|
||||
providerMimo: 'MiMo TTS',
|
||||
mimoHint: 'Xiaomi MiMo TTS — supports preset voices, voice design, and voice clone modes',
|
||||
mimoApiKey: 'API Key',
|
||||
mimoApiKeyHint: 'Get your key at platform.xiaomimimo.com',
|
||||
mimoApiKeyPlaceholder: 'MiMo API Key',
|
||||
mimoBaseUrl: 'Base URL',
|
||||
mimoBaseUrlHint: 'MiMo API endpoint URL',
|
||||
mimoModel: 'Model',
|
||||
mimoModelHint: 'Select speech synthesis model',
|
||||
mimoModelPreset: 'Preset Voices',
|
||||
mimoModelVoiceDesign: 'Voice Design',
|
||||
mimoModelVoiceClone: 'Voice Clone',
|
||||
mimoVoice: 'Voice',
|
||||
mimoVoiceHint: 'Select a preset voice',
|
||||
mimoVoiceDesignPrompt: 'Voice Description',
|
||||
mimoVoiceDesignPromptHint: 'Describe the voice characteristics you want',
|
||||
mimoVoiceDesignPromptPlaceholder: 'e.g., A warm young female voice, slightly slow, with a magnetic tone',
|
||||
mimoCloneAudio: 'Upload Audio',
|
||||
mimoCloneAudioHint: 'Upload an audio sample for voice cloning (mp3/wav, max 10MB)',
|
||||
mimoCloneAudioUpload: 'Choose File',
|
||||
mimoCloneAudioClear: 'Clear',
|
||||
mimoStylePrompt: 'Style Prompt',
|
||||
mimoStylePromptHint: 'Optional — describe the speaking style in natural language',
|
||||
mimoStylePromptPlaceholder: 'e.g., Bright and bouncy tone, fast pace',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -670,6 +670,32 @@ jobTriggered: 'Job ejecutado',
|
||||
testButton: 'Probar',
|
||||
testButtonPlaying: 'Reproduciendo...',
|
||||
testFailed: 'Prueba fallida: {error}',
|
||||
|
||||
// MiMo TTS
|
||||
providerMimo: 'MiMo TTS',
|
||||
mimoHint: 'Xiaomi MiMo TTS — voces predefinidas, diseño de voz y clonación de voz',
|
||||
mimoApiKey: 'Clave API',
|
||||
mimoApiKeyHint: 'Obtenga su clave en platform.xiaomimimo.com',
|
||||
mimoApiKeyPlaceholder: 'Clave API MiMo',
|
||||
mimoBaseUrl: 'URL base',
|
||||
mimoBaseUrlHint: 'URL del endpoint de la API MiMo',
|
||||
mimoModel: 'Modelo',
|
||||
mimoModelHint: 'Seleccione el modelo de síntesis de voz',
|
||||
mimoModelPreset: 'Voces predefinidas',
|
||||
mimoModelVoiceDesign: 'Diseño de voz',
|
||||
mimoModelVoiceClone: 'Clonación de voz',
|
||||
mimoVoice: 'Voz',
|
||||
mimoVoiceHint: 'Seleccione una voz predefinida',
|
||||
mimoVoiceDesignPrompt: 'Descripción de voz',
|
||||
mimoVoiceDesignPromptHint: 'Describa las características de voz deseadas',
|
||||
mimoVoiceDesignPromptPlaceholder: 'Ej: Una voz femenina cálida y joven, algo lenta, con tono magnético',
|
||||
mimoCloneAudio: 'Subir audio',
|
||||
mimoCloneAudioHint: 'Suba una muestra de audio para clonación (mp3/wav, máx. 10 MB)',
|
||||
mimoCloneAudioUpload: 'Elegir archivo',
|
||||
mimoCloneAudioClear: 'Borrar',
|
||||
mimoStylePrompt: 'Indicador de estilo',
|
||||
mimoStylePromptHint: 'Opcional — describa el estilo de habla en lenguaje natural',
|
||||
mimoStylePromptPlaceholder: 'Ej: Tono brillante y animado, ritmo rápido',
|
||||
},
|
||||
lockedIps: {
|
||||
title: 'IPs bloqueadas',
|
||||
|
||||
@@ -670,6 +670,32 @@ jobTriggered: 'Job declenche',
|
||||
testButton: 'Tester',
|
||||
testButtonPlaying: 'Lecture...',
|
||||
testFailed: 'Echec du test : {error}',
|
||||
|
||||
// MiMo TTS
|
||||
providerMimo: 'MiMo TTS',
|
||||
mimoHint: 'Xiaomi MiMo TTS — voices predefinies, conception vocale et clonage vocal',
|
||||
mimoApiKey: 'Cle API',
|
||||
mimoApiKeyHint: 'Obtenez votre cle sur platform.xiaomimimo.com',
|
||||
mimoApiKeyPlaceholder: 'Cle API MiMo',
|
||||
mimoBaseUrl: 'URL de base',
|
||||
mimoBaseUrlHint: 'URL de l\'endpoint API MiMo',
|
||||
mimoModel: 'Modele',
|
||||
mimoModelHint: 'Selectionnez le modele de synthese vocale',
|
||||
mimoModelPreset: 'Voix predefinies',
|
||||
mimoModelVoiceDesign: 'Conception vocale',
|
||||
mimoModelVoiceClone: 'Clonage vocal',
|
||||
mimoVoice: 'Voix',
|
||||
mimoVoiceHint: 'Selectionnez une voix predefinie',
|
||||
mimoVoiceDesignPrompt: 'Description vocale',
|
||||
mimoVoiceDesignPromptHint: 'Decrivez les caracteristiques vocales souhaitees',
|
||||
mimoVoiceDesignPromptPlaceholder: 'Ex : Une voix feminine chaude et jeune, legerement lente, avec un ton magnetique',
|
||||
mimoCloneAudio: 'Televerser un audio',
|
||||
mimoCloneAudioHint: 'Televersez un echantillon audio pour le clonage (mp3/wav, max 10 Mo)',
|
||||
mimoCloneAudioUpload: 'Choisir un fichier',
|
||||
mimoCloneAudioClear: 'Effacer',
|
||||
mimoStylePrompt: 'Invite de style',
|
||||
mimoStylePromptHint: 'Optionnel — decrivez le style de parole en langage naturel',
|
||||
mimoStylePromptPlaceholder: 'Ex : Ton vif et entrain, rythme rapide',
|
||||
},
|
||||
lockedIps: {
|
||||
title: 'IPs bloquees',
|
||||
|
||||
@@ -670,6 +670,32 @@ export default {
|
||||
testButton: 'テスト',
|
||||
testButtonPlaying: '再生中...',
|
||||
testFailed: 'テスト失敗:{error}',
|
||||
|
||||
// MiMo TTS
|
||||
providerMimo: 'MiMo TTS',
|
||||
mimoHint: 'Xiaomi MiMo TTS — プリセット音声、音声デザイン、音声クローンの3つのモードをサポート',
|
||||
mimoApiKey: 'API Key',
|
||||
mimoApiKeyHint: 'platform.xiaomimimo.com で取得',
|
||||
mimoApiKeyPlaceholder: 'MiMo API Key',
|
||||
mimoBaseUrl: 'Base URL',
|
||||
mimoBaseUrlHint: 'MiMo API エンドポイントURL',
|
||||
mimoModel: 'モデル',
|
||||
mimoModelHint: '音声合成モデルを選択',
|
||||
mimoModelPreset: 'プリセット音声',
|
||||
mimoModelVoiceDesign: '音声デザイン',
|
||||
mimoModelVoiceClone: '音声クローン',
|
||||
mimoVoice: '音声',
|
||||
mimoVoiceHint: 'プリセット音声を選択',
|
||||
mimoVoiceDesignPrompt: '音声の説明',
|
||||
mimoVoiceDesignPromptHint: '希望する音声の特徴を説明してください',
|
||||
mimoVoiceDesignPromptPlaceholder: '例:温かみのある若い女性の声、少しゆっくり、磁力的なトーン',
|
||||
mimoCloneAudio: '音声アップロード',
|
||||
mimoCloneAudioHint: '音声クローン用の音声サンプルをアップロード(mp3/wav、最大10MB)',
|
||||
mimoCloneAudioUpload: 'ファイルを選択',
|
||||
mimoCloneAudioClear: 'クリア',
|
||||
mimoStylePrompt: 'スタイルプロンプト',
|
||||
mimoStylePromptHint: 'オプション — 自然言語で話すスタイルを説明',
|
||||
mimoStylePromptPlaceholder: '例:明るく弾むようなトーン、速めのテンポ',
|
||||
},
|
||||
lockedIps: {
|
||||
title: 'ロック済みIP管理',
|
||||
|
||||
@@ -670,6 +670,32 @@ export default {
|
||||
testButton: '테스트',
|
||||
testButtonPlaying: '재생 중...',
|
||||
testFailed: '테스트 실패: {error}',
|
||||
|
||||
// MiMo TTS
|
||||
providerMimo: 'MiMo TTS',
|
||||
mimoHint: '샤오미 MiMo TTS — 프리셋 음성, 음성 디자인, 음성 클론 세 가지 모드 지원',
|
||||
mimoApiKey: 'API Key',
|
||||
mimoApiKeyHint: 'platform.xiaomimimo.com에서 발급',
|
||||
mimoApiKeyPlaceholder: 'MiMo API Key',
|
||||
mimoBaseUrl: 'Base URL',
|
||||
mimoBaseUrlHint: 'MiMo API 엔드포인트 URL',
|
||||
mimoModel: '모델',
|
||||
mimoModelHint: '음성 합성 모델 선택',
|
||||
mimoModelPreset: '프리셋 음성',
|
||||
mimoModelVoiceDesign: '음성 디자인',
|
||||
mimoModelVoiceClone: '음성 클론',
|
||||
mimoVoice: '음성',
|
||||
mimoVoiceHint: '프리셋 음성 선택',
|
||||
mimoVoiceDesignPrompt: '음성 설명',
|
||||
mimoVoiceDesignPromptHint: '원하는 음성 특징을 설명하세요',
|
||||
mimoVoiceDesignPromptPlaceholder: '예: 따뜻한 젊은 여성 목소리, 약간 느린 속도, 마그네틱한 톤',
|
||||
mimoCloneAudio: '오디오 업로드',
|
||||
mimoCloneAudioHint: '음성 클론용 오디오 샘플 업로드 (mp3/wav, 최대 10MB)',
|
||||
mimoCloneAudioUpload: '파일 선택',
|
||||
mimoCloneAudioClear: '지우기',
|
||||
mimoStylePrompt: '스타일 프롬프트',
|
||||
mimoStylePromptHint: '선택사항 — 자연어로 말하기 스타일 설명',
|
||||
mimoStylePromptPlaceholder: '예: 밝고 경쾌한 톤, 빠른 속도',
|
||||
},
|
||||
lockedIps: {
|
||||
title: '잠긴 IP 관리',
|
||||
|
||||
@@ -670,6 +670,32 @@ jobTriggered: 'Job acionado',
|
||||
testButton: 'Testar',
|
||||
testButtonPlaying: 'Reproduzindo...',
|
||||
testFailed: 'Teste falhou: {error}',
|
||||
|
||||
// MiMo TTS
|
||||
providerMimo: 'MiMo TTS',
|
||||
mimoHint: 'Xiaomi MiMo TTS — vozes predefinidas, design de voz e clonagem de voz',
|
||||
mimoApiKey: 'Chave API',
|
||||
mimoApiKeyHint: 'Obtenha sua chave em platform.xiaomimimo.com',
|
||||
mimoApiKeyPlaceholder: 'Chave API MiMo',
|
||||
mimoBaseUrl: 'URL base',
|
||||
mimoBaseUrlHint: 'URL do endpoint da API MiMo',
|
||||
mimoModel: 'Modelo',
|
||||
mimoModelHint: 'Selecione o modelo de síntese de voz',
|
||||
mimoModelPreset: 'Vozes predefinidas',
|
||||
mimoModelVoiceDesign: 'Design de voz',
|
||||
mimoModelVoiceClone: 'Clonagem de voz',
|
||||
mimoVoice: 'Voz',
|
||||
mimoVoiceHint: 'Selecione uma voz predefinida',
|
||||
mimoVoiceDesignPrompt: 'Descrição da voz',
|
||||
mimoVoiceDesignPromptHint: 'Descreva as características de voz desejadas',
|
||||
mimoVoiceDesignPromptPlaceholder: 'Ex: Uma voz feminina quente e jovem, ligeiramente lenta, com tom magnético',
|
||||
mimoCloneAudio: 'Enviar áudio',
|
||||
mimoCloneAudioHint: 'Envie uma amostra de áudio para clonagem (mp3/wav, máx. 10 MB)',
|
||||
mimoCloneAudioUpload: 'Escolher arquivo',
|
||||
mimoCloneAudioClear: 'Limpar',
|
||||
mimoStylePrompt: 'Prompt de estilo',
|
||||
mimoStylePromptHint: 'Opcional — descreva o estilo de fala em linguagem natural',
|
||||
mimoStylePromptPlaceholder: 'Ex: Tom brilhante e animado, ritmo rápido',
|
||||
},
|
||||
lockedIps: {
|
||||
title: 'IPs bloqueadas',
|
||||
|
||||
@@ -836,6 +836,32 @@ export default {
|
||||
testButton: '試聽',
|
||||
testButtonPlaying: '播放中...',
|
||||
testFailed: '測試失敗:{error}',
|
||||
|
||||
// MiMo TTS
|
||||
providerMimo: 'MiMo TTS',
|
||||
mimoHint: '小米 MiMo TTS,支援預設音色、音色設計、音色複製三種模式',
|
||||
mimoApiKey: 'API Key',
|
||||
mimoApiKeyHint: '在 platform.xiaomimimo.com 取得',
|
||||
mimoApiKeyPlaceholder: 'MiMo API Key',
|
||||
mimoBaseUrl: 'Base URL',
|
||||
mimoBaseUrlHint: 'MiMo API 端點位址',
|
||||
mimoModel: '模型',
|
||||
mimoModelHint: '選擇語音合成模型',
|
||||
mimoModelPreset: '預設音色',
|
||||
mimoModelVoiceDesign: '音色設計',
|
||||
mimoModelVoiceClone: '音色複製',
|
||||
mimoVoice: '音色',
|
||||
mimoVoiceHint: '選擇預設音色',
|
||||
mimoVoiceDesignPrompt: '音色描述',
|
||||
mimoVoiceDesignPromptHint: '描述你想要的音色特徵',
|
||||
mimoVoiceDesignPromptPlaceholder: '例如:溫柔的年輕女聲,語速稍慢,帶著磁性',
|
||||
mimoCloneAudio: '上傳音訊',
|
||||
mimoCloneAudioHint: '上傳音訊樣本用於音色複製,支援 mp3/wav,最大 10MB',
|
||||
mimoCloneAudioUpload: '選擇檔案',
|
||||
mimoCloneAudioClear: '清除音訊',
|
||||
mimoStylePrompt: '風格指令',
|
||||
mimoStylePromptHint: '可選,用自然語言描述語音風格',
|
||||
mimoStylePromptPlaceholder: '例如:用輕快上揚的語調,語速稍快',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -839,6 +839,32 @@ export default {
|
||||
testButton: '试听',
|
||||
testButtonPlaying: '播放中...',
|
||||
testFailed: '测试失败:{error}',
|
||||
|
||||
// MiMo TTS
|
||||
providerMimo: 'MiMo TTS',
|
||||
mimoHint: '小米 MiMo TTS,支持预置音色、音色设计、音色复刻三种模式',
|
||||
mimoApiKey: 'API Key',
|
||||
mimoApiKeyHint: '在 platform.xiaomimimo.com 获取',
|
||||
mimoApiKeyPlaceholder: 'MiMo API Key',
|
||||
mimoBaseUrl: 'Base URL',
|
||||
mimoBaseUrlHint: 'MiMo API 端点地址',
|
||||
mimoModel: '模型',
|
||||
mimoModelHint: '选择语音合成模型',
|
||||
mimoModelPreset: '预置音色',
|
||||
mimoModelVoiceDesign: '音色设计',
|
||||
mimoModelVoiceClone: '音色复刻',
|
||||
mimoVoice: '音色',
|
||||
mimoVoiceHint: '选择预置音色',
|
||||
mimoVoiceDesignPrompt: '音色描述',
|
||||
mimoVoiceDesignPromptHint: '描述你想要的音色特征',
|
||||
mimoVoiceDesignPromptPlaceholder: '例如:温柔的年轻女声,语速稍慢,带着磁性',
|
||||
mimoCloneAudio: '上传音频',
|
||||
mimoCloneAudioHint: '上传音频样本用于音色复刻,支持 mp3/wav,最大 10MB',
|
||||
mimoCloneAudioUpload: '选择文件',
|
||||
mimoCloneAudioClear: '清除音频',
|
||||
mimoStylePrompt: '风格指令',
|
||||
mimoStylePromptHint: '可选,用自然语言描述语音风格',
|
||||
mimoStylePromptPlaceholder: '例如:用轻快上扬的语调,语速稍快',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user