mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-06 06:41:56 +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>
95 lines
3.7 KiB
TypeScript
95 lines
3.7 KiB
TypeScript
import { describe, expect, test } from 'vitest'
|
|
import { DEFAULT_VOICE_PERSONALITY } from '@hapi/protocol/voice-personality'
|
|
import {
|
|
buildElevenLabsSessionOverridesFromPrefs,
|
|
capElevenLabsInitialContext
|
|
} from './voicePersonalitySession'
|
|
|
|
describe('buildElevenLabsSessionOverridesFromPrefs', () => {
|
|
test('empty prefs + language produces byte-parity with upstream baseline', () => {
|
|
const overrides = buildElevenLabsSessionOverridesFromPrefs(
|
|
structuredClone(DEFAULT_VOICE_PERSONALITY),
|
|
{ language: 'en' }
|
|
)
|
|
expect(overrides).toEqual({ agent: { language: 'en' } })
|
|
})
|
|
|
|
test('empty prefs + no language produces empty overrides (no unauthorized fields)', () => {
|
|
const overrides = buildElevenLabsSessionOverridesFromPrefs(
|
|
structuredClone(DEFAULT_VOICE_PERSONALITY),
|
|
{}
|
|
)
|
|
expect(overrides).toEqual({})
|
|
})
|
|
|
|
test('custom voice id with default sliders emits tts.voice_id ONLY', () => {
|
|
const overrides = buildElevenLabsSessionOverridesFromPrefs(
|
|
structuredClone(DEFAULT_VOICE_PERSONALITY),
|
|
{ language: 'en', voiceId: 'voice_abc' }
|
|
)
|
|
expect(overrides.agent).toEqual({ language: 'en' })
|
|
expect(overrides.tts).toEqual({ voice_id: 'voice_abc' })
|
|
})
|
|
|
|
test('custom voice id with non-balanced preset emits sliders + voice_id', () => {
|
|
const overrides = buildElevenLabsSessionOverridesFromPrefs(
|
|
{ ...DEFAULT_VOICE_PERSONALITY, preset: 'warm' },
|
|
{ language: 'en', voiceId: 'voice_abc' }
|
|
)
|
|
expect(overrides.tts?.voice_id).toBe('voice_abc')
|
|
expect(overrides.tts?.stability).toBeDefined()
|
|
})
|
|
|
|
test('custom character emits composed prompt with client tools', () => {
|
|
const overrides = buildElevenLabsSessionOverridesFromPrefs(
|
|
{
|
|
...DEFAULT_VOICE_PERSONALITY,
|
|
character: 'You are a pirate. Arr.'
|
|
},
|
|
{ language: 'en' }
|
|
)
|
|
expect(overrides.agent?.language).toBe('en')
|
|
expect(overrides.agent?.prompt?.prompt).toContain('You are a pirate')
|
|
expect(overrides.agent?.prompt?.prompt).toContain('# CRITICAL RULE')
|
|
expect(overrides.agent?.prompt?.tools?.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
test('non-balanced preset emits tts slider override', () => {
|
|
const overrides = buildElevenLabsSessionOverridesFromPrefs(
|
|
{ ...DEFAULT_VOICE_PERSONALITY, preset: 'warm' },
|
|
{ language: 'en' }
|
|
)
|
|
expect(overrides.tts?.style).toBeGreaterThan(DEFAULT_VOICE_PERSONALITY.elevenLabs.style)
|
|
})
|
|
|
|
test('balanced preset does not emit tts override', () => {
|
|
const overrides = buildElevenLabsSessionOverridesFromPrefs(
|
|
{ ...DEFAULT_VOICE_PERSONALITY, preset: 'balanced' },
|
|
{ language: 'en' }
|
|
)
|
|
expect(overrides.tts).toBeUndefined()
|
|
})
|
|
|
|
test('caps initial context for WebRTC transport', () => {
|
|
const huge = 'x'.repeat(100_000)
|
|
const capped = capElevenLabsInitialContext(huge)
|
|
expect(new TextEncoder().encode(capped).length).toBeLessThanOrEqual(4_500)
|
|
expect(capped).toContain('truncated')
|
|
})
|
|
|
|
test('sends use_speaker_boost in tts override when set', () => {
|
|
const overrides = buildElevenLabsSessionOverridesFromPrefs(
|
|
{
|
|
...DEFAULT_VOICE_PERSONALITY,
|
|
preset: 'warm',
|
|
elevenLabs: {
|
|
...DEFAULT_VOICE_PERSONALITY.elevenLabs,
|
|
use_speaker_boost: true
|
|
}
|
|
},
|
|
{ language: 'en' }
|
|
)
|
|
expect(overrides.tts).toHaveProperty('use_speaker_boost', true)
|
|
})
|
|
})
|