/** * Host-only custom ASR / TTS module. * * Ship as `dist/media-providers/index.js` next to the sandboxed script entry. * The host `require`s this file outside the script VM (full Node: `ws`, `process`, …). * * Do **not** re-export `mediaProviders` from `dist/index.js` — the sandbox has no * `process`, so loading `ws` there fails. Keep `defineScript` in a separate file * (see `custom-media-providers-script.ts`). * * Batch and streaming TTS share one class (`supportsStreaming()` + streaming * lifecycle). Do not register a separate streaming map. */ import { Readable } from 'stream'; import { Subject } from 'rxjs'; import { defineMediaProviders, type MediaConnectorContext, type MediaProviderShared, type ScriptAsrConnector, type ScriptTtsConnector, type ScriptTtsSynthesisContext, } from '@voctiv/agent-sdk'; type SharedSession = MediaProviderShared & { turns: number; }; class EchoAsr implements ScriptAsrConnector { readonly transcription$ = new Subject<{ text: string; isFinal: boolean }>(); readonly result$ = new Subject(); readonly error$ = new Subject<{ message: string; code?: number }>(); constructor( private readonly ctx: MediaConnectorContext, private readonly shared: SharedSession | undefined, ) {} isOpen(): boolean { return true; } send(_audio: ArrayBufferLike): void { // Forward PCM (S16LE mono 16 kHz) to your vendor here. void this.ctx; } speech(_active: boolean): void {} finalize(): void { const text = 'heard'; this.transcription$.next({ text, isFinal: true }); this.result$.next(text); if (this.shared) this.shared.turns += 1; } close(): void { this.transcription$.complete(); this.result$.complete(); this.error$.complete(); } } class BatchOrStreamTts implements ScriptTtsConnector { readonly audio$ = new Subject(); readonly done$ = new Subject(); constructor( private readonly ctx: MediaConnectorContext, private readonly shared: SharedSession | undefined, ) {} /** Return true when this connector also owns a long-lived streaming socket. */ supportsStreaming(): boolean { return false; } async textToSpeechStream( rawtext: string, _ctx?: ScriptTtsSynthesisContext, ): Promise { void this.ctx; void this.shared; // Replace with HTTP keep-alive synthesis that returns a PCM stream. return Readable.from([Buffer.from(rawtext, 'utf8')]); } async open(): Promise {} async startGeneration(): Promise {} sendText(_chunk: string): void {} flush(): void {} close(): void { // Soft-close streaming sockets for reuse; do not complete Subjects if the // host may call open() again on the same fingerprint. } } export const mediaProviders = defineMediaProviders({ createShared: (): SharedSession => ({ turns: 0, dispose() { /* release vendor app state for this dialog */ }, }), asr: { 'my-asr': (ctx, shared) => new EchoAsr(ctx, shared as SharedSession), }, tts: { 'my-tts': (ctx, shared) => new BatchOrStreamTts(ctx, shared as SharedSession), }, });