import type { CoreConfig } from "./core-bridge.js"; import type { VoiceCallTtsConfig } from "./config.js"; import { convertPcmToMulaw8k } from "./telephony-audio.js"; export type TelephonyTtsRuntime = { textToSpeechTelephony: (params: { text: string; cfg: CoreConfig; prefsPath?: string; }) => Promise<{ success: boolean; audioBuffer?: Buffer; sampleRate?: number; provider?: string; error?: string; }>; }; export type TelephonyTtsProvider = { synthesizeForTelephony: (text: string) => Promise; }; export function createTelephonyTtsProvider(params: { coreConfig: CoreConfig; ttsOverride?: VoiceCallTtsConfig; runtime: TelephonyTtsRuntime; }): TelephonyTtsProvider { const { coreConfig, ttsOverride, runtime } = params; const mergedConfig = applyTtsOverride(coreConfig, ttsOverride); return { synthesizeForTelephony: async (text: string) => { const result = await runtime.textToSpeechTelephony({ text, cfg: mergedConfig, }); if (!result.success || !result.audioBuffer || !result.sampleRate) { throw new Error(result.error ?? "TTS conversion failed"); } return convertPcmToMulaw8k(result.audioBuffer, result.sampleRate); }, }; } function applyTtsOverride( coreConfig: CoreConfig, override?: VoiceCallTtsConfig, ): CoreConfig { if (!override) return coreConfig; const base = coreConfig.messages?.tts; const merged = mergeTtsConfig(base, override); if (!merged) return coreConfig; return { ...coreConfig, messages: { ...(coreConfig.messages ?? {}), tts: merged, }, }; } function mergeTtsConfig( base?: VoiceCallTtsConfig, override?: VoiceCallTtsConfig, ): VoiceCallTtsConfig | undefined { if (!base && !override) return undefined; if (!override) return base; if (!base) return override; return deepMerge(base, override); } function deepMerge(base: T, override: T): T { if (!isPlainObject(base) || !isPlainObject(override)) { return override; } const result: Record = { ...base }; for (const [key, value] of Object.entries(override)) { if (value === undefined) continue; const existing = (base as Record)[key]; if (isPlainObject(existing) && isPlainObject(value)) { result[key] = deepMerge(existing, value); } else { result[key] = value; } } return result as T; } function isPlainObject(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }