Character schema v2: background, dialogue_style, appearance, skills, gaze_presets with automatic v1→v2 migration. LLM-assisted character creation via Character MCP server. Two-tier memory system (personal per-character + general shared) with budget-based injection into LLM system prompt. Per-character TTS voice routing via state file — Wyoming TTS server reads active config to route between Kokoro (local) and ElevenLabs (cloud PCM 24kHz). Dashboard: memories page, conversation history, character profile on cards, auto-TTS engine selection from character config. Also includes VTube Studio expression bridge and ComfyUI API guide. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
57 lines
1.5 KiB
JavaScript
57 lines
1.5 KiB
JavaScript
import { useState, useRef, useCallback } from 'react'
|
|
import { synthesize } from '../lib/api'
|
|
|
|
export function useTtsPlayback(voice, engine = 'kokoro', model = null) {
|
|
const [isPlaying, setIsPlaying] = useState(false)
|
|
const audioCtxRef = useRef(null)
|
|
const sourceRef = useRef(null)
|
|
|
|
const getAudioContext = () => {
|
|
if (!audioCtxRef.current || audioCtxRef.current.state === 'closed') {
|
|
audioCtxRef.current = new AudioContext()
|
|
}
|
|
return audioCtxRef.current
|
|
}
|
|
|
|
const speak = useCallback(async (text) => {
|
|
if (!text) return
|
|
|
|
// Stop any current playback
|
|
if (sourceRef.current) {
|
|
try { sourceRef.current.stop() } catch {}
|
|
}
|
|
|
|
setIsPlaying(true)
|
|
try {
|
|
const audioData = await synthesize(text, voice, engine, model)
|
|
const ctx = getAudioContext()
|
|
if (ctx.state === 'suspended') await ctx.resume()
|
|
|
|
const audioBuffer = await ctx.decodeAudioData(audioData)
|
|
const source = ctx.createBufferSource()
|
|
source.buffer = audioBuffer
|
|
source.connect(ctx.destination)
|
|
sourceRef.current = source
|
|
|
|
source.onended = () => {
|
|
setIsPlaying(false)
|
|
sourceRef.current = null
|
|
}
|
|
source.start()
|
|
} catch (err) {
|
|
console.error('TTS playback error:', err)
|
|
setIsPlaying(false)
|
|
}
|
|
}, [voice, engine, model])
|
|
|
|
const stop = useCallback(() => {
|
|
if (sourceRef.current) {
|
|
try { sourceRef.current.stop() } catch {}
|
|
sourceRef.current = null
|
|
}
|
|
setIsPlaying(false)
|
|
}, [])
|
|
|
|
return { isPlaying, speak, stop }
|
|
}
|