dvgateway-sdk 1.6.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +204 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +236 -7
- package/dist/client.js.map +1 -1
- package/dist/flow/builder.d.ts +102 -0
- package/dist/flow/builder.d.ts.map +1 -0
- package/dist/flow/builder.js +341 -0
- package/dist/flow/builder.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/play-audio.test.js +23 -1
- package/dist/play-audio.test.js.map +1 -1
- package/dist/streams/audio-playback.test.d.ts +8 -0
- package/dist/streams/audio-playback.test.d.ts.map +1 -0
- package/dist/streams/audio-playback.test.js +114 -0
- package/dist/streams/audio-playback.test.js.map +1 -0
- package/dist/streams/call-events.d.ts.map +1 -1
- package/dist/streams/call-events.js +51 -0
- package/dist/streams/call-events.js.map +1 -1
- package/dist/streams/tts-playback.test.d.ts +10 -0
- package/dist/streams/tts-playback.test.d.ts.map +1 -0
- package/dist/streams/tts-playback.test.js +103 -0
- package/dist/streams/tts-playback.test.js.map +1 -0
- package/dist/streams/tts-stream.d.ts +17 -4
- package/dist/streams/tts-stream.d.ts.map +1 -1
- package/dist/streams/tts-stream.js +34 -10
- package/dist/streams/tts-stream.js.map +1 -1
- package/dist/transport/http-client.d.ts +2 -2
- package/dist/transport/http-client.d.ts.map +1 -1
- package/dist/transport/http-client.js +4 -3
- package/dist/transport/http-client.js.map +1 -1
- package/dist/types/index.d.ts +175 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { type AudioStreamHandle, type PipelineType } from './streams/audio-stream.js';
|
|
23
23
|
import { PipelineBuilder } from './pipeline/builder.js';
|
|
24
|
+
import { VoiceFlowBuilder } from './flow/builder.js';
|
|
24
25
|
import { PipelineMetrics } from './observability/metrics.js';
|
|
25
26
|
import { type SimulateOptions, type SimulatedCall } from './simulator/simulator.js';
|
|
26
|
-
import type { DVGatewayOptions, CallSession, CallEvent, ChannelStateChangeEvent, TTSCompleteEvent, TranscriptResult, StreamDir, TtsAdapter, Unsubscribe, DTMFResult, PlayAudioResult, WarmTransferResult } from './types/index.js';
|
|
27
|
+
import type { DVGatewayOptions, CallSession, CallEvent, ChannelStateChangeEvent, AudioPlaybackEvent, TTSCompleteEvent, TtsPlaybackEvent, TranscriptResult, StreamDir, TtsAdapter, Unsubscribe, DTMFResult, InjectTtsResult, PlayAudioResult, WarmTransferResult } from './types/index.js';
|
|
27
28
|
export declare class DVGatewayClient {
|
|
28
29
|
private readonly auth;
|
|
29
30
|
private readonly wsPool;
|
|
@@ -131,6 +132,89 @@ export declare class DVGatewayClient {
|
|
|
131
132
|
dir?: StreamDir;
|
|
132
133
|
pipelineType?: PipelineType;
|
|
133
134
|
}): AudioStreamHandle;
|
|
135
|
+
/**
|
|
136
|
+
* Attach audio streaming (ExternalMedia) to a flow-mode call mid-call.
|
|
137
|
+
*
|
|
138
|
+
* Only valid for calls that entered the gateway with the dialplan arg
|
|
139
|
+
* `Stasis(dvgateway, flow=true, ...)` — those calls land in a holding
|
|
140
|
+
* bridge with no ExternalMedia until this method is called. Idempotent —
|
|
141
|
+
* if audio is already attached the gateway returns the existing channel.
|
|
142
|
+
*
|
|
143
|
+
* Use `dir`:
|
|
144
|
+
* - `'both'` (default) — full duplex, STT + TTS
|
|
145
|
+
* - `'out'` — gateway → caller only (TTS playback, no STT cost)
|
|
146
|
+
* - `'in'` — caller → gateway only (STT only)
|
|
147
|
+
*
|
|
148
|
+
* @returns The Asterisk ExternalMedia channel ID. Returned mainly for
|
|
149
|
+
* observability — most callers will ignore it.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* await gw.attachAudio(linkedId, 'out');
|
|
154
|
+
* await gw.say(linkedId, 'Welcome', tts);
|
|
155
|
+
* await gw.detachAudio(linkedId);
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* @see {@link flow} for the high-level VoiceFlow builder that handles
|
|
159
|
+
* attach/detach automatically per stage.
|
|
160
|
+
*/
|
|
161
|
+
attachAudio(linkedId: string, dir?: 'both' | 'in' | 'out'): Promise<string>;
|
|
162
|
+
/**
|
|
163
|
+
* Detach audio streaming from a flow-mode call. The original channel
|
|
164
|
+
* stays parked in its holding bridge so {@link attachAudio} can be
|
|
165
|
+
* called again later. Idempotent — no-op if audio is not attached.
|
|
166
|
+
*/
|
|
167
|
+
detachAudio(linkedId: string): Promise<void>;
|
|
168
|
+
/**
|
|
169
|
+
* Query audio attach state for a flow-mode call.
|
|
170
|
+
*
|
|
171
|
+
* `flowMode` is `false` when the call did NOT enter with `flow=true`
|
|
172
|
+
* (or has already ended) — those calls have audio attached at
|
|
173
|
+
* StasisStart and cannot be detached.
|
|
174
|
+
*/
|
|
175
|
+
getAudioStatus(linkedId: string): Promise<{
|
|
176
|
+
flowMode: boolean;
|
|
177
|
+
attached: boolean;
|
|
178
|
+
extMediaId: string;
|
|
179
|
+
bridgeId: string;
|
|
180
|
+
}>;
|
|
181
|
+
/**
|
|
182
|
+
* Create a fluent SDK voice flow — a stage graph that drives a call
|
|
183
|
+
* through IVR-like steps, attaching/detaching audio per stage.
|
|
184
|
+
*
|
|
185
|
+
* Only meaningful for calls that entered with dialplan arg
|
|
186
|
+
* `flow=true`. The flow runtime listens for `call:new` events and
|
|
187
|
+
* spawns a per-call execution that:
|
|
188
|
+
* 1. Enters the start stage (calls `attachAudio` if `audio !== 'none'`)
|
|
189
|
+
* 2. Runs the stage's `onEnter` (which can `say`, `playAudio`, `collectDtmf`)
|
|
190
|
+
* 3. Transitions on DTMF (`onDtmf` map) or explicit `ctx.transitionTo()`
|
|
191
|
+
* 4. Detaches audio between stages when modes differ
|
|
192
|
+
* 5. Ends on `call:ended` or when a stage calls `ctx.hangup()`
|
|
193
|
+
*
|
|
194
|
+
* @example IVR menu
|
|
195
|
+
* ```typescript
|
|
196
|
+
* gw.flow()
|
|
197
|
+
* .stage('greet', {
|
|
198
|
+
* audio: 'tts-only',
|
|
199
|
+
* onEnter: async (ctx) => ctx.say('1번 상담, 9번 종료', tts),
|
|
200
|
+
* onDtmf: { '1': 'chat', '9': 'bye' },
|
|
201
|
+
* })
|
|
202
|
+
* .stage('chat', {
|
|
203
|
+
* audio: 'full',
|
|
204
|
+
* onEnter: async (ctx) => ctx.runPipeline(myPipeline),
|
|
205
|
+
* })
|
|
206
|
+
* .stage('bye', {
|
|
207
|
+
* audio: 'tts-only',
|
|
208
|
+
* onEnter: async (ctx) => {
|
|
209
|
+
* await ctx.say('감사합니다', tts);
|
|
210
|
+
* await ctx.hangup();
|
|
211
|
+
* },
|
|
212
|
+
* })
|
|
213
|
+
* .startStage('greet')
|
|
214
|
+
* .start();
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
flow(): VoiceFlowBuilder;
|
|
134
218
|
/**
|
|
135
219
|
* Notify the gateway that AI processing has started (STT → LLM → TTS).
|
|
136
220
|
* If comfort noise is enabled on the gateway (GW_COMFORT_NOISE_ENABLED=true),
|
|
@@ -180,17 +264,34 @@ export declare class DVGatewayClient {
|
|
|
180
264
|
* Inject synthesized speech into an active call.
|
|
181
265
|
* audioChunks should be 16kHz, 16-bit PCM (slin16) audio.
|
|
182
266
|
*
|
|
267
|
+
* Returns an {@link InjectTtsResult} carrying the `injectId` (UUID) the
|
|
268
|
+
* gateway will surface on the corresponding `tts:playback` events. SDK
|
|
269
|
+
* 1.6.1+ generates the UUID client-side so callers can correlate events
|
|
270
|
+
* to *this* inject without an extra round-trip.
|
|
271
|
+
*
|
|
183
272
|
* @example
|
|
184
273
|
* ```typescript
|
|
185
|
-
* const
|
|
186
|
-
*
|
|
274
|
+
* const r = await gw.injectTts(linkedId, ttsAdapter.synthesize('Hello!'));
|
|
275
|
+
* gw.onTtsPlayback((ev) => {
|
|
276
|
+
* if (ev.injectId === r.injectId && ev.phase === 'complete') {
|
|
277
|
+
* console.log('real RTP playback complete');
|
|
278
|
+
* }
|
|
279
|
+
* });
|
|
187
280
|
* ```
|
|
281
|
+
*
|
|
282
|
+
* @param injectId - Optional caller-supplied UUID; defaults to a fresh
|
|
283
|
+
* one. Override only for tests / multi-process correlation.
|
|
188
284
|
*/
|
|
189
|
-
injectTts(linkedId: string, audioChunks: AsyncIterable<Buffer
|
|
285
|
+
injectTts(linkedId: string, audioChunks: AsyncIterable<Buffer>, injectId?: string): Promise<InjectTtsResult>;
|
|
190
286
|
/**
|
|
191
287
|
* Broadcast synthesized speech to all conference participants.
|
|
288
|
+
*
|
|
289
|
+
* Returns {@link InjectTtsResult} for parity with {@link injectTts}.
|
|
290
|
+
* Note: gateway 1.3.9.3 does not yet emit `tts:playback` for conference
|
|
291
|
+
* participant Players (only 1:1 calls), so the ID is forward-compat
|
|
292
|
+
* plumbing today.
|
|
192
293
|
*/
|
|
193
|
-
broadcastTts(confId: string, audioChunks: AsyncIterable<Buffer
|
|
294
|
+
broadcastTts(confId: string, audioChunks: AsyncIterable<Buffer>, injectId?: string): Promise<InjectTtsResult>;
|
|
194
295
|
/**
|
|
195
296
|
* Convenience: synthesize text and inject it directly.
|
|
196
297
|
* Requires a TtsAdapter instance.
|
|
@@ -210,7 +311,7 @@ export declare class DVGatewayClient {
|
|
|
210
311
|
* await gw.say(linkedId, '안녕하세요', tts); // cache hit — no API call
|
|
211
312
|
* ```
|
|
212
313
|
*/
|
|
213
|
-
say(linkedId: string, text: string, tts: TtsAdapter): Promise<
|
|
314
|
+
say(linkedId: string, text: string, tts: TtsAdapter): Promise<InjectTtsResult>;
|
|
214
315
|
/**
|
|
215
316
|
* Broadcast synthesized speech to a conference.
|
|
216
317
|
* Pass a CachedTtsAdapter for zero-cost repeated broadcasts.
|
|
@@ -220,7 +321,7 @@ export declare class DVGatewayClient {
|
|
|
220
321
|
* await gw.broadcastSay(confId, '회의를 시작합니다.', cachedTts);
|
|
221
322
|
* ```
|
|
222
323
|
*/
|
|
223
|
-
broadcastSay(confId: string, text: string, tts: TtsAdapter): Promise<
|
|
324
|
+
broadcastSay(confId: string, text: string, tts: TtsAdapter): Promise<InjectTtsResult>;
|
|
224
325
|
/**
|
|
225
326
|
* Mute the gateway's STT audio pipeline for a call.
|
|
226
327
|
*
|
|
@@ -637,6 +738,102 @@ export declare class DVGatewayClient {
|
|
|
637
738
|
* @since SDK 1.5.3
|
|
638
739
|
*/
|
|
639
740
|
onChannelState(handler: (event: ChannelStateChangeEvent) => void | Promise<void>): Unsubscribe;
|
|
741
|
+
/**
|
|
742
|
+
* Subscribe to `audio:playback` events — the canonical signal for
|
|
743
|
+
* {@link playAudio} lifecycle (start / complete / canceled / failed).
|
|
744
|
+
*
|
|
745
|
+
* **Primary use case — DTMF prompt audio with accurate timeout extension:**
|
|
746
|
+
* application code that plays an instructional prompt and then waits for
|
|
747
|
+
* DTMF input previously had to estimate prompt length with a hard-coded
|
|
748
|
+
* fallback (e.g. `baseTimeoutMs + 8000`). With this event the application
|
|
749
|
+
* can extend its timeout by the *actual* playback duration, dropping both
|
|
750
|
+
* unnecessary slack and the risk of the timer firing before the prompt
|
|
751
|
+
* finishes.
|
|
752
|
+
*
|
|
753
|
+
* @example
|
|
754
|
+
* ```typescript
|
|
755
|
+
* const result = await gw.playAudio({
|
|
756
|
+
* linkedId,
|
|
757
|
+
* url: promptUrl,
|
|
758
|
+
* waitForCompletion: false,
|
|
759
|
+
* });
|
|
760
|
+
* const playbackId = result.playbackId;
|
|
761
|
+
*
|
|
762
|
+
* let actualMs = 0;
|
|
763
|
+
* const audioDone = new Promise<void>((resolve) => {
|
|
764
|
+
* const unsub = gw.onAudioPlayback((ev) => {
|
|
765
|
+
* if (ev.playbackId !== playbackId) return;
|
|
766
|
+
* if (ev.phase === 'complete' || ev.phase === 'canceled' || ev.phase === 'failed') {
|
|
767
|
+
* actualMs = ev.durationMs ?? 0;
|
|
768
|
+
* unsub();
|
|
769
|
+
* resolve();
|
|
770
|
+
* }
|
|
771
|
+
* });
|
|
772
|
+
* });
|
|
773
|
+
*
|
|
774
|
+
* try {
|
|
775
|
+
* await Promise.race([audioDone, sleep(30_000)]);
|
|
776
|
+
* const effectiveTimeout = baseTimeoutMs + actualMs + 500;
|
|
777
|
+
* } catch {
|
|
778
|
+
* // fallback to conservative estimate
|
|
779
|
+
* }
|
|
780
|
+
* ```
|
|
781
|
+
*
|
|
782
|
+
* @see AudioPlaybackEvent for phase contract, `errorReason` vocabulary,
|
|
783
|
+
* and full payload details.
|
|
784
|
+
*
|
|
785
|
+
* @since SDK 1.6.0 (gateway v1.3.9.2+ required)
|
|
786
|
+
*/
|
|
787
|
+
onAudioPlayback(handler: (event: AudioPlaybackEvent) => void | Promise<void>): Unsubscribe;
|
|
788
|
+
/**
|
|
789
|
+
* Subscribe to `tts:playback` events — the canonical signal for
|
|
790
|
+
* {@link injectTts} lifecycle (start / complete / canceled / failed).
|
|
791
|
+
*
|
|
792
|
+
* Mirrors {@link onAudioPlayback} for the streaming-PCM injection path.
|
|
793
|
+
* Strictly more informative than the legacy {@link onTtsComplete}:
|
|
794
|
+
*
|
|
795
|
+
* - `onTtsComplete` fires for *every* terminal phase but exposes only
|
|
796
|
+
* `linkedId` — no way to distinguish complete from canceled
|
|
797
|
+
* (e.g. preempted by a new `injectTts`) or failed (e.g. `channel_lost`).
|
|
798
|
+
* - `onTtsPlayback` carries `injectId` (correlation across sequential
|
|
799
|
+
* injects in the same call), `phase` (4-way), `durationMs`
|
|
800
|
+
* (deterministic — gateway ticker is exactly 20ms paced), and
|
|
801
|
+
* `errorReason` (cancel cause / error class).
|
|
802
|
+
*
|
|
803
|
+
* @example
|
|
804
|
+
* ```typescript
|
|
805
|
+
* // Chain TTS prompts and wait for the *real* end of each.
|
|
806
|
+
* const r1 = await gw.injectTts(linkedId, greetingAudio());
|
|
807
|
+
* const done = new Promise<void>((resolve) => {
|
|
808
|
+
* const unsub = gw.onTtsPlayback((ev) => {
|
|
809
|
+
* if (ev.injectId !== r1.injectId) return;
|
|
810
|
+
* if (ev.phase === 'complete' || ev.phase === 'canceled' || ev.phase === 'failed') {
|
|
811
|
+
* unsub();
|
|
812
|
+
* resolve();
|
|
813
|
+
* }
|
|
814
|
+
* });
|
|
815
|
+
* });
|
|
816
|
+
* await done;
|
|
817
|
+
* const r2 = await gw.injectTts(linkedId, menuAudio()); // safe to start now
|
|
818
|
+
* ```
|
|
819
|
+
*
|
|
820
|
+
* **Pre-1.6.1 fallback** for SDK consumers — older SDKs / older gateways
|
|
821
|
+
* won't see `tts:playback`. Use a `hasattr`-style feature check if
|
|
822
|
+
* supporting both:
|
|
823
|
+
* ```typescript
|
|
824
|
+
* if ('onTtsPlayback' in gw) {
|
|
825
|
+
* gw.onTtsPlayback(handler);
|
|
826
|
+
* } else {
|
|
827
|
+
* gw.onTtsComplete((ev) => handler({ ...ev, phase: 'complete' } as any));
|
|
828
|
+
* }
|
|
829
|
+
* ```
|
|
830
|
+
*
|
|
831
|
+
* @see TtsPlaybackEvent for phase contract, `errorReason` vocabulary,
|
|
832
|
+
* and full payload details.
|
|
833
|
+
*
|
|
834
|
+
* @since SDK 1.6.1 (gateway v1.3.9.3+ required)
|
|
835
|
+
*/
|
|
836
|
+
onTtsPlayback(handler: (event: TtsPlaybackEvent) => void | Promise<void>): Unsubscribe;
|
|
640
837
|
/** List all currently active call sessions */
|
|
641
838
|
listSessions(): Promise<CallSession[]>;
|
|
642
839
|
/** List active call sessions filtered by tenant ID */
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,EAAqB,KAAK,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAIzG,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACX,SAAS,EAET,uBAAuB,EACvB,gBAAgB,EAEhB,gBAAgB,EAChB,SAAS,EACT,UAAU,EACV,WAAW,EAEX,UAAU,EACV,eAAe,EACf,kBAAkB,EACnB,MAAM,kBAAkB,CAAC;AAE1B,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkB;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;gBAElD,IAAI,EAAE,gBAAgB;IAsBlC;;;;;;;;;;;;OAYG;IACH,QAAQ,IAAI,eAAe;IAW3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2CG;IACG,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC;IAM7D;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,WAAW,CACT,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;QAAE,GAAG,CAAC,EAAE,SAAS,CAAC;QAAC,YAAY,CAAC,EAAE,YAAY,CAAA;KAAO,GAC1D,iBAAiB;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -28,6 +28,7 @@ import { TtsInjector } from './streams/tts-stream.js';
|
|
|
28
28
|
import { SessionManager } from './session/manager.js';
|
|
29
29
|
import { MinutesManager } from './minutes/manager.js';
|
|
30
30
|
import { PipelineBuilder } from './pipeline/builder.js';
|
|
31
|
+
import { VoiceFlowBuilder } from './flow/builder.js';
|
|
31
32
|
import { PipelineMetrics } from './observability/metrics.js';
|
|
32
33
|
import { createLogger } from './observability/logger.js';
|
|
33
34
|
import { createSimulatedCall, } from './simulator/simulator.js';
|
|
@@ -156,6 +157,106 @@ export class DVGatewayClient {
|
|
|
156
157
|
streamAudio(linkedId, opts = {}) {
|
|
157
158
|
return createAudioStream(this.wsPool, linkedId, this.logger, opts);
|
|
158
159
|
}
|
|
160
|
+
// ─── Voice-Flow On-Demand Audio (gateway flow=true mode) ──────────────
|
|
161
|
+
// @since SDK 1.6.2 (gateway v1.3.9.4+ required)
|
|
162
|
+
/**
|
|
163
|
+
* Attach audio streaming (ExternalMedia) to a flow-mode call mid-call.
|
|
164
|
+
*
|
|
165
|
+
* Only valid for calls that entered the gateway with the dialplan arg
|
|
166
|
+
* `Stasis(dvgateway, flow=true, ...)` — those calls land in a holding
|
|
167
|
+
* bridge with no ExternalMedia until this method is called. Idempotent —
|
|
168
|
+
* if audio is already attached the gateway returns the existing channel.
|
|
169
|
+
*
|
|
170
|
+
* Use `dir`:
|
|
171
|
+
* - `'both'` (default) — full duplex, STT + TTS
|
|
172
|
+
* - `'out'` — gateway → caller only (TTS playback, no STT cost)
|
|
173
|
+
* - `'in'` — caller → gateway only (STT only)
|
|
174
|
+
*
|
|
175
|
+
* @returns The Asterisk ExternalMedia channel ID. Returned mainly for
|
|
176
|
+
* observability — most callers will ignore it.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* await gw.attachAudio(linkedId, 'out');
|
|
181
|
+
* await gw.say(linkedId, 'Welcome', tts);
|
|
182
|
+
* await gw.detachAudio(linkedId);
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* @see {@link flow} for the high-level VoiceFlow builder that handles
|
|
186
|
+
* attach/detach automatically per stage.
|
|
187
|
+
*/
|
|
188
|
+
async attachAudio(linkedId, dir = 'both') {
|
|
189
|
+
const res = await this.http.post(`/api/v1/audio/${encodeURIComponent(linkedId)}/attach`, { dir });
|
|
190
|
+
const data = (res.data ?? {});
|
|
191
|
+
const extMediaId = typeof data['extMediaId'] === 'string' ? data['extMediaId'] : '';
|
|
192
|
+
this.logger.debug({ linkedId, dir, extMediaId }, 'Audio attached');
|
|
193
|
+
return extMediaId;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Detach audio streaming from a flow-mode call. The original channel
|
|
197
|
+
* stays parked in its holding bridge so {@link attachAudio} can be
|
|
198
|
+
* called again later. Idempotent — no-op if audio is not attached.
|
|
199
|
+
*/
|
|
200
|
+
async detachAudio(linkedId) {
|
|
201
|
+
await this.http.delete(`/api/v1/audio/${encodeURIComponent(linkedId)}/detach`);
|
|
202
|
+
this.logger.debug({ linkedId }, 'Audio detached');
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Query audio attach state for a flow-mode call.
|
|
206
|
+
*
|
|
207
|
+
* `flowMode` is `false` when the call did NOT enter with `flow=true`
|
|
208
|
+
* (or has already ended) — those calls have audio attached at
|
|
209
|
+
* StasisStart and cannot be detached.
|
|
210
|
+
*/
|
|
211
|
+
async getAudioStatus(linkedId) {
|
|
212
|
+
const res = await this.http.get(`/api/v1/audio/${encodeURIComponent(linkedId)}/status`);
|
|
213
|
+
const data = (res.data ?? {});
|
|
214
|
+
return {
|
|
215
|
+
flowMode: Boolean(data['flowMode']),
|
|
216
|
+
attached: Boolean(data['attached']),
|
|
217
|
+
extMediaId: typeof data['extMediaId'] === 'string' ? data['extMediaId'] : '',
|
|
218
|
+
bridgeId: typeof data['bridgeId'] === 'string' ? data['bridgeId'] : '',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Create a fluent SDK voice flow — a stage graph that drives a call
|
|
223
|
+
* through IVR-like steps, attaching/detaching audio per stage.
|
|
224
|
+
*
|
|
225
|
+
* Only meaningful for calls that entered with dialplan arg
|
|
226
|
+
* `flow=true`. The flow runtime listens for `call:new` events and
|
|
227
|
+
* spawns a per-call execution that:
|
|
228
|
+
* 1. Enters the start stage (calls `attachAudio` if `audio !== 'none'`)
|
|
229
|
+
* 2. Runs the stage's `onEnter` (which can `say`, `playAudio`, `collectDtmf`)
|
|
230
|
+
* 3. Transitions on DTMF (`onDtmf` map) or explicit `ctx.transitionTo()`
|
|
231
|
+
* 4. Detaches audio between stages when modes differ
|
|
232
|
+
* 5. Ends on `call:ended` or when a stage calls `ctx.hangup()`
|
|
233
|
+
*
|
|
234
|
+
* @example IVR menu
|
|
235
|
+
* ```typescript
|
|
236
|
+
* gw.flow()
|
|
237
|
+
* .stage('greet', {
|
|
238
|
+
* audio: 'tts-only',
|
|
239
|
+
* onEnter: async (ctx) => ctx.say('1번 상담, 9번 종료', tts),
|
|
240
|
+
* onDtmf: { '1': 'chat', '9': 'bye' },
|
|
241
|
+
* })
|
|
242
|
+
* .stage('chat', {
|
|
243
|
+
* audio: 'full',
|
|
244
|
+
* onEnter: async (ctx) => ctx.runPipeline(myPipeline),
|
|
245
|
+
* })
|
|
246
|
+
* .stage('bye', {
|
|
247
|
+
* audio: 'tts-only',
|
|
248
|
+
* onEnter: async (ctx) => {
|
|
249
|
+
* await ctx.say('감사합니다', tts);
|
|
250
|
+
* await ctx.hangup();
|
|
251
|
+
* },
|
|
252
|
+
* })
|
|
253
|
+
* .startStage('greet')
|
|
254
|
+
* .start();
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
flow() {
|
|
258
|
+
return new VoiceFlowBuilder(this, this.logger);
|
|
259
|
+
}
|
|
159
260
|
// ─── Comfort Noise / Thinking Signals ─────────────────────────────────
|
|
160
261
|
/**
|
|
161
262
|
* Notify the gateway that AI processing has started (STT → LLM → TTS).
|
|
@@ -219,20 +320,37 @@ export class DVGatewayClient {
|
|
|
219
320
|
* Inject synthesized speech into an active call.
|
|
220
321
|
* audioChunks should be 16kHz, 16-bit PCM (slin16) audio.
|
|
221
322
|
*
|
|
323
|
+
* Returns an {@link InjectTtsResult} carrying the `injectId` (UUID) the
|
|
324
|
+
* gateway will surface on the corresponding `tts:playback` events. SDK
|
|
325
|
+
* 1.6.1+ generates the UUID client-side so callers can correlate events
|
|
326
|
+
* to *this* inject without an extra round-trip.
|
|
327
|
+
*
|
|
222
328
|
* @example
|
|
223
329
|
* ```typescript
|
|
224
|
-
* const
|
|
225
|
-
*
|
|
330
|
+
* const r = await gw.injectTts(linkedId, ttsAdapter.synthesize('Hello!'));
|
|
331
|
+
* gw.onTtsPlayback((ev) => {
|
|
332
|
+
* if (ev.injectId === r.injectId && ev.phase === 'complete') {
|
|
333
|
+
* console.log('real RTP playback complete');
|
|
334
|
+
* }
|
|
335
|
+
* });
|
|
226
336
|
* ```
|
|
337
|
+
*
|
|
338
|
+
* @param injectId - Optional caller-supplied UUID; defaults to a fresh
|
|
339
|
+
* one. Override only for tests / multi-process correlation.
|
|
227
340
|
*/
|
|
228
|
-
async injectTts(linkedId, audioChunks) {
|
|
229
|
-
return this.ttsInjector.injectStreaming(linkedId, audioChunks);
|
|
341
|
+
async injectTts(linkedId, audioChunks, injectId) {
|
|
342
|
+
return this.ttsInjector.injectStreaming(linkedId, audioChunks, injectId);
|
|
230
343
|
}
|
|
231
344
|
/**
|
|
232
345
|
* Broadcast synthesized speech to all conference participants.
|
|
346
|
+
*
|
|
347
|
+
* Returns {@link InjectTtsResult} for parity with {@link injectTts}.
|
|
348
|
+
* Note: gateway 1.3.9.3 does not yet emit `tts:playback` for conference
|
|
349
|
+
* participant Players (only 1:1 calls), so the ID is forward-compat
|
|
350
|
+
* plumbing today.
|
|
233
351
|
*/
|
|
234
|
-
async broadcastTts(confId, audioChunks) {
|
|
235
|
-
return this.ttsInjector.broadcastToConference(confId, audioChunks);
|
|
352
|
+
async broadcastTts(confId, audioChunks, injectId) {
|
|
353
|
+
return this.ttsInjector.broadcastToConference(confId, audioChunks, injectId);
|
|
236
354
|
}
|
|
237
355
|
/**
|
|
238
356
|
* Convenience: synthesize text and inject it directly.
|
|
@@ -557,9 +675,19 @@ export class DVGatewayClient {
|
|
|
557
675
|
};
|
|
558
676
|
const res = await this.http.post(`/api/v1/play/${encodeURIComponent(linkedId)}`, body);
|
|
559
677
|
const data = (res.data ?? {});
|
|
678
|
+
// playbackId is surfaced on both 202 (async start) and 200 (sync result).
|
|
679
|
+
// Empty string when the gateway is older than 1.3.9.2 — older code
|
|
680
|
+
// treats it as "no correlation handle available".
|
|
681
|
+
const playbackIdRaw = data['playbackId'];
|
|
682
|
+
const playbackId = typeof playbackIdRaw === 'string' ? playbackIdRaw : '';
|
|
560
683
|
if (!waitForCompletion) {
|
|
561
684
|
// 202 — gateway accepted the request; playback proceeds async.
|
|
562
|
-
return {
|
|
685
|
+
return {
|
|
686
|
+
completed: false,
|
|
687
|
+
interruptedByDtmf: null,
|
|
688
|
+
durationMs: 0,
|
|
689
|
+
playbackId,
|
|
690
|
+
};
|
|
563
691
|
}
|
|
564
692
|
const rawDtmf = data['interruptedByDtmf'];
|
|
565
693
|
const interruptedByDtmf = typeof rawDtmf === 'string' && rawDtmf !== '' ? rawDtmf : null;
|
|
@@ -569,6 +697,7 @@ export class DVGatewayClient {
|
|
|
569
697
|
completed: Boolean(data['completed']),
|
|
570
698
|
interruptedByDtmf,
|
|
571
699
|
durationMs,
|
|
700
|
+
playbackId,
|
|
572
701
|
};
|
|
573
702
|
}
|
|
574
703
|
/**
|
|
@@ -973,6 +1102,106 @@ export class DVGatewayClient {
|
|
|
973
1102
|
onChannelState(handler) {
|
|
974
1103
|
return this.callEvents.onType('channel:state', handler);
|
|
975
1104
|
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Subscribe to `audio:playback` events — the canonical signal for
|
|
1107
|
+
* {@link playAudio} lifecycle (start / complete / canceled / failed).
|
|
1108
|
+
*
|
|
1109
|
+
* **Primary use case — DTMF prompt audio with accurate timeout extension:**
|
|
1110
|
+
* application code that plays an instructional prompt and then waits for
|
|
1111
|
+
* DTMF input previously had to estimate prompt length with a hard-coded
|
|
1112
|
+
* fallback (e.g. `baseTimeoutMs + 8000`). With this event the application
|
|
1113
|
+
* can extend its timeout by the *actual* playback duration, dropping both
|
|
1114
|
+
* unnecessary slack and the risk of the timer firing before the prompt
|
|
1115
|
+
* finishes.
|
|
1116
|
+
*
|
|
1117
|
+
* @example
|
|
1118
|
+
* ```typescript
|
|
1119
|
+
* const result = await gw.playAudio({
|
|
1120
|
+
* linkedId,
|
|
1121
|
+
* url: promptUrl,
|
|
1122
|
+
* waitForCompletion: false,
|
|
1123
|
+
* });
|
|
1124
|
+
* const playbackId = result.playbackId;
|
|
1125
|
+
*
|
|
1126
|
+
* let actualMs = 0;
|
|
1127
|
+
* const audioDone = new Promise<void>((resolve) => {
|
|
1128
|
+
* const unsub = gw.onAudioPlayback((ev) => {
|
|
1129
|
+
* if (ev.playbackId !== playbackId) return;
|
|
1130
|
+
* if (ev.phase === 'complete' || ev.phase === 'canceled' || ev.phase === 'failed') {
|
|
1131
|
+
* actualMs = ev.durationMs ?? 0;
|
|
1132
|
+
* unsub();
|
|
1133
|
+
* resolve();
|
|
1134
|
+
* }
|
|
1135
|
+
* });
|
|
1136
|
+
* });
|
|
1137
|
+
*
|
|
1138
|
+
* try {
|
|
1139
|
+
* await Promise.race([audioDone, sleep(30_000)]);
|
|
1140
|
+
* const effectiveTimeout = baseTimeoutMs + actualMs + 500;
|
|
1141
|
+
* } catch {
|
|
1142
|
+
* // fallback to conservative estimate
|
|
1143
|
+
* }
|
|
1144
|
+
* ```
|
|
1145
|
+
*
|
|
1146
|
+
* @see AudioPlaybackEvent for phase contract, `errorReason` vocabulary,
|
|
1147
|
+
* and full payload details.
|
|
1148
|
+
*
|
|
1149
|
+
* @since SDK 1.6.0 (gateway v1.3.9.2+ required)
|
|
1150
|
+
*/
|
|
1151
|
+
onAudioPlayback(handler) {
|
|
1152
|
+
return this.callEvents.onType('audio:playback', handler);
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Subscribe to `tts:playback` events — the canonical signal for
|
|
1156
|
+
* {@link injectTts} lifecycle (start / complete / canceled / failed).
|
|
1157
|
+
*
|
|
1158
|
+
* Mirrors {@link onAudioPlayback} for the streaming-PCM injection path.
|
|
1159
|
+
* Strictly more informative than the legacy {@link onTtsComplete}:
|
|
1160
|
+
*
|
|
1161
|
+
* - `onTtsComplete` fires for *every* terminal phase but exposes only
|
|
1162
|
+
* `linkedId` — no way to distinguish complete from canceled
|
|
1163
|
+
* (e.g. preempted by a new `injectTts`) or failed (e.g. `channel_lost`).
|
|
1164
|
+
* - `onTtsPlayback` carries `injectId` (correlation across sequential
|
|
1165
|
+
* injects in the same call), `phase` (4-way), `durationMs`
|
|
1166
|
+
* (deterministic — gateway ticker is exactly 20ms paced), and
|
|
1167
|
+
* `errorReason` (cancel cause / error class).
|
|
1168
|
+
*
|
|
1169
|
+
* @example
|
|
1170
|
+
* ```typescript
|
|
1171
|
+
* // Chain TTS prompts and wait for the *real* end of each.
|
|
1172
|
+
* const r1 = await gw.injectTts(linkedId, greetingAudio());
|
|
1173
|
+
* const done = new Promise<void>((resolve) => {
|
|
1174
|
+
* const unsub = gw.onTtsPlayback((ev) => {
|
|
1175
|
+
* if (ev.injectId !== r1.injectId) return;
|
|
1176
|
+
* if (ev.phase === 'complete' || ev.phase === 'canceled' || ev.phase === 'failed') {
|
|
1177
|
+
* unsub();
|
|
1178
|
+
* resolve();
|
|
1179
|
+
* }
|
|
1180
|
+
* });
|
|
1181
|
+
* });
|
|
1182
|
+
* await done;
|
|
1183
|
+
* const r2 = await gw.injectTts(linkedId, menuAudio()); // safe to start now
|
|
1184
|
+
* ```
|
|
1185
|
+
*
|
|
1186
|
+
* **Pre-1.6.1 fallback** for SDK consumers — older SDKs / older gateways
|
|
1187
|
+
* won't see `tts:playback`. Use a `hasattr`-style feature check if
|
|
1188
|
+
* supporting both:
|
|
1189
|
+
* ```typescript
|
|
1190
|
+
* if ('onTtsPlayback' in gw) {
|
|
1191
|
+
* gw.onTtsPlayback(handler);
|
|
1192
|
+
* } else {
|
|
1193
|
+
* gw.onTtsComplete((ev) => handler({ ...ev, phase: 'complete' } as any));
|
|
1194
|
+
* }
|
|
1195
|
+
* ```
|
|
1196
|
+
*
|
|
1197
|
+
* @see TtsPlaybackEvent for phase contract, `errorReason` vocabulary,
|
|
1198
|
+
* and full payload details.
|
|
1199
|
+
*
|
|
1200
|
+
* @since SDK 1.6.1 (gateway v1.3.9.3+ required)
|
|
1201
|
+
*/
|
|
1202
|
+
onTtsPlayback(handler) {
|
|
1203
|
+
return this.callEvents.onType('tts:playback', handler);
|
|
1204
|
+
}
|
|
976
1205
|
// ─── Session Management ─────────────────────────────────────────────────
|
|
977
1206
|
/** List all currently active call sessions */
|
|
978
1207
|
async listSessions() {
|