getpatter 0.6.3 → 0.6.4

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 CHANGED
@@ -74,6 +74,7 @@ Every provider reads its credentials from the environment by default. Pass `apiK
74
74
  |---|---|
75
75
  | `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN` | `new Twilio()` carrier |
76
76
  | `TELNYX_API_KEY`, `TELNYX_CONNECTION_ID`, `TELNYX_PUBLIC_KEY` (optional) | `new Telnyx()` carrier |
77
+ | `PLIVO_AUTH_ID`, `PLIVO_AUTH_TOKEN` | `new Plivo()` carrier — Auth Token doubles as the V3 webhook signature key |
77
78
  | `OPENAI_API_KEY` | `OpenAIRealtime`, `WhisperSTT`, `OpenAITTS` |
78
79
  | `ELEVENLABS_API_KEY`, `ELEVENLABS_AGENT_ID` | `ElevenLabsConvAI`, `ElevenLabsTTS` |
79
80
  | `DEEPGRAM_API_KEY` | `DeepgramSTT` |
@@ -92,7 +93,7 @@ cp .env.example .env
92
93
  # Edit .env with your API keys
93
94
  ```
94
95
 
95
- > **Telnyx:** Telnyx is a fully supported telephony provider alternative to Twilio. Both carriers receive equal support for DTMF, transfer, and metrics. Recording parity is supported via Telnyx Call Control; consult the Telnyx portal for configuration details.
96
+ > **Other carriers:** **Telnyx** and **Plivo** are both fully supported alternatives to Twilio. All three carriers receive equal support for inbound DTMF, transfer, AMD, status callbacks, recording, voicemail drop, and metrics. **Plivo** additionally supports native DTMF *send* over the media WebSocket — a capability Twilio Media Streams lacks. Plivo's Auth Token doubles as the V3 webhook signature key (no separate public key, unlike Telnyx Ed25519).
96
97
 
97
98
  ## Voice Modes
98
99
 
@@ -108,7 +109,7 @@ cp .env.example .env
108
109
 
109
110
  ```typescript
110
111
  new Patter({
111
- carrier: Twilio | Telnyx;
112
+ carrier: Twilio | Telnyx | Plivo;
112
113
  phoneNumber: string;
113
114
  webhookUrl?: string; // Public hostname. Mutually exclusive with tunnel.
114
115
  tunnel?: CloudflareTunnel | StaticTunnel | boolean; // `true` is shorthand for new CloudflareTunnel().
@@ -117,7 +118,7 @@ new Patter({
117
118
 
118
119
  | Parameter | Type | Description |
119
120
  |---|---|---|
120
- | `carrier` | `Twilio` / `Telnyx` | Carrier instance. Reads env vars by default. |
121
+ | `carrier` | `Twilio` / `Telnyx` / `Plivo` | Carrier instance. Reads env vars by default. |
121
122
  | `phoneNumber` | `string` | Your phone number in E.164 format. |
122
123
  | `webhookUrl` | `string` | Public hostname your local server is reachable on. |
123
124
  | `tunnel` | `CloudflareTunnel \| StaticTunnel \| boolean` | `new CloudflareTunnel()`, `new StaticTunnel({ hostname: ... })`, or `true` (shorthand for `new CloudflareTunnel()`). |
@@ -179,7 +180,7 @@ await phone.call({
179
180
  ```typescript
