mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
* feat(voice): voice personality, picker catalog, and prompt layer foundation - voicePickerCatalog.ts: per-backend voice lists for Gemini and Qwen with resolve helpers (resolveGeminiLiveVoice, resolveQwenRealtimeVoice) - voicePersonality.ts: VoicePersonalityPreferences schema, presets, composed system prompt with identity/character/response-length layers - voicePromptLayers.ts: buildResolvedVoiceSystemPrompt, preset delivery snippets - voiceSystemPromptParam.ts: hub-side base64url decode for ?systemPrompt= - voicePickerPreferences.ts, voicePersonalitySession.ts: browser-side encode, decode, and storage helpers - useVoicePersonality: React hook for preferences persistence via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(voice): preset delivery included when non-balanced preset selected; restore test typecheck - isDefaultVoicePersonality: add preset check so warm/calm/direct presets trigger the delivery snippet instead of being treated as default - web/tsconfig.json: remove test file exclusion from typecheck (restoring strict coverage of test code); fix resulting type error in mock declaration via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(voice): include use_speaker_boost in ElevenLabs TTS override payload The checkbox persisted the pref but ttsDiffersFromDefault and buildElevenLabsTtsOverride both omitted it, so the setting was never sent to the agent. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * test(voice): update speaker_boost test to assert it IS included in override The previous test asserted use_speaker_boost was omitted; now it's correctly included in the TTS payload. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(voice): authorize use_speaker_boost in ElevenLabs override schema Add use_speaker_boost to both the VoiceAgentConfig tts override type and the buildVoiceAgentConfig() platform_settings so the field is accepted by the ElevenLabs agent runtime. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(voice): propagate full language code through composed prompt, not just zh getDefaultVoiceSystemPrompt and resolveComposedVoiceSystemPrompt were filtering language to zh-only before passing to composeVoiceAgentPrompt. Now append buildVoiceLanguageBlock(language) after composition so French, Spanish, Japanese etc. reach Gemini/Qwen sessions correctly. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(voice): only append language block when language explicitly set Building language block unconditionally when no language is given caused getDefaultVoiceSystemPrompt() to diverge from VOICE_SYSTEM_PROMPT. Only append the block when a code is explicitly provided. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(voice): always include language block for Gemini/Qwen in composed prompt When auto-detect is on (language=undefined), the composed prompt sent via hub proxy was losing the language auto-detect instruction because the block was only added when language was explicitly set. Now: ElevenLabs skips the block (has its own language field); Gemini/Qwen always include it — undefined produces the auto-detect block, an explicit code produces the appropriate language instruction. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: HAPI <noreply@hapi.run>
132 lines
4.0 KiB
TypeScript
132 lines
4.0 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react'
|
|
import {
|
|
DEFAULT_VOICE_PERSONALITY,
|
|
VOICE_CHARACTER_MAX_LENGTH,
|
|
VOICE_IDENTITY_MAX_LENGTH,
|
|
VOICE_PERSONALITY_STORAGE_KEY,
|
|
getPresetDeliverySnippet,
|
|
getVoicePersonalityPreset,
|
|
parseVoicePersonalityPreferences,
|
|
type ElevenLabsVoiceSettings,
|
|
type VoicePersonalityPreferences,
|
|
type VoicePersonalityPresetId,
|
|
type ResponseLengthOption
|
|
} from '@hapi/protocol/voice-personality'
|
|
|
|
function readStoredVoicePersonality(): VoicePersonalityPreferences {
|
|
try {
|
|
const raw = localStorage.getItem(VOICE_PERSONALITY_STORAGE_KEY)
|
|
if (!raw) return structuredClone(DEFAULT_VOICE_PERSONALITY)
|
|
return parseVoicePersonalityPreferences(JSON.parse(raw))
|
|
} catch {
|
|
return structuredClone(DEFAULT_VOICE_PERSONALITY)
|
|
}
|
|
}
|
|
|
|
function writeStoredVoicePersonality(prefs: VoicePersonalityPreferences): void {
|
|
localStorage.setItem(VOICE_PERSONALITY_STORAGE_KEY, JSON.stringify(prefs))
|
|
}
|
|
|
|
export function useVoicePersonality() {
|
|
const [prefs, setPrefs] = useState<VoicePersonalityPreferences>(readStoredVoicePersonality)
|
|
|
|
useEffect(() => {
|
|
writeStoredVoicePersonality(prefs)
|
|
}, [prefs])
|
|
|
|
const setPreset = useCallback((preset: VoicePersonalityPresetId) => {
|
|
setPrefs((prev) => {
|
|
const definition = getVoicePersonalityPreset(preset)
|
|
return {
|
|
...prev,
|
|
preset,
|
|
elevenLabs: preset === 'custom' ? prev.elevenLabs : { ...definition.elevenLabs }
|
|
}
|
|
})
|
|
}, [])
|
|
|
|
const setIdentity = useCallback((identity: string) => {
|
|
setPrefs((prev) => ({
|
|
...prev,
|
|
identity: identity.slice(0, VOICE_IDENTITY_MAX_LENGTH),
|
|
systemPrompt: ''
|
|
}))
|
|
}, [])
|
|
|
|
const setCharacter = useCallback((character: string) => {
|
|
setPrefs((prev) => ({
|
|
...prev,
|
|
character: character.slice(0, VOICE_CHARACTER_MAX_LENGTH),
|
|
systemPrompt: ''
|
|
}))
|
|
}, [])
|
|
|
|
const resetIdentity = useCallback(() => {
|
|
setPrefs((prev) => ({ ...prev, identity: '' }))
|
|
}, [])
|
|
|
|
const resetCharacter = useCallback(() => {
|
|
setPrefs((prev) => ({ ...prev, character: '' }))
|
|
}, [])
|
|
|
|
const resetVoicePersonalityLayers = useCallback(() => {
|
|
setPrefs((prev) => ({
|
|
...prev,
|
|
identity: '',
|
|
character: '',
|
|
systemPrompt: ''
|
|
}))
|
|
}, [])
|
|
|
|
const appendPresetDeliveryToCharacter = useCallback(() => {
|
|
setPrefs((prev) => {
|
|
const snippet = getPresetDeliverySnippet(prev.preset).trim()
|
|
if (!snippet) return prev
|
|
const base = prev.character.trim()
|
|
const merged = base.includes(snippet) ? base : (base ? `${base}\n\n${snippet}` : snippet)
|
|
return {
|
|
...prev,
|
|
character: merged.slice(0, VOICE_CHARACTER_MAX_LENGTH),
|
|
systemPrompt: ''
|
|
}
|
|
})
|
|
}, [])
|
|
|
|
const setElevenLabs = useCallback((patch: Partial<ElevenLabsVoiceSettings>) => {
|
|
setPrefs((prev) => ({
|
|
...prev,
|
|
preset: 'custom',
|
|
elevenLabs: { ...prev.elevenLabs, ...patch }
|
|
}))
|
|
}, [])
|
|
|
|
const setGeminiAffectiveDialog = useCallback((affective_dialog: boolean) => {
|
|
setPrefs((prev) => ({
|
|
...prev,
|
|
gemini: { ...prev.gemini, affective_dialog }
|
|
}))
|
|
}, [])
|
|
|
|
const setResponseLength = useCallback((responseLength: ResponseLengthOption) => {
|
|
setPrefs((prev) => ({ ...prev, responseLength }))
|
|
}, [])
|
|
|
|
return {
|
|
prefs,
|
|
setPreset,
|
|
setIdentity,
|
|
setCharacter,
|
|
resetIdentity,
|
|
resetCharacter,
|
|
resetVoicePersonalityLayers,
|
|
appendPresetDeliveryToCharacter,
|
|
setElevenLabs,
|
|
setGeminiAffectiveDialog,
|
|
setResponseLength
|
|
}
|
|
}
|
|
|
|
export function loadVoicePersonalityFromStorage(): VoicePersonalityPreferences {
|
|
return readStoredVoicePersonality()
|
|
}
|