mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(voice): dynamic settings voice picker with safe fallback + preview (#690)
* feat(voice): dynamic settings voice picker with safe fallback + preview * fix(voice): honor picker with configured agent and stop preview on unmount * fix(voice): apply PR review feedback for agent selection and preview cleanup
This commit is contained in:
@@ -403,6 +403,8 @@ export default {
|
||||
'settings.voice.title': 'Voice Assistant',
|
||||
'settings.voice.language': 'Voice Language',
|
||||
'settings.voice.autoDetect': 'Auto-detect',
|
||||
'settings.voice.voice': 'Voice',
|
||||
'settings.voice.voiceDefault': 'Default',
|
||||
'settings.about.title': 'About',
|
||||
'settings.about.website': 'Website',
|
||||
'settings.about.appVersion': 'App Version',
|
||||
|
||||
@@ -405,6 +405,8 @@ export default {
|
||||
'settings.voice.title': '语音助手',
|
||||
'settings.voice.language': '语音语言',
|
||||
'settings.voice.autoDetect': '自动检测',
|
||||
'settings.voice.voice': '声音',
|
||||
'settings.voice.voiceDefault': '默认',
|
||||
'settings.about.title': '关于',
|
||||
'settings.about.website': '官方网站',
|
||||
'settings.about.appVersion': '应用版本',
|
||||
|
||||
@@ -40,11 +40,12 @@ export function VoiceProvider({ children }: { children: ReactNode }) {
|
||||
setCurrentSessionId(sessionId)
|
||||
const initialContext = voiceHooks.onVoiceStarted(sessionId)
|
||||
|
||||
// Read voice language preference from localStorage
|
||||
// Read voice preferences from localStorage
|
||||
const voiceLang = localStorage.getItem('hapi-voice-lang')
|
||||
const elevenLabsLang = getElevenLabsCodeFromPreference(voiceLang)
|
||||
const voiceId = localStorage.getItem('hapi-voice-id') ?? undefined
|
||||
|
||||
await startRealtimeSession(sessionId, initialContext, elevenLabsLang)
|
||||
await startRealtimeSession(sessionId, initialContext, elevenLabsLang, voiceId)
|
||||
}, [])
|
||||
|
||||
const stopVoice = useCallback(async () => {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { getFallbackVoices } from './voices'
|
||||
|
||||
describe('getFallbackVoices', () => {
|
||||
it('returns localized Chinese aliases for zh-CN fallback list', () => {
|
||||
const voices = getFallbackVoices('zh-CN')
|
||||
expect(voices.some(v => /杰西卡|瑞秋|贝拉|乔什|亚当/.test(v.name))).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps canonical English names for en fallback list', () => {
|
||||
const voices = getFallbackVoices('en')
|
||||
expect(voices.some(v => v.name === 'Jessica')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Locale } from '@/lib/use-translation'
|
||||
|
||||
export interface Voice {
|
||||
id: string
|
||||
name: string
|
||||
gender: 'female' | 'male'
|
||||
description: string
|
||||
aliases?: Partial<Record<Locale, string>>
|
||||
}
|
||||
|
||||
export const VOICES: Voice[] = [
|
||||
{
|
||||
id: 'cgSgspJ2msm6clMCkdW9',
|
||||
name: 'Jessica',
|
||||
aliases: { 'zh-CN': '杰西卡' },
|
||||
gender: 'female',
|
||||
description: 'Default — warm, conversational',
|
||||
},
|
||||
{
|
||||
id: '21m00Tcm4TlvDq8ikWAM',
|
||||
name: 'Rachel',
|
||||
aliases: { 'zh-CN': '瑞秋' },
|
||||
gender: 'female',
|
||||
description: 'Calm, professional',
|
||||
},
|
||||
{
|
||||
id: 'EXAVITQu4vr4xnSDxMaL',
|
||||
name: 'Bella',
|
||||
aliases: { 'zh-CN': '贝拉' },
|
||||
gender: 'female',
|
||||
description: 'Soft, warm',
|
||||
},
|
||||
{
|
||||
id: 'TxGEqnHWrfWFTfGW9XjX',
|
||||
name: 'Josh',
|
||||
aliases: { 'zh-CN': '乔什' },
|
||||
gender: 'male',
|
||||
description: 'Deep, smooth',
|
||||
},
|
||||
{
|
||||
id: 'pNInz6obpgDQGcFmaJgB',
|
||||
name: 'Adam',
|
||||
aliases: { 'zh-CN': '亚当' },
|
||||
gender: 'male',
|
||||
description: 'Narration, clear',
|
||||
},
|
||||
{ id: 'AZnzlk1XvdvUeBnXmlld', name: 'Domi', gender: 'female', description: 'Strong, confident' },
|
||||
{ id: 'MF3mGyEYCl7XYWbV9V6O', name: 'Elli', gender: 'female', description: 'Young, clear' },
|
||||
{ id: 'VR6AewLTigWG4xSOukaG', name: 'Arnold', gender: 'male', description: 'Crisp, authoritative' },
|
||||
{ id: 'ErXwobaYiN019PkySvjV', name: 'Antoni', gender: 'male', description: 'Well-rounded' },
|
||||
{ id: 'yoZ06aMxZJJ28mfd3POQ', name: 'Sam', gender: 'male', description: 'Raspy, dynamic' },
|
||||
]
|
||||
|
||||
export const DEFAULT_VOICE_ID = 'cgSgspJ2msm6clMCkdW9'
|
||||
|
||||
export function getVoiceById(id: string | null): Voice | undefined {
|
||||
return VOICES.find(v => v.id === id)
|
||||
}
|
||||
|
||||
export function getFallbackVoices(locale: Locale): Voice[] {
|
||||
return VOICES.map((voice) => ({
|
||||
...voice,
|
||||
name: voice.aliases?.[locale] ?? voice.name,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user