Files
Hermes-ui/packages/client/src/composables/useVoiceSettings.ts
T
memeflyfly a68b9bf01f feat: add Edge TTS rate/pitch sliders to voice settings (#629)
Add speed (rate) and pitch controls for Edge TTS provider:
- Frontend: speedToEdgeRate()/hzToEdgePitch() helpers + UI sliders
- Backend: rate/pitch passthrough in OpenaiTtsRequest and controller
- i18n: add edgeRate/edgePitch keys across all 8 languages
- Rate: 0.5x-2.0x slider, Pitch: -20Hz to +20Hz slider
2026-05-11 21:56:11 +08:00

179 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
edgeRate: number // 语速倍率 0.5~2.01.0 = 正常
edgePitchHz: number // 音调偏移 Hz-20~200 = 正常
}
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',
edgeRate: 1.0,
edgePitchHz: 0,
}
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)
const edgeRate = ref<number>(load().edgeRate)
const edgePitchHz = ref<number>(load().edgePitchHz)
// Auto-persist on change
watch(
[provider, webspeechVoice, openaiApiKey, openaiBaseUrl, openaiModel, openaiVoice,
customUrl, customApiKey, edgeUrl, edgeVoice, edgeRate, edgePitchHz],
() => {
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,
edgeRate: edgeRate.value,
edgePitchHz: edgePitchHz.value,
}))
},
)
export function useVoiceSettings() {
return {
provider,
webspeechVoice,
openaiApiKey,
openaiBaseUrl,
openaiModel,
openaiVoice,
customUrl,
customApiKey,
edgeUrl,
edgeVoice,
edgeRate,
edgePitchHz,
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 },
setEdgeRate(v: number) { edgeRate.value = v },
setEdgePitchHz(v: number) { edgePitchHz.value = v },
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
edgeRate.value = DEFAULT.edgeRate
edgePitchHz.value = DEFAULT.edgePitchHz
},
}
}