| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export type CallLogEvent = |
| | { e: 'lifecycle'; phase: 'open' | 'ready' | 'greet' | 'hangup' | 'close'; sid?: string } |
| | { e: 'tts'; action: 'start' | 'end' | 'dedupe'; source: 'overlay' | 'app-level'; textLen: number } |
| | { e: 'vad'; action: 'speech_start' | 'speech_end' | 'suppressed'; level?: number; state: string } |
| | { e: 'turn'; action: 'user_out' | 'assistant_in' | 'barge_in'; route: 'ws' | 'chat_rest'; latencyMs?: number } |
| | { e: 'state'; from: string; to: string; trigger: string } |
| | { e: 'error'; where: string; message: string } |
|
|
| declare global { |
| interface Window { |
| __hp_tts_owner__?: 'overlay' | 'app-level' | null |
| } |
| } |
|
|
| export function clog(evt: CallLogEvent): void { |
| if (typeof performance === 'undefined') return |
| const ts = Math.floor(performance.now()) |
| |
| |
| |
| console.info(`[Call+${ts}]`, evt) |
| } |
|
|
| |
| |
| |
| |
| export function isCallFullDuplexEnabled(): boolean { |
| if (typeof window === 'undefined') return true |
| try { |
| const override = window.localStorage.getItem('homepilot_call_full_duplex') |
| if (override === 'true') return true |
| if (override === 'false') return false |
| } catch { |
| |
| } |
| const envVal = (import.meta as unknown as { |
| env?: Record<string, string | undefined> |
| }).env?.VITE_CALL_FULL_DUPLEX_ENABLED |
| return String(envVal ?? 'true') !== 'false' |
| } |
|
|
| export interface SpeakOwnedHooks { |
| onStart?: () => void |
| onEnd?: () => void |
| onError?: (message: string) => void |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function speakOwned( |
| source: 'overlay' | 'app-level', |
| text: string, |
| hooks?: SpeakOwnedHooks, |
| ): boolean { |
| if (typeof window === 'undefined') return false |
| const t = (text ?? '').trim() |
| if (!t) return false |
| const owner = window.__hp_tts_owner__ |
| if (owner && owner !== source) { |
| clog({ e: 'tts', action: 'dedupe', source, textLen: t.length }) |
| return false |
| } |
| window.__hp_tts_owner__ = source |
| try { |
| hooks?.onStart?.() |
| clog({ e: 'tts', action: 'start', source, textLen: t.length }) |
| const svc = (window as unknown as { |
| SpeechService?: { speak?: (text: string) => void } |
| }).SpeechService |
| svc?.speak?.(t) |
| |
| |
| |
| |
| |
| const estimateMs = Math.max(800, Math.min(10_000, t.length * 80)) |
| window.setTimeout(() => { |
| if (window.__hp_tts_owner__ === source) { |
| window.__hp_tts_owner__ = null |
| } |
| hooks?.onEnd?.() |
| clog({ e: 'tts', action: 'end', source, textLen: t.length }) |
| }, estimateMs) |
| return true |
| } catch (err) { |
| if (window.__hp_tts_owner__ === source) { |
| window.__hp_tts_owner__ = null |
| } |
| const message = err instanceof Error ? err.message : String(err) |
| hooks?.onError?.(message) |
| clog({ e: 'error', where: 'speakOwned', message }) |
| return false |
| } |
| } |
|
|