Download src/hooks/useAutoTTS.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 3.4 kB
-
https://huggingface.co/chenbhao/codev/resolve/e40073cc959cd4729bf4009662bca374c5c4e266/src/hooks/useAutoTTS.ts
- Command line
-
hf download hf://chenbhao/codev@e40073cc959cd4729bf4009662bca374c5c4e266/src/hooks/useAutoTTS.ts
-
curl -L -o useAutoTTS.ts https://huggingface.co/chenbhao/codev/resolve/e40073cc959cd4729bf4009662bca374c5c4e266/src/hooks/useAutoTTS.ts
3.4 kB
| import { useEffect, useRef } from 'react' | |
| import { speakWithEdgeTTS, playAudioFile } from '../services/voice/edgeTTS.js' | |
| import { getInitialSettings } from '../utils/settings/settings.js' | |
| import type { RenderableMessage } from '../types/message.js' | |
| // Map language codes to Edge TTS Chinese voices | |
| const CHINESE_VOICES: Record<string, string> = { | |
| zh: 'zh-CN-XiaoxiaoNeural', | |
| 'zh-CN': 'zh-CN-XiaoxiaoNeural', | |
| 'zh-TW': 'zh-TW-HsiaoChenNeural', | |
| 'zh-HK': 'zh-HK-HiuMaanNeural', | |
| } | |
| function resolveEdgeTTSVoice(language: string | undefined, explicitVoice: string | undefined): string { | |
| if (explicitVoice) return explicitVoice | |
| if (!language) return 'en-US-JennyNeural' | |
| const base = language.split('-')[0].toLowerCase() | |
| if (base === 'zh') return CHINESE_VOICES[language] || CHINESE_VOICES['zh-CN'] | |
| if (base === 'ja') return 'ja-JP-NanamiNeural' | |
| if (base === 'ko') return 'ko-KR-SunHiNeural' | |
| if (base === 'fr') return 'fr-FR-DeniseNeural' | |
| if (base === 'de') return 'de-DE-KatjaNeural' | |
| if (base === 'es') return 'es-ES-ElviraNeural' | |
| return 'en-US-JennyNeural' | |
| } | |
| export function useAutoTTS(messages: RenderableMessage[], isLoading?: boolean): void { | |
| const triggeredIdsRef = useRef<Set<string>>(new Set()) | |
| const pendingPlayRef = useRef<Promise<void> | null>(null) | |
| // Snapshot of message IDs present when the conversation first becomes "active". | |
| // isLoading goes true on the first user submit and stays false on resume. | |
| // By waiting for isLoading=true we ensure the snapshot captures the correct | |
| // boundary: everything before it is history (skip), everything after is new. | |
| const historyIdsRef = useRef<Set<string> | null>(null) | |
| const wasLoadingRef = useRef<boolean>(false) | |
| useEffect(() => { | |
| const settings = getInitialSettings() | |
| if (!settings.voiceAutoTTS || !settings.voiceEnabled) return | |
| // Capture history snapshot on the transition from idle → active. | |
| // This fires once: when the user first submits a message in a fresh REPL, | |
| // or (importantly) never during a resume where isLoading stays false. | |
| if (!historyIdsRef.current && !wasLoadingRef.current && isLoading) { | |
| historyIdsRef.current = new Set(messages.map(m => m.uuid)) | |
| } | |
| wasLoadingRef.current = isLoading ?? false | |
| const voice = resolveEdgeTTSVoice( | |
| settings.voiceLanguage || settings.language, | |
| settings.voiceTTSVoice, | |
| ) | |
| for (const msg of messages) { | |
| if (triggeredIdsRef.current.has(msg.uuid)) continue | |
| // Skip messages that were present before the conversation became active. | |
| if (historyIdsRef.current?.has(msg.uuid)) continue | |
| triggeredIdsRef.current.add(msg.uuid) | |
| if (msg.type !== 'assistant') continue | |
| const content = msg.message.content[0] | |
| if (content?.type !== 'text') continue | |
| if (!content.text.trim()) continue | |
| const text = content.text | |
| const run = async () => { | |
| const result = await speakWithEdgeTTS(text, { | |
| voice, | |
| pythonPath: settings.voiceTTSCommand || undefined, | |
| }) | |
| if (result.success && result.audioPath) { | |
| await playAudioFile(result.audioPath) | |
| } | |
| } | |
| if (pendingPlayRef.current) { | |
| pendingPlayRef.current = pendingPlayRef.current.then(run, run).catch(() => {}) | |
| } else { | |
| pendingPlayRef.current = run().catch(() => {}) | |
| } | |
| } | |
| }, [messages, isLoading]) | |
| } |