180
181
  import {
181
182
  // Carriers
182
- Twilio, Telnyx,
183
+ Twilio, Telnyx, Plivo,
183
184
  // Engines
184
185
  OpenAIRealtime, ElevenLabsConvAI,
185
186
  // STT
@@ -7,6 +7,9 @@ import {
7
7
 
8
8
  // src/carrier-config.ts
9
9
  init_esm_shims();
10
+ function redactPhone(n) {
11
+ return n.slice(0, 3) + "***" + n.slice(-4);
12
+ }
10
13
  var TWILIO_API_BASE = "https://api.twilio.com/2010-04-01";
11
14
  var TELNYX_API_BASE = "https://api.telnyx.com/v2";
12
15
  var PLIVO_API_BASE = "https://api.plivo.com/v1";
@@ -25,7 +28,7 @@ async function configureTwilioNumber(accountSid, authToken, phoneNumber, voiceUr
25
28
  const body = await listResp.json();
26
29
  const match = body.incoming_phone_numbers?.[0];
27
30
  if (!match) {
28
- throw new Error(`Twilio number ${phoneNumber} not found on account ${accountSid}`);
31
+ throw new Error(`Twilio number ${redactPhone(phoneNumber)} not found on account ${accountSid}`);
29
32
  }
30
33
  const updateUrl = `${TWILIO_API_BASE}/Accounts/${accountSid}/IncomingPhoneNumbers/${match.sid}.json`;
31
34
  const form = new URLSearchParams({ VoiceUrl: voiceUrl, VoiceMethod: "POST" });
@@ -44,17 +47,20 @@ async function configureTwilioNumber(accountSid, authToken, phoneNumber, voiceUr
44
47
  }
45
48
  }
46
49
  async function configureTelnyxNumber(apiKey, connectionId, phoneNumber) {
47
- const resp = await fetch(`${TELNYX_API_BASE}/phone_numbers/${encodeURIComponent(phoneNumber)}`, {
48
- method: "PATCH",
49
- headers: {
50
- Authorization: `Bearer ${apiKey}`,
51
- "Content-Type": "application/json"
52
- },
53
- body: JSON.stringify({ connection_id: connectionId })
54
- });
50
+ const resp = await fetch(
51
+ `${TELNYX_API_BASE}/phone_numbers/${encodeURIComponent(phoneNumber)}/voice`,
52
+ {
53
+ method: "PATCH",
54
+ headers: {
55
+ Authorization: `Bearer ${apiKey}`,
56
+ "Content-Type": "application/json"
57
+ },
58
+ body: JSON.stringify({ connection_id: connectionId, tech_prefix_enabled: false })
59
+ }
60
+ );
55
61
  if (!resp.ok) {
56
62
  throw new Error(
57
- `Telnyx PATCH /phone_numbers/${phoneNumber} failed: ${resp.status} ${await resp.text()}`
63
+ `Telnyx PATCH /phone_numbers/${redactPhone(phoneNumber)}/voice failed: ${resp.status} ${await resp.text()}`
58
64
  );
59
65
  }
60
66
  }
@@ -104,7 +110,7 @@ async function autoConfigureCarrier(params) {
104
110
  if (provider === "telnyx" && params.telnyxKey && params.telnyxConnectionId) {
105
111
  try {
106
112
  await configureTelnyxNumber(params.telnyxKey, params.telnyxConnectionId, params.phoneNumber);
107
- log.info("Telnyx number %s associated with connection %s", params.phoneNumber, params.telnyxConnectionId);
113
+ log.info("Telnyx number ***%s associated with connection %s", params.phoneNumber.slice(-4), params.telnyxConnectionId);
108
114
  } catch (err) {
109
115
  log.warn("Could not auto-configure Telnyx number: %s", err instanceof Error ? err.message : String(err));
110
116
  }
@@ -193,6 +193,8 @@ var SileroVAD = class _SileroVAD {
193
193
  speechThresholdDuration = 0;
194
194
  silenceThresholdDuration = 0;
195
195
  closed = false;
196
+ /** Transitions produced in the current processFrame call but not yet returned. */
197
+ eventQueue = [];
196
198
  /**
197
199
  * Load the Silero VAD model.
198
200
  * Throws if `onnxruntime-node` is not installed.
@@ -318,22 +320,21 @@ var SileroVAD = class _SileroVAD {
318
320
  );
319
321
  }
320
322
  if (pcmChunk.length === 0) {
321
- return null;
323
+ return this.eventQueue.shift() ?? null;
322
324
  }
323
325
  const numSamples = Math.floor(pcmChunk.length / 2);
324
326
  if (numSamples === 0) {
325
- return null;
327
+ return this.eventQueue.shift() ?? null;
326
328
  }
327
329
  const samples = new Float32Array(numSamples);
328
330
  for (let i = 0; i < numSamples; i++) {
329
- samples[i] = pcmChunk.readInt16LE(i * 2) / 32767;
331
+ samples[i] = pcmChunk.readInt16LE(i * 2) / 32768;
330
332
  }
331
333
  const merged = new Float32Array(this.pending.length + samples.length);
332
334
  merged.set(this.pending, 0);
333
335
  merged.set(samples, this.pending.length);
334
336
  this.pending = merged;
335
337
  const windowSize = this.model.windowSizeSamples;
336
- let event = null;
337
338
  while (this.pending.length >= windowSize) {
338
339
  const window = this.pending.slice(0, windowSize);
339
340
  this.pending = this.pending.slice(windowSize);
@@ -342,10 +343,10 @@ var SileroVAD = class _SileroVAD {
342
343
  const windowDuration = windowSize / this.opts.sampleRate;
343
344
  const transition = this.advanceState(p, windowDuration);
344
345
  if (transition !== null) {
345
- event = transition;
346
+ this.eventQueue.push(transition);
346
347
  }
347
348
  }
348
- return event;
349
+ return this.eventQueue.shift() ?? null;
349
350
  }
350
351
  advanceState(p, windowDuration) {
351
352
  const opts = this.opts;
@@ -400,6 +401,7 @@ var SileroVAD = class _SileroVAD {
400
401
  this.pubSpeaking = false;
401
402
  this.speechThresholdDuration = 0;
402
403
  this.silenceThresholdDuration = 0;
404
+ this.eventQueue = [];
403
405
  this.expFilter.reset();
404
406
  this.model.reset();
405
407
  }