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>
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
/**
|
|
* Static voice catalogs for Settings picker (Gemini Live, Qwen Realtime).
|
|
* ElevenLabs voices remain dynamic via GET /api/voice/voices (#690).
|
|
*
|
|
* @see https://github.com/tiann/hapi/issues/742
|
|
*/
|
|
|
|
export type VoicePickerOption = {
|
|
id: string
|
|
label: string
|
|
description?: string
|
|
}
|
|
|
|
/** Prebuilt voices documented for Gemini Live BidiGenerateContent. */
|
|
export const GEMINI_LIVE_VOICE_OPTIONS: readonly VoicePickerOption[] = [
|
|
{ id: 'Puck', label: 'Puck', description: 'Conversational, friendly' },
|
|
{ id: 'Charon', label: 'Charon', description: 'Deep, authoritative' },
|
|
{ id: 'Kore', label: 'Kore', description: 'Neutral, professional' },
|
|
{ id: 'Fenrir', label: 'Fenrir', description: 'Warm, approachable' },
|
|
{ id: 'Aoede', label: 'Aoede', description: 'Default' }
|
|
] as const
|
|
|
|
/** English-accessible Qwen Realtime voices (expand after DashScope verification). */
|
|
export const QWEN_REALTIME_VOICE_OPTIONS: readonly VoicePickerOption[] = [
|
|
{ id: 'Tina', label: 'Tina', description: 'Default' },
|
|
{ id: 'Cherry', label: 'Cherry' },
|
|
{ id: 'Mia', label: 'Mia' },
|
|
{ id: 'Chelsie', label: 'Chelsie' },
|
|
{ id: 'Serena', label: 'Serena' },
|
|
{ id: 'Ethan', label: 'Ethan' }
|
|
] as const
|
|
|
|
export const VOICE_PICKER_STORAGE_KEYS = {
|
|
elevenlabs: 'hapi-voice-elevenlabs',
|
|
'gemini-live': 'hapi-voice-gemini',
|
|
'qwen-realtime': 'hapi-voice-qwen'
|
|
} as const
|
|
|
|
/** Legacy ElevenLabs key from #690 — read for migration. */
|
|
export const LEGACY_ELEVENLABS_VOICE_STORAGE_KEY = 'hapi-voice-id'
|
|
|
|
/** User-selected voice backend when hub has more than one configured. */
|
|
export const VOICE_BACKEND_PREFERENCE_STORAGE_KEY = 'hapi-voice-backend'
|
|
|
|
export const VOICE_BACKEND_LABELS = {
|
|
elevenlabs: 'ElevenLabs',
|
|
'gemini-live': 'Gemini Live',
|
|
'qwen-realtime': 'Qwen Realtime'
|
|
} as const
|
|
|
|
const geminiVoiceIds = new Set(GEMINI_LIVE_VOICE_OPTIONS.map((v) => v.id))
|
|
const qwenVoiceIds = new Set(QWEN_REALTIME_VOICE_OPTIONS.map((v) => v.id))
|
|
|
|
/** Valid Gemini Live prebuilt voice name, or default (Aoede). */
|
|
export function resolveGeminiLiveVoice(voiceName?: string | null): string {
|
|
if (voiceName && geminiVoiceIds.has(voiceName)) {
|
|
return voiceName
|
|
}
|
|
return GEMINI_LIVE_VOICE_OPTIONS.find((v) => v.id === 'Aoede')?.id ?? GEMINI_LIVE_VOICE_OPTIONS[0].id
|
|
}
|
|
|
|
/** Valid Qwen Realtime voice id, or hub default (Tina — matches QWEN_REALTIME_VOICE on qwen3.5-omni-flash-realtime). */
|
|
export function resolveQwenRealtimeVoice(voiceName?: string | null): string {
|
|
if (voiceName && qwenVoiceIds.has(voiceName)) {
|
|
return voiceName
|
|
}
|
|
return QWEN_REALTIME_VOICE_OPTIONS.find((v) => v.id === 'Tina')?.id ?? QWEN_REALTIME_VOICE_OPTIONS[0].id
|
|
}
|