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 消息可以播放
|
// 只有 assistant 消息可以播放
|
||||||
if (props.message.role !== 'assistant') return false
|
if (props.message.role !== 'assistant') return false
|
||||||
if (!copyableContent.value) return false
|
if (!copyableContent.value) return false
|
||||||
// OpenAI / Custom / Edge 不依赖浏览器 Web Speech API
|
// OpenAI / Custom / Edge / MiMo 不依赖浏览器 Web Speech API
|
||||||
if (voiceSettings.provider.value === 'openai' || voiceSettings.provider.value === 'custom' || voiceSettings.provider.value === 'edge') return true
|
if (voiceSettings.provider.value === 'openai' || voiceSettings.provider.value === 'custom' || voiceSettings.provider.value === 'edge' || voiceSettings.provider.value === 'mimo') return true
|
||||||
return speech.isSupported
|
return speech.isSupported
|
||||||
})
|
})
|
||||||
|
|
||||||
const isPlayingThisMessage = computed(() => {
|
const isPlayingThisMessage = computed(() => {
|
||||||
// OpenAI / Custom / Edge 模式
|
// OpenAI / Custom / Edge / MiMo 模式
|
||||||
if (voiceSettings.provider.value === 'openai' || voiceSettings.provider.value === 'custom' || voiceSettings.provider.value === 'edge') {
|
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.currentCustomMessageId.value === props.message.id && speech.isCustomPlaying.value
|
||||||
}
|
}
|
||||||
return speech.currentMessageId.value === props.message.id && speech.isPlaying.value
|
return speech.currentMessageId.value === props.message.id && speech.isPlaying.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const isPausedThisMessage = computed(() => {
|
const isPausedThisMessage = computed(() => {
|
||||||
// OpenAI / Custom / Edge 模式
|
// OpenAI / Custom / Edge / MiMo 模式
|
||||||
if (voiceSettings.provider.value === 'openai' || voiceSettings.provider.value === 'custom' || voiceSettings.provider.value === 'edge') {
|
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.currentCustomMessageId.value === props.message.id && speech.isCustomPaused.value
|
||||||
}
|
}
|
||||||
return speech.currentMessageId.value === props.message.id && speech.isPaused.value
|
return speech.currentMessageId.value === props.message.id && speech.isPaused.value
|
||||||
@@ -441,6 +441,24 @@ function handleSpeechToggle() {
|
|||||||
return
|
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 模式
|
// Web Speech API 模式
|
||||||
if (voiceSettings.provider.value === 'webspeech') {
|
if (voiceSettings.provider.value === 'webspeech') {
|
||||||
const text = speech.extractReadableText(content)
|
const text = speech.extractReadableText(content)
|
||||||
@@ -486,6 +504,18 @@ onMounted(() => {
|
|||||||
rate: speedToEdgeRate(voiceSettings.edgeRate.value),
|
rate: speedToEdgeRate(voiceSettings.edgeRate.value),
|
||||||
pitch: hzToEdgePitch(voiceSettings.edgePitchHz.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') {
|
} else if (voiceSettings.provider.value === 'webspeech') {
|
||||||
const text = speech.extractReadableText(content)
|
const text = speech.extractReadableText(content)
|
||||||
if (text) {
|
if (text) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const providerOptions = [
|
|||||||
{ label: t('settings.voice.providerOpenai'), value: 'openai' },
|
{ label: t('settings.voice.providerOpenai'), value: 'openai' },
|
||||||
{ label: t('settings.voice.providerCustom'), value: 'custom' },
|
{ label: t('settings.voice.providerCustom'), value: 'custom' },
|
||||||
{ label: t('settings.voice.providerEdge'), value: 'edge' },
|
{ label: t('settings.voice.providerEdge'), value: 'edge' },
|
||||||
|
{ label: t('settings.voice.providerMimo'), value: 'mimo' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const openaiModelOptions = [
|
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() {
|
async function handleTest() {
|
||||||
const text = testText.value.trim()
|
const text = testText.value.trim()
|
||||||
if (!text) return
|
if (!text) return
|
||||||
@@ -113,6 +136,19 @@ async function handleTest() {
|
|||||||
rate: speedToEdgeRate(vs.edgeRate.value),
|
rate: speedToEdgeRate(vs.edgeRate.value),
|
||||||
pitch: hzToEdgePitch(vs.edgePitchHz.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) {
|
} catch (err) {
|
||||||
console.error('[VoiceSettings] Test failed:', err)
|
console.error('[VoiceSettings] Test failed:', err)
|
||||||
@@ -312,6 +348,104 @@ async function handleTest() {
|
|||||||
|
|
||||||
</template>
|
</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 ─── -->
|
<!-- ─── Test / Audition ─── -->
|
||||||
<div class="test-section">
|
<div class="test-section">
|
||||||
<h4 class="test-title">{{ t('settings.voice.testTitle') }}</h4>
|
<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"
|
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 {
|
export interface SpeechState {
|
||||||
isPlaying: boolean
|
isPlaying: boolean
|
||||||
isPaused: boolean
|
isPaused: boolean
|
||||||
@@ -333,20 +342,17 @@ export function useSpeech() {
|
|||||||
function openaiToggle(messageId: string, content: string, opts: OpenaiTtsOptions) {
|
function openaiToggle(messageId: string, content: string, opts: OpenaiTtsOptions) {
|
||||||
if (currentCustomMessageId.value === messageId && isCustomPlaying.value) {
|
if (currentCustomMessageId.value === messageId && isCustomPlaying.value) {
|
||||||
if (isCustomPaused.value) {
|
if (isCustomPaused.value) {
|
||||||
// Resume
|
|
||||||
if (customAudio) {
|
if (customAudio) {
|
||||||
customAudio.play()
|
customAudio.play()
|
||||||
}
|
}
|
||||||
isCustomPaused.value = false
|
isCustomPaused.value = false
|
||||||
} else {
|
} else {
|
||||||
// Pause
|
|
||||||
if (customAudio) {
|
if (customAudio) {
|
||||||
customAudio.pause()
|
customAudio.pause()
|
||||||
}
|
}
|
||||||
isCustomPaused.value = true
|
isCustomPaused.value = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Stop other speech and start new
|
|
||||||
stop(false)
|
stop(false)
|
||||||
if (customAudio) {
|
if (customAudio) {
|
||||||
customAudio.pause()
|
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 ──────────────────────────────────────────
|
// ─── Unified speak ──────────────────────────────────────────
|
||||||
|
|
||||||
function speak(messageId: string, text: string, options: SpeechOptions = {}) {
|
function speak(messageId: string, text: string, options: SpeechOptions = {}) {
|
||||||
@@ -473,6 +621,10 @@ export function useSpeech() {
|
|||||||
openaiPlay,
|
openaiPlay,
|
||||||
openaiToggle,
|
openaiToggle,
|
||||||
|
|
||||||
|
// MiMo TTS
|
||||||
|
mimoPlay,
|
||||||
|
mimoToggle,
|
||||||
|
|
||||||
// Browser WebSpeech (直接调用避免 Rolldown 树摇)
|
// Browser WebSpeech (直接调用避免 Rolldown 树摇)
|
||||||
speakViaBrowser,
|
speakViaBrowser,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
export type TtsProvider = 'webspeech' | 'openai' | 'custom' | 'edge'
|
export type TtsProvider = 'webspeech' | 'openai' | 'custom' | 'edge' | 'mimo'
|
||||||
|
|
||||||
export interface VoiceSettingsData {
|
export interface VoiceSettingsData {
|
||||||
provider: TtsProvider
|
provider: TtsProvider
|
||||||
@@ -23,6 +23,14 @@ export interface VoiceSettingsData {
|
|||||||
edgeVoice: string
|
edgeVoice: string
|
||||||
edgeRate: number // 语速倍率 0.5~2.0,1.0 = 正常
|
edgeRate: number // 语速倍率 0.5~2.0,1.0 = 正常
|
||||||
edgePitchHz: number // 音调偏移 Hz,-20~20,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'
|
const STORAGE_KEY = 'hermes-tts-settings-v2'
|
||||||
@@ -67,6 +75,13 @@ const DEFAULT: VoiceSettingsData = {
|
|||||||
edgeVoice: 'zh-CN-XiaoxiaoNeural',
|
edgeVoice: 'zh-CN-XiaoxiaoNeural',
|
||||||
edgeRate: 1.0,
|
edgeRate: 1.0,
|
||||||
edgePitchHz: 0,
|
edgePitchHz: 0,
|
||||||
|
|
||||||
|
mimoApiKey: '',
|
||||||
|
mimoBaseUrl: 'https://api.xiaomimimo.com/v1',
|
||||||
|
mimoModel: 'mimo-v2.5-tts',
|
||||||
|
mimoVoice: '冰糖',
|
||||||
|
mimoVoiceDesignDesc: '',
|
||||||
|
mimoStylePrompt: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitize(data: VoiceSettingsData): VoiceSettingsData {
|
function sanitize(data: VoiceSettingsData): VoiceSettingsData {
|
||||||
@@ -110,10 +125,19 @@ const edgeVoice = ref<string>(load().edgeVoice)
|
|||||||
const edgeRate = ref<number>(load().edgeRate)
|
const edgeRate = ref<number>(load().edgeRate)
|
||||||
const edgePitchHz = ref<number>(load().edgePitchHz)
|
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
|
// Auto-persist on change
|
||||||
watch(
|
watch(
|
||||||
[provider, webspeechVoice, openaiApiKey, openaiBaseUrl, openaiModel, openaiVoice,
|
[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({
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||||
provider: provider.value,
|
provider: provider.value,
|
||||||
@@ -128,6 +152,12 @@ watch(
|
|||||||
edgeVoice: edgeVoice.value,
|
edgeVoice: edgeVoice.value,
|
||||||
edgeRate: edgeRate.value,
|
edgeRate: edgeRate.value,
|
||||||
edgePitchHz: edgePitchHz.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,
|
edgeVoice,
|
||||||
edgeRate,
|
edgeRate,
|
||||||
edgePitchHz,
|
edgePitchHz,
|
||||||
|
mimoApiKey,
|
||||||
|
mimoBaseUrl,
|
||||||
|
mimoModel,
|
||||||
|
mimoVoice,
|
||||||
|
mimoVoiceDesignDesc,
|
||||||
|
mimoStylePrompt,
|
||||||
|
|
||||||
setProvider(v: TtsProvider) { provider.value = v },
|
setProvider(v: TtsProvider) { provider.value = v },
|
||||||
setWebSpeechVoice(v: string) { webspeechVoice.value = v },
|
setWebSpeechVoice(v: string) { webspeechVoice.value = v },
|
||||||
@@ -159,6 +195,12 @@ export function useVoiceSettings() {
|
|||||||
setEdgeVoice(v: string) { edgeVoice.value = v },
|
setEdgeVoice(v: string) { edgeVoice.value = v },
|
||||||
setEdgeRate(v: number) { edgeRate.value = v },
|
setEdgeRate(v: number) { edgeRate.value = v },
|
||||||
setEdgePitchHz(v: number) { edgePitchHz.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() {
|
reset() {
|
||||||
provider.value = DEFAULT.provider
|
provider.value = DEFAULT.provider
|
||||||
@@ -173,6 +215,12 @@ export function useVoiceSettings() {
|
|||||||
edgeVoice.value = DEFAULT.edgeVoice
|
edgeVoice.value = DEFAULT.edgeVoice
|
||||||
edgeRate.value = DEFAULT.edgeRate
|
edgeRate.value = DEFAULT.edgeRate
|
||||||
edgePitchHz.value = DEFAULT.edgePitchHz
|
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',
|
testButton: 'Testen',
|
||||||
testButtonPlaying: 'Wiedergabe...',
|
testButtonPlaying: 'Wiedergabe...',
|
||||||
testFailed: 'Test fehlgeschlagen: {error}',
|
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: {
|
lockedIps: {
|
||||||
title: 'Gesperrte IPs',
|
title: 'Gesperrte IPs',
|
||||||
|
|||||||
@@ -847,6 +847,32 @@ export default {
|
|||||||
testButton: 'Test',
|
testButton: 'Test',
|
||||||
testButtonPlaying: 'Playing...',
|
testButtonPlaying: 'Playing...',
|
||||||
testFailed: 'Test failed: {error}',
|
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',
|
testButton: 'Probar',
|
||||||
testButtonPlaying: 'Reproduciendo...',
|
testButtonPlaying: 'Reproduciendo...',
|
||||||
testFailed: 'Prueba fallida: {error}',
|
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: {
|
lockedIps: {
|
||||||
title: 'IPs bloqueadas',
|
title: 'IPs bloqueadas',
|
||||||
|
|||||||
@@ -670,6 +670,32 @@ jobTriggered: 'Job declenche',
|
|||||||
testButton: 'Tester',
|
testButton: 'Tester',
|
||||||
testButtonPlaying: 'Lecture...',
|
testButtonPlaying: 'Lecture...',
|
||||||
testFailed: 'Echec du test : {error}',
|
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: {
|
lockedIps: {
|
||||||
title: 'IPs bloquees',
|
title: 'IPs bloquees',
|
||||||
|
|||||||
@@ -670,6 +670,32 @@ export default {
|
|||||||
testButton: 'テスト',
|
testButton: 'テスト',
|
||||||
testButtonPlaying: '再生中...',
|
testButtonPlaying: '再生中...',
|
||||||
testFailed: 'テスト失敗:{error}',
|
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: {
|
lockedIps: {
|
||||||
title: 'ロック済みIP管理',
|
title: 'ロック済みIP管理',
|
||||||
|
|||||||
@@ -670,6 +670,32 @@ export default {
|
|||||||
testButton: '테스트',
|
testButton: '테스트',
|
||||||
testButtonPlaying: '재생 중...',
|
testButtonPlaying: '재생 중...',
|
||||||
testFailed: '테스트 실패: {error}',
|
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: {
|
lockedIps: {
|
||||||
title: '잠긴 IP 관리',
|
title: '잠긴 IP 관리',
|
||||||
|
|||||||
@@ -670,6 +670,32 @@ jobTriggered: 'Job acionado',
|
|||||||
testButton: 'Testar',
|
testButton: 'Testar',
|
||||||
testButtonPlaying: 'Reproduzindo...',
|
testButtonPlaying: 'Reproduzindo...',
|
||||||
testFailed: 'Teste falhou: {error}',
|
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: {
|
lockedIps: {
|
||||||
title: 'IPs bloqueadas',
|
title: 'IPs bloqueadas',
|
||||||
|
|||||||
@@ -836,6 +836,32 @@ export default {
|
|||||||
testButton: '試聽',
|
testButton: '試聽',
|
||||||
testButtonPlaying: '播放中...',
|
testButtonPlaying: '播放中...',
|
||||||
testFailed: '測試失敗:{error}',
|
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: '试听',
|
testButton: '试听',
|
||||||
testButtonPlaying: '播放中...',
|
testButtonPlaying: '播放中...',
|
||||||
testFailed: '测试失败:{error}',
|
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