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>
|
||||
|
||||
Reference in New Issue
Block a user