feat: voice support zh-CN
Browse files---
STT(语音识别中文)
src/hooks/useVoice.ts 两处改成:
const rawLanguage = getInitialSettings().voiceLanguage || getInitialSettings().language
优先用 voiceLanguage 字段,没有则 fallback language。zh (中文) 已在 Deepgram 支持列表中。
TTS(中文语音输出)
src/hooks/useAutoTTS.ts 新增 resolveEdgeTTSVoice() 函数,根据语言自动选择 voice:
- zh / zh-CN → zh-CN-XiaoxiaoNeural
- zh-TW → zh-TW-HsiaoYuNeural
- zh-HK → zh-HK-HiuMaanNeural
- ja → ja-JP-NanamiNeural
- 其他 → en-US-JennyNeural(默认)
配置方法(在 ~/.claude/settings.json 里加):
{
voiceLanguage: zh-CN,
voiceTTSVoice: zh-CN-XiaoxiaoNeural
}
或者更简单,改 language 为 zh-CN 也能自动生效(两处都会读)。
- README.md +6 -1
- src/hooks/useAutoTTS.ts +23 -1
- src/hooks/useVoice.ts +3 -2
README.md
CHANGED
|
@@ -91,7 +91,12 @@ cp VersperClaw ~/.local/bin
|
|
| 91 |
|
| 92 |
# set auto-dream config
|
| 93 |
"autoMemoryEnabled": true,
|
| 94 |
-
"autoDreamEnabled": true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
}
|
| 96 |
```
|
| 97 |
|
|
|
|
| 91 |
|
| 92 |
# set auto-dream config
|
| 93 |
"autoMemoryEnabled": true,
|
| 94 |
+
"autoDreamEnabled": true,
|
| 95 |
+
|
| 96 |
+
# stt-tts support zh-CN
|
| 97 |
+
"voiceLanguage": "zh-CN",
|
| 98 |
+
"voiceTTSVoice": "zh-CN-XiaoxiaoNeural"
|
| 99 |
+
|
| 100 |
}
|
| 101 |
```
|
| 102 |
|
src/hooks/useAutoTTS.ts
CHANGED
|
@@ -3,6 +3,27 @@ import { speakWithEdgeTTS, playAudioFile } from '../services/voice/edgeTTS.js'
|
|
| 3 |
import { getInitialSettings } from '../utils/settings/settings.js'
|
| 4 |
import type { RenderableMessage } from '../types/message.js'
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
export function useAutoTTS(messages: RenderableMessage[]): void {
|
| 7 |
const triggeredIdsRef = useRef<Set<string>>(new Set())
|
| 8 |
const pendingPlayRef = useRef<Promise<void> | null>(null)
|
|
@@ -22,7 +43,8 @@ export function useAutoTTS(messages: RenderableMessage[]): void {
|
|
| 22 |
triggeredIdsRef.current.add(msg.uuid)
|
| 23 |
|
| 24 |
const text = content.text
|
| 25 |
-
const
|
|
|
|
| 26 |
|
| 27 |
const run = async () => {
|
| 28 |
const result = await speakWithEdgeTTS(text, {
|
|
|
|
| 3 |
import { getInitialSettings } from '../utils/settings/settings.js'
|
| 4 |
import type { RenderableMessage } from '../types/message.js'
|
| 5 |
|
| 6 |
+
// Map language codes to Edge TTS Chinese voices
|
| 7 |
+
const CHINESE_VOICES: Record<string, string> = {
|
| 8 |
+
zh: 'zh-CN-XiaoxiaoNeural',
|
| 9 |
+
'zh-CN': 'zh-CN-XiaoxiaoNeural',
|
| 10 |
+
'zh-TW': 'zh-TW-HsiaoYuNeural',
|
| 11 |
+
'zh-HK': 'zh-HK-HiuMaanNeural',
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
function resolveEdgeTTSVoice(language: string | undefined, explicitVoice: string | undefined): string {
|
| 15 |
+
if (explicitVoice) return explicitVoice
|
| 16 |
+
if (!language) return 'en-US-JennyNeural'
|
| 17 |
+
const base = language.split('-')[0].toLowerCase()
|
| 18 |
+
if (base === 'zh') return CHINESE_VOICES[language] || CHINESE_VOICES['zh-CN']
|
| 19 |
+
if (base === 'ja') return 'ja-JP-NanamiNeural'
|
| 20 |
+
if (base === 'ko') return 'ko-KR-SunHiNeural'
|
| 21 |
+
if (base === 'fr') return 'fr-FR-DeniseNeural'
|
| 22 |
+
if (base === 'de') return 'de-DE-KatjaNeural'
|
| 23 |
+
if (base === 'es') return 'es-ES-ElviraNeural'
|
| 24 |
+
return 'en-US-JennyNeural'
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
export function useAutoTTS(messages: RenderableMessage[]): void {
|
| 28 |
const triggeredIdsRef = useRef<Set<string>>(new Set())
|
| 29 |
const pendingPlayRef = useRef<Promise<void> | null>(null)
|
|
|
|
| 43 |
triggeredIdsRef.current.add(msg.uuid)
|
| 44 |
|
| 45 |
const text = content.text
|
| 46 |
+
const language = settings.voiceLanguage || settings.language
|
| 47 |
+
const voice = resolveEdgeTTSVoice(language, settings.voiceTTSVoice)
|
| 48 |
|
| 49 |
const run = async () => {
|
| 50 |
const result = await speakWithEdgeTTS(text, {
|
src/hooks/useVoice.ts
CHANGED
|
@@ -421,7 +421,8 @@ export function useVoice({
|
|
| 421 |
const replayBuffer = fullAudioRef.current
|
| 422 |
await sleep(250)
|
| 423 |
if (isStale()) return
|
| 424 |
-
const
|
|
|
|
| 425 |
const keyterms = await getVoiceKeyterms()
|
| 426 |
if (isStale()) return
|
| 427 |
await new Promise<void>(resolve => {
|
|
@@ -764,7 +765,7 @@ export function useVoice({
|
|
| 764 |
return
|
| 765 |
}
|
| 766 |
|
| 767 |
-
const rawLanguage = getInitialSettings().language
|
| 768 |
const stt = normalizeLanguageForSTT(rawLanguage)
|
| 769 |
logEvent('tengu_voice_recording_started', {
|
| 770 |
focusTriggered: focusTriggeredRef.current,
|
|
|
|
| 421 |
const replayBuffer = fullAudioRef.current
|
| 422 |
await sleep(250)
|
| 423 |
if (isStale()) return
|
| 424 |
+
const rawLanguage = getInitialSettings().voiceLanguage || getInitialSettings().language
|
| 425 |
+
const stt = normalizeLanguageForSTT(rawLanguage)
|
| 426 |
const keyterms = await getVoiceKeyterms()
|
| 427 |
if (isStale()) return
|
| 428 |
await new Promise<void>(resolve => {
|
|
|
|
| 765 |
return
|
| 766 |
}
|
| 767 |
|
| 768 |
+
const rawLanguage = getInitialSettings().voiceLanguage || getInitialSettings().language
|
| 769 |
const stt = normalizeLanguageForSTT(rawLanguage)
|
| 770 |
logEvent('tengu_voice_recording_started', {
|
| 771 |
focusTriggered: focusTriggeredRef.current,
|