2026-05-10 20:08:38 +08:00
|
|
|
|
import { ref, watch } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
export type TtsProvider = 'webspeech' | 'openai' | 'custom' | 'edge'
|
|
|
|
|
|
|
|
|
|
|
|
export interface VoiceSettingsData {
|
|
|
|
|
|
provider: TtsProvider
|
|
|
|
|
|
|
|
|
|
|
|
// WebSpeech
|
|
|
|
|
|
webspeechVoice: string
|
|
|
|
|
|
|
|
|
|
|
|
// OpenAI
|
|
|
|
|
|
openaiApiKey: string
|
|
|
|
|
|
openaiBaseUrl: string
|
|
|
|
|
|
openaiModel: string
|
|
|
|
|
|
openaiVoice: string
|
|
|
|
|
|
|
|
|
|
|
|
// Custom endpoint (OpenAI-compatible)
|
|
|
|
|
|
customUrl: string
|
|
|
|
|
|
customApiKey: string
|
|
|
|
|
|
|
|
|
|
|
|
// Edge TTS
|
|
|
|
|
|
edgeUrl: string
|
|
|
|
|
|
edgeVoice: string
|
2026-05-11 21:56:11 +08:00
|
|
|
|
edgeRate: number // 语速倍率 0.5~2.0,1.0 = 正常
|
|
|
|
|
|
edgePitchHz: number // 音调偏移 Hz,-20~20,0 = 正常
|
2026-05-10 20:08:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const STORAGE_KEY = 'hermes-tts-settings-v2'
|
|
|
|
|
|
|
|
|
|
|
|
function migrateOldKeys() {
|
|
|
|
|
|
const oldKey = 'hermes-tts-settings'
|
|
|
|
|
|
try {
|
|
|
|
|
|
const old = localStorage.getItem(oldKey)
|
|
|
|
|
|
if (old) {
|
|
|
|
|
|
const parsed = JSON.parse(old)
|
|
|
|
|
|
// Old 'custom' provider maps to new 'custom'
|
|
|
|
|
|
// Old 'gptsovits' provider maps to new 'custom'
|
|
|
|
|
|
if (parsed.provider === 'gptsovits') {
|
|
|
|
|
|
parsed.provider = 'custom'
|
|
|
|
|
|
// old gptsovitsUrl -> customUrl
|
|
|
|
|
|
if (parsed.gptsovitsUrl && !parsed.customUrl) {
|
|
|
|
|
|
parsed.customUrl = parsed.gptsovitsUrl
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Store as new format
|
|
|
|
|
|
const data = { ...DEFAULT, ...parsed }
|
|
|
|
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
|
|
|
|
|
|
localStorage.removeItem(oldKey)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch { /* ignore */ }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const DEFAULT: VoiceSettingsData = {
|
|
|
|
|
|
provider: 'webspeech',
|
|
|
|
|
|
|
|
|
|
|
|
webspeechVoice: '',
|
|
|
|
|
|
|
|
|
|
|
|
openaiApiKey: '',
|
|
|
|
|
|
openaiBaseUrl: '',
|
|
|
|
|
|
openaiModel: 'tts-1',
|
|
|
|
|
|
openaiVoice: 'alloy',
|
|
|
|
|
|
|
|
|
|
|
|
customUrl: '',
|
|
|
|
|
|
customApiKey: '',
|
|
|
|
|
|
|
|
|
|
|
|
edgeUrl: '',
|
|
|
|
|
|
edgeVoice: 'zh-CN-XiaoxiaoNeural',
|
2026-05-11 21:56:11 +08:00
|
|
|
|
edgeRate: 1.0,
|
|
|
|
|
|
edgePitchHz: 0,
|
2026-05-10 20:08:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sanitize(data: VoiceSettingsData): VoiceSettingsData {
|
|
|
|
|
|
// Clear old Edge TTS adapter URLs — now uses internal node-edge-tts
|
|
|
|
|
|
if (data.edgeUrl && data.edgeUrl !== '') {
|
|
|
|
|
|
data.edgeUrl = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return data
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function load(): VoiceSettingsData {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const raw = localStorage.getItem(STORAGE_KEY)
|
|
|
|
|
|
if (raw) return sanitize({ ...DEFAULT, ...JSON.parse(raw) })
|
|
|
|
|
|
} catch { /* ignore */ }
|
|
|
|
|
|
return { ...DEFAULT }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Run migration once on import
|
|
|
|
|
|
migrateOldKeys()
|
|
|
|
|
|
|
|
|
|
|
|
// ── Reactive state ──
|
|
|
|
|
|
const provider = ref<TtsProvider>(load().provider)
|
|
|
|
|
|
|
|
|
|
|
|
// WebSpeech
|
|
|
|
|
|
const webspeechVoice = ref<string>(load().webspeechVoice)
|
|
|
|
|
|
|
|
|
|
|
|
// OpenAI
|
|
|
|
|
|
const openaiApiKey = ref<string>(load().openaiApiKey)
|
|
|
|
|
|
const openaiBaseUrl = ref<string>(load().openaiBaseUrl)
|
|
|
|
|
|
const openaiModel = ref<string>(load().openaiModel)
|
|
|
|
|
|
const openaiVoice = ref<string>(load().openaiVoice)
|
|
|
|
|
|
|
|
|
|
|
|
// Custom
|
|
|
|
|
|
const customUrl = ref<string>(load().customUrl)
|
|
|
|
|
|
const customApiKey = ref<string>(load().customApiKey)
|
|
|
|
|
|
|
|
|
|
|
|
// Edge TTS
|
|
|
|
|
|
const edgeUrl = ref<string>(load().edgeUrl)
|
|
|
|
|
|
const edgeVoice = ref<string>(load().edgeVoice)
|
2026-05-11 21:56:11 +08:00
|
|
|
|
const edgeRate = ref<number>(load().edgeRate)
|
|
|
|
|
|
const edgePitchHz = ref<number>(load().edgePitchHz)
|
2026-05-10 20:08:38 +08:00
|
|
|
|
|
|
|
|
|
|
// Auto-persist on change
|
|
|
|
|
|
watch(
|
|
|
|
|
|
[provider, webspeechVoice, openaiApiKey, openaiBaseUrl, openaiModel, openaiVoice,
|
2026-05-11 21:56:11 +08:00
|
|
|
|
customUrl, customApiKey, edgeUrl, edgeVoice, edgeRate, edgePitchHz],
|
2026-05-10 20:08:38 +08:00
|
|
|
|
() => {
|
|
|
|
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
|
|
|
|
provider: provider.value,
|
|
|
|
|
|
webspeechVoice: webspeechVoice.value,
|
|
|
|
|
|
openaiApiKey: openaiApiKey.value,
|
|
|
|
|
|
openaiBaseUrl: openaiBaseUrl.value,
|
|
|
|
|
|
openaiModel: openaiModel.value,
|
|
|
|
|
|
openaiVoice: openaiVoice.value,
|
|
|
|
|
|
customUrl: customUrl.value,
|
|
|
|
|
|
customApiKey: customApiKey.value,
|
|
|
|
|
|
edgeUrl: edgeUrl.value,
|
|
|
|
|
|
edgeVoice: edgeVoice.value,
|
2026-05-11 21:56:11 +08:00
|
|
|
|
edgeRate: edgeRate.value,
|
|
|
|
|
|
edgePitchHz: edgePitchHz.value,
|
2026-05-10 20:08:38 +08:00
|
|
|
|
}))
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
export function useVoiceSettings() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
provider,
|
|
|
|
|
|
webspeechVoice,
|
|
|
|
|
|
openaiApiKey,
|
|
|
|
|
|
openaiBaseUrl,
|
|
|
|
|
|
openaiModel,
|
|
|
|
|
|
openaiVoice,
|
|
|
|
|
|
customUrl,
|
|
|
|
|
|
customApiKey,
|
|
|
|
|
|
edgeUrl,
|
|
|
|
|
|
edgeVoice,
|
2026-05-11 21:56:11 +08:00
|
|
|
|
edgeRate,
|
|
|
|
|
|
edgePitchHz,
|
2026-05-10 20:08:38 +08:00
|
|
|
|
|
|
|
|
|
|
setProvider(v: TtsProvider) { provider.value = v },
|
|
|
|
|
|
setWebSpeechVoice(v: string) { webspeechVoice.value = v },
|
|
|
|
|
|
setOpenaiApiKey(v: string) { openaiApiKey.value = v },
|
|
|
|
|
|
setOpenaiBaseUrl(v: string) { openaiBaseUrl.value = v },
|
|
|
|
|
|
setOpenaiModel(v: string) { openaiModel.value = v },
|
|
|
|
|
|
setOpenaiVoice(v: string) { openaiVoice.value = v },
|
|
|
|
|
|
setCustomUrl(v: string) { customUrl.value = v },
|
|
|
|
|
|
setCustomApiKey(v: string) { customApiKey.value = v },
|
|
|
|
|
|
setEdgeUrl(v: string) { edgeUrl.value = v },
|
|
|
|
|
|
setEdgeVoice(v: string) { edgeVoice.value = v },
|
2026-05-11 21:56:11 +08:00
|
|
|
|
setEdgeRate(v: number) { edgeRate.value = v },
|
|
|
|
|
|
setEdgePitchHz(v: number) { edgePitchHz.value = v },
|
2026-05-10 20:08:38 +08:00
|
|
|
|
|
|
|
|
|
|
reset() {
|
|
|
|
|
|
provider.value = DEFAULT.provider
|
|
|
|
|
|
webspeechVoice.value = DEFAULT.webspeechVoice
|
|
|
|
|
|
openaiApiKey.value = DEFAULT.openaiApiKey
|
|
|
|
|
|
openaiBaseUrl.value = DEFAULT.openaiBaseUrl
|
|
|
|
|
|
openaiModel.value = DEFAULT.openaiModel
|
|
|
|
|
|
openaiVoice.value = DEFAULT.openaiVoice
|
|
|
|
|
|
customUrl.value = DEFAULT.customUrl
|
|
|
|
|
|
customApiKey.value = DEFAULT.customApiKey
|
|
|
|
|
|
edgeUrl.value = DEFAULT.edgeUrl
|
|
|
|
|
|
edgeVoice.value = DEFAULT.edgeVoice
|
2026-05-11 21:56:11 +08:00
|
|
|
|
edgeRate.value = DEFAULT.edgeRate
|
|
|
|
|
|
edgePitchHz.value = DEFAULT.edgePitchHz
|
2026-05-10 20:08:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|