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>
82 lines
3.3 KiB
TypeScript
82 lines
3.3 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
import { VOICE_SYSTEM_PROMPT } from './voice'
|
|
import {
|
|
ELEVENLABS_WEBRTC_CONTEXT_MAX_BYTES,
|
|
getDefaultVoiceSystemPrompt,
|
|
getVoicePersonalityPreset,
|
|
isDefaultVoicePersonality,
|
|
parseVoicePersonalityPreferences,
|
|
resolveComposedVoiceSystemPrompt,
|
|
resolveElevenLabsVoiceSettings,
|
|
truncateUtf8ByteLength,
|
|
utf8ByteLength
|
|
} from './voicePersonality'
|
|
import { VOICE_PLATFORM_FIXTURES } from './voicePromptLayers'
|
|
|
|
describe('voicePersonality', () => {
|
|
test('parseVoicePersonalityPreferences returns defaults for invalid input', () => {
|
|
const prefs = parseVoicePersonalityPreferences(null)
|
|
expect(prefs.preset).toBe('balanced')
|
|
expect(prefs.identity).toBe('')
|
|
expect(prefs.character).toBe('')
|
|
})
|
|
|
|
test('migrates legacy customPrompt into character', () => {
|
|
const prefs = parseVoicePersonalityPreferences({
|
|
customPrompt: 'Call me G.'
|
|
})
|
|
expect(prefs.character).toBe('Call me G.')
|
|
})
|
|
|
|
test('migrates non-monolith systemPrompt into character', () => {
|
|
const prefs = parseVoicePersonalityPreferences({
|
|
systemPrompt: 'You are a pirate. Arr.'
|
|
})
|
|
expect(prefs.character).toBe('You are a pirate. Arr.')
|
|
expect(prefs.systemPrompt).toBe('')
|
|
})
|
|
|
|
test('composed prompt always includes platform fixtures', () => {
|
|
const prefs = parseVoicePersonalityPreferences({
|
|
character: 'You are a pirate. Arr.'
|
|
})
|
|
const { prompt } = resolveComposedVoiceSystemPrompt(prefs)
|
|
expect(prompt).toContain('messageCodingAgent')
|
|
expect(prompt).toContain(VOICE_PLATFORM_FIXTURES.slice(0, 40))
|
|
expect(prompt).toContain('You are a pirate')
|
|
expect(prompt).toContain('Never refer to yourself as Gemini')
|
|
})
|
|
|
|
test('resolveComposedVoiceSystemPrompt does not embed session context', () => {
|
|
const prefs = parseVoicePersonalityPreferences({ character: 'Base.' })
|
|
const { prompt } = resolveComposedVoiceSystemPrompt(prefs)
|
|
expect(prompt).not.toContain('[Current Context]')
|
|
expect(prompt).not.toContain('Working on auth.')
|
|
})
|
|
|
|
test('getDefaultVoiceSystemPrompt matches bundled VOICE_SYSTEM_PROMPT', () => {
|
|
const prefs = parseVoicePersonalityPreferences({})
|
|
expect(getDefaultVoiceSystemPrompt()).toBe(VOICE_SYSTEM_PROMPT)
|
|
expect(isDefaultVoicePersonality(prefs)).toBe(true)
|
|
})
|
|
|
|
test('resolveElevenLabsVoiceSettings uses preset sliders unless custom', () => {
|
|
const prefs = parseVoicePersonalityPreferences({
|
|
preset: 'custom',
|
|
elevenLabs: { stability: 0.42, similarity_boost: 0.8, style: 0.2, speed: 1.05, use_speaker_boost: true }
|
|
})
|
|
expect(resolveElevenLabsVoiceSettings(prefs).stability).toBe(0.42)
|
|
})
|
|
|
|
test('defines all preset ids', () => {
|
|
expect(getVoicePersonalityPreset('calm').elevenLabs.speed).toBeLessThan(1)
|
|
})
|
|
|
|
test('truncateUtf8ByteLength respects byte budget', () => {
|
|
const text = truncateUtf8ByteLength('hello 🎙️ world', ELEVENLABS_WEBRTC_CONTEXT_MAX_BYTES)
|
|
expect(text).toBe('hello 🎙️ world')
|
|
const huge = truncateUtf8ByteLength('a'.repeat(50_000), 100)
|
|
expect(utf8ByteLength(huge)).toBeLessThanOrEqual(100)
|
|
})
|
|
})
|