@ariaflowagents/livekit-plugin-transport-twilio 0.9.0

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/README.md ADDED
@@ -0,0 +1,221 @@
1
+ # @ariaflow/livekit-plugin-transport-twilio
2
+
3
+ Twilio Media Streams transport adapter for AriaFlow voice agents. Works with Node.js, Bun, Deno, and any WebSocket-enabled runtime. Cloudflare Workers support is currently unstable.
4
+
5
+ ## Features
6
+
7
+ - ⚠️ **Cloudflare Workers Compatible** (Unstable) - No Node.js dependencies
8
+ - ✅ **Twilio Media Streams Support** - Native integration with Twilio's API
9
+ - ✅ **Automatic Codec Conversion** - G.711 μ-law ↔ PCM with resampling (8kHz ↔ 24kHz)
10
+ - ✅ **AriaFlow Native** - Works with `createAriaFlowSession` and AriaFlow Runtime
11
+ - ✅ **Session Management** - Full lifecycle management with SessionManager
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # Using npm
17
+ npm install @ariaflow/livekit-plugin @ariaflow/livekit-plugin-transport-twilio
18
+
19
+ # Using bun
20
+ bun add @ariaflow/livekit-plugin @ariaflow/livekit-plugin-transport-twilio
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ### Cloudflare Workers (Unstable)
26
+
27
+ > ⚠️ **Note:** The Cloudflare Workers integration is currently unstable and not officially supported. Use at your own risk.
28
+
29
+ ```typescript
30
+ // wrangler.toml:
31
+ // name = "ariaflow-twilio-agent"
32
+ // compatibility_date = "2024-01-01"
33
+ // compatibility_flags = ["nodejs_compat"]
34
+
35
+ import { createTwilioWorker } from '@ariaflow/livekit-plugin-transport-twilio/cloudflare';
36
+ import { createAriaFlowSession } from '@ariaflow/livekit-plugin';
37
+ import { GeminiLiveSTT, GeminiLiveTTS } from '@ariaflow/livekit-plugin/gemini';
38
+
39
+ const agentConfig = {
40
+ agents: [{ id: 'assistant', prompt: 'You are helpful.' }],
41
+ defaultAgentId: 'assistant',
42
+ };
43
+
44
+ export default createTwilioWorker({
45
+ agent: () => createAriaFlowSession({
46
+ runtime: agentConfig,
47
+ stt: new GeminiLiveSTT(),
48
+ tts: new GeminiLiveTTS(),
49
+ greeting: 'Hello! How can I help?',
50
+ }),
51
+ });
52
+ ```
53
+
54
+ ### Node.js / Deno / Any WebSocket
55
+
56
+ ```typescript
57
+ import { WebSocketServer } from 'ws';
58
+ import { TwilioTransportAdapter } from '@ariaflow/livekit-plugin-transport-twilio';
59
+ import { createAriaFlowSession } from '@ariaflow/livekit-plugin';
60
+ import { SessionManager } from '@ariaflow/livekit-plugin';
61
+ import { GeminiLiveSTT, GeminiLiveTTS } from '@ariaflow/livekit-plugin/gemini';
62
+
63
+ const wss = new WebSocketServer({ port: 8080 });
64
+ const sessionManager = new SessionManager();
65
+
66
+ const agentConfig = {
67
+ agents: [{ id: 'assistant', prompt: 'You are helpful.' }],
68
+ defaultAgentId: 'assistant',
69
+ };
70
+
71
+ wss.on('connection', (ws) => {
72
+ const transport = new TwilioTransportAdapter({
73
+ send: (msg) => ws.send(msg),
74
+ });
75
+
76
+ ws.on('message', (data) => {
77
+ transport.handleMessage(data.toString());
78
+ });
79
+
80
+ ws.on('close', async () => {
81
+ await transport.close();
82
+ });
83
+
84
+ // Wait for 'connected' event before starting session
85
+ ws.once('message', async (data) => {
86
+ const event = JSON.parse(data.toString());
87
+ if (event.event === 'connected') {
88
+ const { agent, sessionOptions } = createAriaFlowSession({
89
+ runtime: agentConfig,
90
+ stt: new GeminiLiveSTT(),
91
+ tts: new GeminiLiveTTS(),
92
+ });
93
+
94
+ await sessionManager.startSession(transport, agent, sessionOptions);
95
+ }
96
+ });
97
+ });
98
+ ```
99
+
100
+ ## TwiML Setup
101
+
102
+ Create a TwiML bin in your Twilio console:
103
+
104
+ ```xml
105
+ <?xml version="1.0" encoding="UTF-8"?>
106
+ <Response>
107
+ <Say>Connecting to AI assistant...</Say>
108
+ <Connect>
109
+ <Stream url="wss://your-worker.workers.dev/voice/stream" />
110
+ </Connect>
111
+ </Response>
112
+ ```
113
+
114
+ ## Audio Specifications
115
+
116
+ | Direction | Twilio Format | Internal Format | Sample Rate |
117
+ |-----------|---------------|-----------------|-------------|
118
+ | Inbound | G.711 μ-law | PCM S16 LE | 8kHz → 24kHz |
119
+ | Outbound | PCM S16 LE | G.711 μ-law | 24kHz → 8kHz |
120
+
121
+ The transport handles all codec conversion and resampling automatically.
122
+
123
+ ## API Reference
124
+
125
+ ### `TwilioTransportAdapter`
126
+
127
+ Main transport adapter class.
128
+
129
+ ```typescript
130
+ const transport = new TwilioTransportAdapter({
131
+ id: 'optional-custom-id',
132
+ send: (message: string) => {
133
+ // Send message to WebSocket
134
+ ws.send(message);
135
+ },
136
+ });
137
+ ```
138
+
139
+ **Properties:**
140
+ - `id: string` - Unique identifier for this transport
141
+ - `audioInput: TwilioAudioInput` - Audio input stream
142
+ - `audioOutput: TwilioAudioOutput` - Audio output stream
143
+ - `textOutput: TwilioTextOutput` - Text output stream
144
+ - `config: TransportAdapterConfig` - Configuration object
145
+ - `isOpen: boolean` - Whether the transport is active
146
+
147
+ **Methods:**
148
+ - `handleMessage(message: string)` - Process incoming Twilio message
149
+ - `clearAudio()` - Clear audio buffer
150
+ - `close()` - Close the transport
151
+
152
+ ### `createTwilioWorker(options)` (Unstable)
153
+
154
+ > ⚠️ **Note:** This Cloudflare Workers integration helper is currently unstable and not officially supported.
155
+
156
+ Cloudflare Workers integration helper.
157
+
158
+ ```typescript
159
+ export default createTwilioWorker({
160
+ agent: () => createAriaFlowSession({...}),
161
+ });
162
+ ```
163
+
164
+ **Options:**
165
+ - `agent: () => Agent | { agent: Agent; sessionOptions: any }` - Agent factory
166
+ - `stt?: any` - STT instance (for raw LiveKit pattern)
167
+ - `llm?: any` - LLM instance
168
+ - `tts?: any` - TTS instance
169
+ - `vad?: any` - VAD instance
170
+ - `turnDetection?: any` - Turn detection config
171
+
172
+ ## Environment Compatibility
173
+
174
+ | Runtime | WebSocket Support | Status |
175
+ |---------|------------------|--------|
176
+ | Cloudflare Workers | ✅ Native | ⚠️ Unstable |
177
+ | Node.js | ✅ via `ws` package | ✅ Supported |
178
+ | Deno | ✅ Native | ✅ Supported |
179
+ | Bun | ✅ Native | ✅ Supported |
180
+ | Browser | ✅ Native | ✅ Supported (development only) |
181
+
182
+ ## Limitations
183
+
184
+ 1. **Twilio Sample Rate**: Twilio uses 8kHz G.711 μ-law. The transport resamples to 24kHz for AriaFlow, but this adds slight latency.
185
+ 2. **Codec**: Only G.711 μ-law is supported (Twilio's standard).
186
+ 3. **Latency**: Expect 500-1000ms additional latency compared to direct WebRTC.
187
+ 4. **Full Duplex**: Twilio Media Streams is full-duplex, but the implementation treats inbound/outbound as separate streams.
188
+
189
+ ## Troubleshooting
190
+
191
+ ### Audio cutting out
192
+ - Check your WebSocket connection is stable
193
+ - Verify `send` callback is working correctly
194
+ - Check for errors in the console
195
+
196
+ ### Poor audio quality
197
+ - The 8kHz sample rate is lower than typical 24kHz
198
+ - This is a Twilio limitation, not the transport
199
+ - Consider using Twilio's programmable voice SDK for higher quality
200
+
201
+ ### Connection issues
202
+ - Ensure your WebSocket URL is publicly accessible
203
+ - Check Twilio can reach your server (no firewall blocking)
204
+ - Verify CORS headers if using browser-based testing
205
+
206
+ ## Examples
207
+
208
+ See `examples/` directory for:
209
+ - `cloudflare_worker.ts` - Complete Cloudflare Worker example (⚠️ Unstable)
210
+ - `wrangler.toml` - Deployment configuration
211
+
212
+ ## License
213
+
214
+ Apache-2.0
215
+
216
+ ## Related Packages
217
+
218
+ - `@ariaflow/livekit-plugin` - Core AriaFlow LiveKit integration
219
+ - `@ariaflow/livekit-plugin/gemini` - Gemini STT/TTS plugins
220
+ - `@ariaflow/livekit-plugin-transport-ws` - WebSocket transport
221
+ - `@ariaflow/livekit-plugin-transport-http` - HTTP/SSE transport
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Audio input for Twilio Media Streams.
3
+ *
4
+ * Receives mu-law encoded audio at 8kHz from Twilio, decodes to PCM,
5
+ * resamples to 24kHz via a streaming Sox sinc resampler, and provides
6
+ * AudioFrame objects to the STT pipeline.
7
+ *
8
+ * The resampler is allocated once and reused across all media events
9
+ * for the lifetime of this input (local Rust FFI, no LiveKit server).
10
+ */
11
+ import { AudioInput } from '@ariaflow/livekit-plugin';
12
+ import type { TwilioMediaEvent } from './twilio_protocol.js';
13
+ /**
14
+ * Receives audio from Twilio Media Streams and provides it as a
15
+ * ReadableStream<AudioFrame> for the STT pipeline.
16
+ *
17
+ * Flow:
18
+ * 1. Twilio sends mu-law audio at 8kHz (base64 encoded)
19
+ * 2. Decode mu-law to PCM Int16
20
+ * 3. Wrap in AudioFrame at 8kHz
21
+ * 4. Push through streaming resampler -> 24kHz AudioFrames
22
+ * 5. Write frames to STT stream
23
+ */
24
+ export declare class TwilioAudioInput extends AudioInput {
25
+ private currentStreamId;
26
+ private streamWriter;
27
+ private closed;
28
+ private resampler;
29
+ /**
30
+ * Process a media event from Twilio.
31
+ *
32
+ * @param event - Twilio media event with base64 mu-law payload
33
+ */
34
+ handleMediaEvent(event: TwilioMediaEvent): void;
35
+ private startNewStream;
36
+ /**
37
+ * End the current audio stream (flush remaining audio from resampler).
38
+ */
39
+ endCurrentStream(): void;
40
+ close(): Promise<void>;
41
+ }
42
+ //# sourceMappingURL=audio_input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio_input.d.ts","sourceRoot":"","sources":["../src/audio_input.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAc,MAAM,0BAA0B,CAAC;AAIlE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAK7D;;;;;;;;;;GAUG;AACH,qBAAa,gBAAiB,SAAQ,UAAU;IAC9C,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,YAAY,CAAwD;IAC5E,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,SAAS,CAA2D;IAE5E;;;;OAIG;IACH,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAgD/C,OAAO,CAAC,cAAc;IAMtB;;OAEG;IACH,gBAAgB,IAAI,IAAI;IAuBlB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAK7B"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Audio input for Twilio Media Streams.
3
+ *
4
+ * Receives mu-law encoded audio at 8kHz from Twilio, decodes to PCM,
5
+ * resamples to 24kHz via a streaming Sox sinc resampler, and provides
6
+ * AudioFrame objects to the STT pipeline.
7
+ *
8
+ * The resampler is allocated once and reused across all media events
9
+ * for the lifetime of this input (local Rust FFI, no LiveKit server).
10
+ */
11
+ import { AudioInput, AudioFrame } from '@ariaflow/livekit-plugin';
12
+ import { createResampler } from '@ariaflow/livekit-plugin/utils/resample';
13
+ import { TransformStream } from 'stream/web';
14
+ import { mulawDecodeArray } from '@ariaflow/livekit-plugin/codec/g711';
15
+ const TWILIO_SAMPLE_RATE = 8000;
16
+ const TARGET_SAMPLE_RATE = 24000;
17
+ /**
18
+ * Receives audio from Twilio Media Streams and provides it as a
19
+ * ReadableStream<AudioFrame> for the STT pipeline.
20
+ *
21
+ * Flow:
22
+ * 1. Twilio sends mu-law audio at 8kHz (base64 encoded)
23
+ * 2. Decode mu-law to PCM Int16
24
+ * 3. Wrap in AudioFrame at 8kHz
25
+ * 4. Push through streaming resampler -> 24kHz AudioFrames
26
+ * 5. Write frames to STT stream
27
+ */
28
+ export class TwilioAudioInput extends AudioInput {
29
+ currentStreamId = null;
30
+ streamWriter = null;
31
+ closed = false;
32
+ resampler = createResampler(TWILIO_SAMPLE_RATE, TARGET_SAMPLE_RATE);
33
+ /**
34
+ * Process a media event from Twilio.
35
+ *
36
+ * @param event - Twilio media event with base64 mu-law payload
37
+ */
38
+ handleMediaEvent(event) {
39
+ if (this.closed)
40
+ return;
41
+ const payload = event.media.payload;
42
+ if (!payload)
43
+ return;
44
+ try {
45
+ // Decode base64 -> mu-law bytes
46
+ const mulawData = Uint8Array.from(atob(payload), (c) => c.charCodeAt(0));
47
+ // Decode mu-law to PCM at 8kHz
48
+ const pcm8kHz = mulawDecodeArray(mulawData);
49
+ // Wrap in AudioFrame at the input rate
50
+ const inputFrame = new AudioFrame(pcm8kHz, TWILIO_SAMPLE_RATE, 1, pcm8kHz.length);
51
+ // Resample 8kHz -> 24kHz via streaming resampler
52
+ const outputFrames = this.resampler.push(inputFrame);
53
+ if (outputFrames.length === 0)
54
+ return;
55
+ // Ensure we have an active stream
56
+ if (!this.currentStreamId) {
57
+ this.startNewStream();
58
+ }
59
+ // Write resampled frames to STT stream
60
+ for (const frame of outputFrames) {
61
+ if (this.streamWriter) {
62
+ this.streamWriter.write(frame).catch((err) => {
63
+ if (!this.closed) {
64
+ console.error('[TwilioAudioInput] Error writing frame:', {
65
+ error: err instanceof Error ? err.message : String(err),
66
+ streamId: this.currentStreamId,
67
+ timestamp: new Date().toISOString(),
68
+ });
69
+ }
70
+ });
71
+ }
72
+ }
73
+ }
74
+ catch (error) {
75
+ console.error('[TwilioAudioInput] Error processing media event:', {
76
+ error: error instanceof Error ? error.message : String(error),
77
+ payloadLength: payload?.length || 0,
78
+ timestamp: new Date().toISOString(),
79
+ });
80
+ }
81
+ }
82
+ startNewStream() {
83
+ const { readable, writable } = new TransformStream();
84
+ this.streamWriter = writable.getWriter();
85
+ this.currentStreamId = this.multiStream.addInputStream(readable);
86
+ }
87
+ /**
88
+ * End the current audio stream (flush remaining audio from resampler).
89
+ */
90
+ endCurrentStream() {
91
+ // Flush resampler to emit any remaining buffered samples
92
+ if (this.streamWriter) {
93
+ for (const frame of this.resampler.flush()) {
94
+ this.streamWriter.write(frame).catch(() => { });
95
+ }
96
+ this.streamWriter.close().catch((err) => {
97
+ if (!this.closed) {
98
+ console.error('[TwilioAudioInput] Error closing stream writer:', {
99
+ error: err instanceof Error ? err.message : String(err),
100
+ streamId: this.currentStreamId,
101
+ timestamp: new Date().toISOString(),
102
+ });
103
+ }
104
+ });
105
+ this.streamWriter = null;
106
+ }
107
+ this.currentStreamId = null;
108
+ // Re-create the resampler for the next stream (fresh state)
109
+ this.resampler = createResampler(TWILIO_SAMPLE_RATE, TARGET_SAMPLE_RATE);
110
+ }
111
+ async close() {
112
+ this.closed = true;
113
+ this.endCurrentStream();
114
+ await super.close();
115
+ }
116
+ }
117
+ //# sourceMappingURL=audio_input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio_input.js","sourceRoot":"","sources":["../src/audio_input.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAGvE,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAChC,MAAM,kBAAkB,GAAG,KAAK,CAAC;AAEjC;;;;;;;;;;GAUG;AACH,MAAM,OAAO,gBAAiB,SAAQ,UAAU;IACtC,eAAe,GAAkB,IAAI,CAAC;IACtC,YAAY,GAAmD,IAAI,CAAC;IACpE,MAAM,GAAY,KAAK,CAAC;IACxB,SAAS,GAAG,eAAe,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,CAAC;IAE5E;;;;OAIG;IACH,gBAAgB,CAAC,KAAuB;QACtC,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QAExB,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC;QACpC,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,IAAI,CAAC;YACH,gCAAgC;YAChC,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;YAEzE,+BAA+B;YAC/B,MAAM,OAAO,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;YAE5C,uCAAuC;YACvC,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,OAAO,EAAE,kBAAkB,EAAE,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YAElF,iDAAiD;YACjD,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACrD,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO;YAEtC,kCAAkC;YAClC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;gBAC1B,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,CAAC;YAED,uCAAuC;YACvC,KAAK,MAAM,KAAK,IAAI,YAAY,EAAE,CAAC;gBACjC,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;oBACtB,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;wBAC3C,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;4BACjB,OAAO,CAAC,KAAK,CAAC,yCAAyC,EAAE;gCACvD,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;gCACvD,QAAQ,EAAE,IAAI,CAAC,eAAe;gCAC9B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;6BACpC,CAAC,CAAC;wBACL,CAAC;oBACH,CAAC,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,kDAAkD,EAAE;gBAChE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;gBAC7D,aAAa,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;gBACnC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACpC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAEO,cAAc;QACpB,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,eAAe,EAAc,CAAC;QACjE,IAAI,CAAC,YAAY,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC;QACzC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,WAAW,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;IACnE,CAAC;IAED;;OAEG;IACH,gBAAgB;QACd,yDAAyD;QACzD,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,CAAC;gBAC3C,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACjD,CAAC;YACD,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACtC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;oBACjB,OAAO,CAAC,KAAK,CAAC,iDAAiD,EAAE;wBAC/D,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;wBACvD,QAAQ,EAAE,IAAI,CAAC,eAAe;wBAC9B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;qBACpC,CAAC,CAAC;gBACL,CAAC;YACH,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAE5B,4DAA4D;QAC5D,IAAI,CAAC,SAAS,GAAG,eAAe,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,CAAC;IAC3E,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxB,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;CACF"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Audio output for Twilio Media Streams.
3
+ *
4
+ * Receives AudioFrame objects at 24kHz from the TTS pipeline,
5
+ * resamples to 8kHz via a streaming Sox sinc resampler, encodes
6
+ * to mu-law, and sends to Twilio via WebSocket.
7
+ *
8
+ * The resampler is allocated once and reused across all frames
9
+ * for the lifetime of this output (local Rust FFI, no LiveKit server).
10
+ */
11
+ import { AudioOutput, AudioFrame } from '@ariaflow/livekit-plugin';
12
+ /**
13
+ * Sends audio frames from the TTS pipeline to Twilio Media Streams.
14
+ *
15
+ * Flow:
16
+ * 1. TTS pipeline sends AudioFrame at 24kHz
17
+ * 2. Push through streaming resampler -> 8kHz AudioFrames
18
+ * 3. Encode each frame to mu-law
19
+ * 4. Base64 encode
20
+ * 5. Send as Twilio media event
21
+ */
22
+ export declare class TwilioAudioOutput extends AudioOutput {
23
+ private sendQueue;
24
+ private sending;
25
+ private segmentSamplesSent;
26
+ private flushed;
27
+ private closed;
28
+ private resampler;
29
+ private sendCallback;
30
+ constructor();
31
+ /**
32
+ * Set the callback for sending messages to Twilio.
33
+ *
34
+ * @param callback - Function that sends JSON string to WebSocket
35
+ */
36
+ setSendCallback(callback: (message: string) => void): void;
37
+ captureFrame(frame: AudioFrame): Promise<void>;
38
+ private processSendQueue;
39
+ /**
40
+ * Encode and send a single 8kHz audio frame to Twilio.
41
+ */
42
+ private sendFrame;
43
+ flush(): void;
44
+ clearBuffer(): void;
45
+ close(): Promise<void>;
46
+ }
47
+ //# sourceMappingURL=audio_output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio_output.d.ts","sourceRoot":"","sources":["../src/audio_output.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAOnE;;;;;;;;;GASG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;IAChD,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,SAAS,CAAwD;IAGzE,OAAO,CAAC,YAAY,CAAuC;;IAM3D;;;;OAIG;IACH,eAAe,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAIpD,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAcpD,OAAO,CAAC,gBAAgB;IAyCxB;;OAEG;IACH,OAAO,CAAC,SAAS;IAoBjB,KAAK,IAAI,IAAI;IA0Bb,WAAW,IAAI,IAAI;IA6Bb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Audio output for Twilio Media Streams.
3
+ *
4
+ * Receives AudioFrame objects at 24kHz from the TTS pipeline,
5
+ * resamples to 8kHz via a streaming Sox sinc resampler, encodes
6
+ * to mu-law, and sends to Twilio via WebSocket.
7
+ *
8
+ * The resampler is allocated once and reused across all frames
9
+ * for the lifetime of this output (local Rust FFI, no LiveKit server).
10
+ */
11
+ import { AudioOutput } from '@ariaflow/livekit-plugin';
12
+ import { createResampler } from '@ariaflow/livekit-plugin/utils/resample';
13
+ import { mulawEncodeArray } from '@ariaflow/livekit-plugin/codec/g711';
14
+ const TTS_SAMPLE_RATE = 24000;
15
+ const TARGET_SAMPLE_RATE = 8000;
16
+ /**
17
+ * Sends audio frames from the TTS pipeline to Twilio Media Streams.
18
+ *
19
+ * Flow:
20
+ * 1. TTS pipeline sends AudioFrame at 24kHz
21
+ * 2. Push through streaming resampler -> 8kHz AudioFrames
22
+ * 3. Encode each frame to mu-law
23
+ * 4. Base64 encode
24
+ * 5. Send as Twilio media event
25
+ */
26
+ export class TwilioAudioOutput extends AudioOutput {
27
+ sendQueue = [];
28
+ sending = false;
29
+ segmentSamplesSent = 0;
30
+ flushed = false;
31
+ closed = false;
32
+ resampler = createResampler(TTS_SAMPLE_RATE, TARGET_SAMPLE_RATE);
33
+ // Callback to send messages to Twilio
34
+ sendCallback = () => { };
35
+ constructor() {
36
+ super(TARGET_SAMPLE_RATE);
37
+ }
38
+ /**
39
+ * Set the callback for sending messages to Twilio.
40
+ *
41
+ * @param callback - Function that sends JSON string to WebSocket
42
+ */
43
+ setSendCallback(callback) {
44
+ this.sendCallback = callback;
45
+ }
46
+ async captureFrame(frame) {
47
+ await super.captureFrame(frame);
48
+ if (this.closed)
49
+ return;
50
+ // Push 24kHz frame through the streaming resampler -> 8kHz frames
51
+ for (const outFrame of this.resampler.push(frame)) {
52
+ this.sendQueue.push(outFrame);
53
+ }
54
+ if (!this.sending) {
55
+ this.processSendQueue();
56
+ }
57
+ }
58
+ processSendQueue() {
59
+ if (this.sending || this.closed)
60
+ return;
61
+ this.sending = true;
62
+ while (this.sendQueue.length > 0) {
63
+ const frame = this.sendQueue.shift();
64
+ if (this.segmentSamplesSent === 0) {
65
+ this.onPlaybackStarted(Date.now());
66
+ }
67
+ try {
68
+ this.sendFrame(frame);
69
+ }
70
+ catch (error) {
71
+ console.error('[TwilioAudioOutput] Error sending frame:', {
72
+ error: error instanceof Error ? error.message : String(error),
73
+ samplesPerChannel: frame.samplesPerChannel,
74
+ timestamp: new Date().toISOString(),
75
+ });
76
+ break;
77
+ }
78
+ this.segmentSamplesSent += frame.samplesPerChannel;
79
+ }
80
+ this.sending = false;
81
+ if (this.flushed && this.sendQueue.length === 0) {
82
+ const sampleRate = this.sampleRate || TARGET_SAMPLE_RATE;
83
+ const playbackDuration = this.segmentSamplesSent / sampleRate;
84
+ this.onPlaybackFinished({
85
+ playbackPosition: playbackDuration,
86
+ interrupted: false,
87
+ });
88
+ this.segmentSamplesSent = 0;
89
+ this.flushed = false;
90
+ }
91
+ }
92
+ /**
93
+ * Encode and send a single 8kHz audio frame to Twilio.
94
+ */
95
+ sendFrame(frame) {
96
+ // Encode to mu-law
97
+ const mulawData = mulawEncodeArray(new Int16Array(frame.data));
98
+ // Base64 encode
99
+ const base64 = Buffer.from(mulawData).toString('base64');
100
+ // Create Twilio media event
101
+ const message = JSON.stringify({
102
+ event: 'media',
103
+ streamSid: '',
104
+ sequenceNumber: `${Date.now()}`,
105
+ media: {
106
+ payload: base64,
107
+ },
108
+ });
109
+ this.sendCallback(message);
110
+ }
111
+ flush() {
112
+ super.flush();
113
+ // Drain any buffered samples from the resampler
114
+ for (const outFrame of this.resampler.flush()) {
115
+ this.sendQueue.push(outFrame);
116
+ }
117
+ this.flushed = true;
118
+ if (this.sendQueue.length === 0) {
119
+ const sampleRate = this.sampleRate || TARGET_SAMPLE_RATE;
120
+ const playbackDuration = this.segmentSamplesSent / sampleRate;
121
+ this.onPlaybackFinished({
122
+ playbackPosition: playbackDuration,
123
+ interrupted: false,
124
+ });
125
+ this.segmentSamplesSent = 0;
126
+ this.flushed = false;
127
+ }
128
+ else if (!this.sending) {
129
+ this.processSendQueue();
130
+ }
131
+ }
132
+ clearBuffer() {
133
+ const sampleRate = this.sampleRate || TARGET_SAMPLE_RATE;
134
+ const playbackDuration = this.segmentSamplesSent / sampleRate;
135
+ this.sendQueue = [];
136
+ this.onPlaybackFinished({
137
+ playbackPosition: playbackDuration,
138
+ interrupted: true,
139
+ });
140
+ this.segmentSamplesSent = 0;
141
+ this.flushed = false;
142
+ // Send clear message to Twilio
143
+ try {
144
+ this.sendCallback(JSON.stringify({
145
+ event: 'clear',
146
+ streamSid: '',
147
+ sequenceNumber: `${Date.now()}`,
148
+ }));
149
+ }
150
+ catch (error) {
151
+ console.error('[TwilioAudioOutput] Error sending clear message:', {
152
+ error: error instanceof Error ? error.message : String(error),
153
+ timestamp: new Date().toISOString(),
154
+ });
155
+ }
156
+ }
157
+ async close() {
158
+ this.closed = true;
159
+ this.sendQueue = [];
160
+ }
161
+ }
162
+ //# sourceMappingURL=audio_output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio_output.js","sourceRoot":"","sources":["../src/audio_output.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * G.711 μ-law codec implementation for Twilio Media Streams.
3
+ *
4
+ * Twilio uses G.711 μ-law (mu-law) encoding at 8kHz.
5
+ * This codec provides bidirectional conversion between PCM Int16
6
+ * and μ-law encoded Uint8.
7
+ */
8
+ /**
9
+ * Encode a PCM Int16 sample to μ-law Uint8.
10
+ *
11
+ * @param sample - 16-bit PCM sample (-32768 to 32767)
12
+ * @returns μ-law encoded byte (0-255)
13
+ */
14
+ export declare function mulawEncode(sample: number): number;
15
+ /**
16
+ * Decode a μ-law Uint8 to PCM Int16.
17
+ *
18
+ * @param mulaw - μ-law encoded byte (0-255)
19
+ * @returns 16-bit PCM sample
20
+ */
21
+ export declare function mulawDecode(mulaw: number): number;
22
+ /**
23
+ * Encode an array of PCM samples to μ-law.
24
+ *
25
+ * @param pcm - Int16Array of PCM samples
26
+ * @returns Uint8Array of μ-law encoded bytes
27
+ */
28
+ export declare function mulawEncodeArray(pcm: Int16Array): Uint8Array;
29
+ /**
30
+ * Decode an array of μ-law bytes to PCM.
31
+ *
32
+ * @param mulaw - Uint8Array of μ-law encoded bytes
33
+ * @returns Int16Array of PCM samples
34
+ */
35
+ export declare function mulawDecodeArray(mulaw: Uint8Array): Int16Array;
36
+ //# sourceMappingURL=g711.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"g711.d.ts","sourceRoot":"","sources":["../../src/codec/g711.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmEH;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CA2BlD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAM5D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CAM9D"}