@absolutejs/voice 0.0.22-beta.631 → 0.0.22-beta.633

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.
@@ -0,0 +1,385 @@
1
+ {
2
+ "contract": 1,
3
+ "identity": {
4
+ "accent": "#f43f5e",
5
+ "category": "voice",
6
+ "description": "Self-hosted voice assistant runtime for Elysia: browser and phone-call sessions over WebSockets, pluggable speech-to-text (`voice/stt`), text-to-speech (`voice/tts`), and speech-to-speech (`voice/realtime`) vendor adapters, turn detection, barge-in, greetings, reconnect recovery, session stores (`voice/session-store`), traces, evals, and live-monitor surfaces — the orchestration hosted voice platforms keep behind their dashboards, inside your own server.",
7
+ "docsUrl": "https://github.com/absolutejs/voice",
8
+ "name": "@absolutejs/voice",
9
+ "tagline": "Let people talk to your site and get spoken answers."
10
+ },
11
+ "implements": [
12
+ {
13
+ "contract": "voice/session-store",
14
+ "factory": "createVoiceMemoryStore",
15
+ "from": "@absolutejs/voice",
16
+ "title": "In memory (development only — calls are forgotten on restart)",
17
+ "wiring": {
18
+ "code": "createVoiceMemoryStore()",
19
+ "imports": [
20
+ {
21
+ "from": "@absolutejs/voice",
22
+ "names": [
23
+ "createVoiceMemoryStore"
24
+ ]
25
+ }
26
+ ]
27
+ }
28
+ },
29
+ {
30
+ "contract": "voice/session-store",
31
+ "factory": "createVoiceSQLiteSessionStore",
32
+ "from": "@absolutejs/voice",
33
+ "settings": {
34
+ "type": "object",
35
+ "required": [
36
+ "path"
37
+ ],
38
+ "properties": {
39
+ "path": {
40
+ "description": "Database file on this machine where call sessions are kept. Created if missing.",
41
+ "examples": [
42
+ "./var/voice-sessions.sqlite"
43
+ ],
44
+ "title": "Database file",
45
+ "type": "string"
46
+ },
47
+ "tableName": {
48
+ "description": "Table the sessions are stored in.",
49
+ "title": "Table name",
50
+ "type": "string"
51
+ }
52
+ }
53
+ },
54
+ "title": "SQLite file on this machine",
55
+ "wiring": {
56
+ "code": "createVoiceSQLiteSessionStore(${settings})",
57
+ "imports": [
58
+ {
59
+ "from": "@absolutejs/voice",
60
+ "names": [
61
+ "createVoiceSQLiteSessionStore"
62
+ ]
63
+ }
64
+ ]
65
+ }
66
+ },
67
+ {
68
+ "contract": "voice/session-store",
69
+ "factory": "createVoicePostgresSessionStore",
70
+ "from": "@absolutejs/voice",
71
+ "requires": {
72
+ "env": [
73
+ {
74
+ "description": "Postgres connection string (call sessions are stored here)",
75
+ "example": "postgres://user:pass@host/db",
76
+ "key": "DATABASE_URL",
77
+ "secret": true
78
+ }
79
+ ],
80
+ "services": [
81
+ {
82
+ "description": "Stores call sessions durably",
83
+ "id": "postgres"
84
+ }
85
+ ]
86
+ },
87
+ "settings": {
88
+ "type": "object",
89
+ "properties": {
90
+ "schemaName": {
91
+ "description": "Postgres schema the table lives in.",
92
+ "title": "Schema name",
93
+ "type": "string"
94
+ },
95
+ "tableName": {
96
+ "description": "Table the sessions are stored in.",
97
+ "title": "Table name",
98
+ "type": "string"
99
+ }
100
+ }
101
+ },
102
+ "title": "Postgres",
103
+ "wiring": {
104
+ "code": "createVoicePostgresSessionStore({ connectionString: ${env.DATABASE_URL} ?? \"\", ...${settings} })",
105
+ "imports": [
106
+ {
107
+ "from": "@absolutejs/voice",
108
+ "names": [
109
+ "createVoicePostgresSessionStore"
110
+ ]
111
+ }
112
+ ]
113
+ }
114
+ },
115
+ {
116
+ "contract": "voice/session-store",
117
+ "factory": "createVoiceFileSessionStore",
118
+ "from": "@absolutejs/voice",
119
+ "settings": {
120
+ "type": "object",
121
+ "required": [
122
+ "directory"
123
+ ],
124
+ "properties": {
125
+ "directory": {
126
+ "description": "Folder on this machine where each call session is kept as a JSON file. Created if missing.",
127
+ "examples": [
128
+ "./var/voice-sessions"
129
+ ],
130
+ "title": "Storage folder",
131
+ "type": "string"
132
+ }
133
+ }
134
+ },
135
+ "title": "JSON files on this machine",
136
+ "wiring": {
137
+ "code": "createVoiceFileSessionStore(${settings})",
138
+ "imports": [
139
+ {
140
+ "from": "@absolutejs/voice",
141
+ "names": [
142
+ "createVoiceFileSessionStore"
143
+ ]
144
+ }
145
+ ]
146
+ }
147
+ }
148
+ ],
149
+ "requires": {
150
+ "peers": [
151
+ {
152
+ "name": "elysia",
153
+ "range": ">= 1.4.18",
154
+ "reason": "plugin host"
155
+ }
156
+ ]
157
+ },
158
+ "settings": {
159
+ "type": "object",
160
+ "properties": {
161
+ "backchannelBargeInGuard": {
162
+ "description": "Keep the assistant talking through listening cues like \"mm-hm\" or \"right\" instead of treating them as interruptions.",
163
+ "title": "Ignore listening cues",
164
+ "type": "boolean"
165
+ },
166
+ "greeting": {
167
+ "description": "Spoken by the assistant the moment a call connects, before the caller says anything.",
168
+ "examples": [
169
+ "Hi! How can I help you today?"
170
+ ],
171
+ "title": "Opening greeting",
172
+ "type": "string"
173
+ },
174
+ "normalizeNumbers": {
175
+ "description": "Rewrite spoken numbers to digits in transcripts (\"ten million\" becomes \"10 million\") so downstream systems read them consistently.",
176
+ "title": "Write numbers as digits",
177
+ "type": "boolean"
178
+ },
179
+ "path": {
180
+ "default": "/voice/realtime",
181
+ "description": "The WebSocket route callers connect to.",
182
+ "title": "Call route",
183
+ "type": "string"
184
+ },
185
+ "preset": {
186
+ "description": "Tuned bundle of turn-taking and latency defaults for the kind of calls you take — casual chat, guided intake, dictation, noisy rooms, or phone lines.",
187
+ "title": "Conversation preset",
188
+ "anyOf": [
189
+ {
190
+ "const": "default",
191
+ "type": "string"
192
+ },
193
+ {
194
+ "const": "chat",
195
+ "type": "string"
196
+ },
197
+ {
198
+ "const": "guided-intake",
199
+ "type": "string"
200
+ },
201
+ {
202
+ "const": "dictation",
203
+ "type": "string"
204
+ },
205
+ {
206
+ "const": "noisy-room",
207
+ "type": "string"
208
+ },
209
+ {
210
+ "const": "pstn-balanced",
211
+ "type": "string"
212
+ },
213
+ {
214
+ "const": "pstn-fast",
215
+ "type": "string"
216
+ },
217
+ {
218
+ "const": "reliability",
219
+ "type": "string"
220
+ }
221
+ ]
222
+ },
223
+ "resumeGreeting": {
224
+ "description": "Spoken when a dropped call reconnects, so the caller knows they are back.",
225
+ "examples": [
226
+ "Sorry, we got cut off — where were we?"
227
+ ],
228
+ "title": "Reconnect greeting",
229
+ "type": "string"
230
+ }
231
+ }
232
+ },
233
+ "slots": {
234
+ "realtime": {
235
+ "configPath": "realtime",
236
+ "contract": "voice/realtime",
237
+ "description": "One speech-to-speech model that listens and talks in a single stream — an alternative to picking separate speech-to-text and text-to-speech services.",
238
+ "known": [
239
+ "@absolutejs/voice-openai",
240
+ "@absolutejs/voice-gemini"
241
+ ]
242
+ },
243
+ "session": {
244
+ "configPath": "session",
245
+ "contract": "voice/session-store",
246
+ "description": "Where each call's transcript and state is kept while it runs (and after, for review)",
247
+ "known": [
248
+ "@absolutejs/voice#memory",
249
+ "@absolutejs/voice#sqlite",
250
+ "@absolutejs/voice#postgres",
251
+ "@absolutejs/voice#file"
252
+ ],
253
+ "required": true
254
+ },
255
+ "stt": {
256
+ "configPath": "stt",
257
+ "contract": "voice/stt",
258
+ "description": "The service that turns the caller's speech into text. Not needed when a speech-to-speech (realtime) adapter is chosen instead.",
259
+ "known": [
260
+ "@absolutejs/voice-deepgram",
261
+ "@absolutejs/voice-assemblyai",
262
+ "@absolutejs/voice-gladia",
263
+ "@absolutejs/voice-soniox",
264
+ "@absolutejs/voice-speechmatics",
265
+ "@absolutejs/voice-azure",
266
+ "@absolutejs/voice-google-speech",
267
+ "@absolutejs/voice-openai-whisper"
268
+ ]
269
+ },
270
+ "tts": {
271
+ "configPath": "tts",
272
+ "contract": "voice/tts",
273
+ "description": "The service that speaks the assistant's replies out loud. Not needed when a speech-to-speech (realtime) adapter is chosen instead.",
274
+ "known": [
275
+ "@absolutejs/voice-elevenlabs",
276
+ "@absolutejs/voice-cartesia",
277
+ "@absolutejs/voice-azure",
278
+ "@absolutejs/voice-lmnt",
279
+ "@absolutejs/voice-playht",
280
+ "@absolutejs/voice-rime",
281
+ "@absolutejs/voice-smallest",
282
+ "@absolutejs/voice-neets"
283
+ ]
284
+ }
285
+ },
286
+ "wiring": [
287
+ {
288
+ "description": "Pick a speech-to-text and a text-to-speech service, choose where call sessions are stored, and write the onTurn handler — the assistant's brain.",
289
+ "id": "default",
290
+ "server": {
291
+ "code": "// Every call's transcript and state lives in the session store.\nconst voiceSessions = ${slot.session};\n\n// Mount with .use(voiceAssistant); callers connect over WebSocket at `path`.\nconst voiceAssistant = voice({\n\tonTurn: async (session, turn) => {\n\t\t// TODO: generate the assistant's reply (call your LLM here).\n\t\treturn { assistantText: \"You said: \" + turn.text };\n\t},\n\tsession: voiceSessions,\n\tstt: ${slot.stt},\n\ttts: ${slot.tts},\n\t...${settings}\n});",
292
+ "imports": [
293
+ {
294
+ "from": "@absolutejs/voice",
295
+ "names": [
296
+ "voice"
297
+ ]
298
+ }
299
+ ],
300
+ "placement": "module-scope"
301
+ },
302
+ "title": "Voice assistant with separate STT + TTS"
303
+ },
304
+ {
305
+ "description": "Use one speech-to-speech model (OpenAI Realtime, Gemini Live) that listens and talks in a single stream instead of separate STT and TTS services.",
306
+ "id": "realtime",
307
+ "server": {
308
+ "code": "// Every call's transcript and state lives in the session store.\nconst voiceSessions = ${slot.session};\n\n// Mount with .use(voiceAssistant); callers connect over WebSocket at `path`.\nconst voiceAssistant = voice({\n\tonTurn: async (session, turn) => {\n\t\t// TODO: generate the assistant's reply (call your LLM here).\n\t\treturn { assistantText: \"You said: \" + turn.text };\n\t},\n\trealtime: ${slot.realtime},\n\tsession: voiceSessions,\n\t...${settings}\n});",
309
+ "imports": [
310
+ {
311
+ "from": "@absolutejs/voice",
312
+ "names": [
313
+ "voice"
314
+ ]
315
+ }
316
+ ],
317
+ "placement": "module-scope"
318
+ },
319
+ "title": "Voice assistant with a speech-to-speech model"
320
+ }
321
+ ],
322
+ "tools": {
323
+ "delete_session": {
324
+ "annotations": {
325
+ "destructiveHint": true,
326
+ "idempotentHint": true
327
+ },
328
+ "description": "Delete one call session (its transcript and state) by id. Deleting an unknown id succeeds.",
329
+ "input": {
330
+ "type": "object",
331
+ "required": [
332
+ "id"
333
+ ],
334
+ "properties": {
335
+ "id": {
336
+ "minLength": 1,
337
+ "type": "string"
338
+ }
339
+ }
340
+ },
341
+ "kind": "runtime"
342
+ },
343
+ "get_session": {
344
+ "annotations": {
345
+ "readOnlyHint": true
346
+ },
347
+ "description": "Fetch one call session by id: status, timing, and the committed conversation turns (caller text and assistant reply), most recent last.",
348
+ "input": {
349
+ "type": "object",
350
+ "required": [
351
+ "id"
352
+ ],
353
+ "properties": {
354
+ "id": {
355
+ "minLength": 1,
356
+ "type": "string"
357
+ },
358
+ "maxTurns": {
359
+ "maximum": 200,
360
+ "minimum": 1,
361
+ "type": "integer"
362
+ }
363
+ }
364
+ },
365
+ "kind": "runtime"
366
+ },
367
+ "list_sessions": {
368
+ "annotations": {
369
+ "readOnlyHint": true
370
+ },
371
+ "description": "List call sessions with status, creation time, last activity, and turn count — most recently active first.",
372
+ "input": {
373
+ "type": "object",
374
+ "properties": {
375
+ "limit": {
376
+ "maximum": 200,
377
+ "minimum": 1,
378
+ "type": "integer"
379
+ }
380
+ }
381
+ },
382
+ "kind": "runtime"
383
+ }
384
+ }
385
+ }
@@ -6751,6 +6751,9 @@ var createVoiceSession = (options) => {
6751
6751
  if (callSilenceTimeoutMs === undefined || callSilenceFired) {
6752
6752
  return;
6753
6753
  }
6754
+ if (callerPaused) {
6755
+ return;
6756
+ }
6754
6757
  clearCallSilenceWatchdog();
6755
6758
  callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
6756
6759
  };
@@ -6796,6 +6799,9 @@ var createVoiceSession = (options) => {
6796
6799
  if (stuckCloseAfterMs === undefined || stuckCloseFired) {
6797
6800
  return;
6798
6801
  }
6802
+ if (callerPaused) {
6803
+ return;
6804
+ }
6799
6805
  clearStuckCloseWatchdog();
6800
6806
  stuckCloseWatchdog = setTimeout(fireStuckClose, stuckCloseAfterMs);
6801
6807
  };
@@ -6818,6 +6824,9 @@ var createVoiceSession = (options) => {
6818
6824
  idleRepromptsUsed = 0;
6819
6825
  }
6820
6826
  clearIdleRepromptWatchdog();
6827
+ if (callerPaused) {
6828
+ return;
6829
+ }
6821
6830
  if (idleRepromptsUsed >= idleRepromptMax) {
6822
6831
  return;
6823
6832
  }
@@ -6838,6 +6847,60 @@ var createVoiceSession = (options) => {
6838
6847
  });
6839
6848
  }, idleRepromptAfterMs);
6840
6849
  };
6850
+ const PAUSE_MAX_MS_DEFAULT = 600000;
6851
+ const pauseMaxMs = options.pause?.maxMs && options.pause.maxMs > 0 ? options.pause.maxMs : PAUSE_MAX_MS_DEFAULT;
6852
+ let callerPaused = false;
6853
+ let pauseTimeout = null;
6854
+ const clearPauseTimeout = () => {
6855
+ if (pauseTimeout) {
6856
+ clearTimeout(pauseTimeout);
6857
+ pauseTimeout = null;
6858
+ }
6859
+ };
6860
+ const pauseInternal = async () => {
6861
+ if (callerPaused) {
6862
+ return;
6863
+ }
6864
+ const snapshot = await readSession();
6865
+ if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
6866
+ return;
6867
+ }
6868
+ callerPaused = true;
6869
+ clearCallSilenceWatchdog();
6870
+ clearStuckCloseWatchdog();
6871
+ clearIdleRepromptWatchdog();
6872
+ clearPauseTimeout();
6873
+ pauseTimeout = setTimeout(() => {
6874
+ pauseTimeout = null;
6875
+ api.close("pause-timeout");
6876
+ }, pauseMaxMs);
6877
+ await appendTrace({
6878
+ payload: {
6879
+ action: "call.paused",
6880
+ maxMs: pauseMaxMs
6881
+ },
6882
+ session: snapshot,
6883
+ type: "call.lifecycle"
6884
+ });
6885
+ };
6886
+ const resumeInternal = async () => {
6887
+ if (!callerPaused) {
6888
+ return;
6889
+ }
6890
+ callerPaused = false;
6891
+ clearPauseTimeout();
6892
+ kickCallSilenceWatchdog();
6893
+ kickStuckCloseWatchdog();
6894
+ kickIdleRepromptWatchdog(true);
6895
+ const snapshot = await readSession();
6896
+ await appendTrace({
6897
+ payload: {
6898
+ action: "call.resumed"
6899
+ },
6900
+ session: snapshot,
6901
+ type: "call.lifecycle"
6902
+ });
6903
+ };
6841
6904
  const recordingConfig = options.recording;
6842
6905
  const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
6843
6906
  const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
@@ -7345,6 +7408,7 @@ var createVoiceSession = (options) => {
7345
7408
  clearCallSilenceWatchdog();
7346
7409
  clearStuckCloseWatchdog();
7347
7410
  clearIdleRepromptWatchdog();
7411
+ clearPauseTimeout();
7348
7412
  clearAmdEvaluationTimer();
7349
7413
  await closeTTSSession("failed");
7350
7414
  await closeAdapter("failed");
@@ -7456,6 +7520,7 @@ var createVoiceSession = (options) => {
7456
7520
  clearCallSilenceWatchdog();
7457
7521
  clearStuckCloseWatchdog();
7458
7522
  clearIdleRepromptWatchdog();
7523
+ clearPauseTimeout();
7459
7524
  clearAmdEvaluationTimer();
7460
7525
  await closeTTSSession("complete");
7461
7526
  await closeAdapter("complete");
@@ -8416,6 +8481,19 @@ var createVoiceSession = (options) => {
8416
8481
  }
8417
8482
  kickStuckCloseWatchdog();
8418
8483
  kickIdleRepromptWatchdog(true);
8484
+ if (callerPaused) {
8485
+ await appendTrace({
8486
+ payload: {
8487
+ action: "turn.skipped",
8488
+ reason: "caller-paused",
8489
+ status: "skipped"
8490
+ },
8491
+ session,
8492
+ turnId: turn.id,
8493
+ type: "call.lifecycle"
8494
+ });
8495
+ return;
8496
+ }
8419
8497
  const liveOpsControl = await options.liveOps?.getControl(options.id);
8420
8498
  if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
8421
8499
  await appendTrace({
@@ -9266,6 +9344,7 @@ var createVoiceSession = (options) => {
9266
9344
  clearCallSilenceWatchdog();
9267
9345
  clearStuckCloseWatchdog();
9268
9346
  clearIdleRepromptWatchdog();
9347
+ clearPauseTimeout();
9269
9348
  clearAmdEvaluationTimer();
9270
9349
  if (options.noiseSuppressor?.close) {
9271
9350
  try {
@@ -9331,9 +9410,15 @@ var createVoiceSession = (options) => {
9331
9410
  markVoicemail: async (input) => runSerial("api.markVoicemail", async () => {
9332
9411
  await markVoicemailInternal(input);
9333
9412
  }),
9413
+ pause: async () => runSerial("api.pause", async () => {
9414
+ await pauseInternal();
9415
+ }),
9334
9416
  receiveAudio: async (audio) => runSerial("api.receiveAudio", async () => {
9335
9417
  await receiveAudioInternal(audio);
9336
9418
  }),
9419
+ resume: async () => runSerial("api.resume", async () => {
9420
+ await resumeInternal();
9421
+ }),
9337
9422
  snapshot: async () => runSerial("api.snapshot", async () => readSession()),
9338
9423
  transfer: async (input) => runSerial("api.transfer", async () => {
9339
9424
  await transferInternal(input);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.631",
3
+ "version": "0.0.22-beta.633",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,7 @@
19
19
  "license": "BSL-1.1",
20
20
  "author": "Alex Kahn",
21
21
  "scripts": {
22
- "build": "rm -rf dist && bun build ./src/index.ts ./src/client/index.ts ./src/react/index.ts ./src/vue/index.ts ./src/svelte/index.ts ./src/angular/index.ts ./src/testing/index.ts ./src/drizzle/index.ts --outdir dist --target bun --external elysia --external react --external vue --external @angular/core --external @absolutejs/absolute --external @absolutejs/ai --external @absolutejs/media --external drizzle-orm && bun build ./src/client/htmxBootstrap.ts --outdir dist/client --target browser --format esm && bun build ./src/embed/index.ts --outfile dist/embed/voice-widget.js --target browser --format iife --minify && bun build ./src/embed/index.ts --outdir dist/embed --target browser --format esm && tsc --emitDeclarationOnly --project tsconfig.json",
22
+ "build": "rm -rf dist && bun build ./src/index.ts ./src/manifest.ts ./src/client/index.ts ./src/react/index.ts ./src/vue/index.ts ./src/svelte/index.ts ./src/angular/index.ts ./src/testing/index.ts ./src/drizzle/index.ts --outdir dist --root ./src --target bun --external elysia --external react --external vue --external @angular/core --external @absolutejs/absolute --external @absolutejs/ai --external @absolutejs/media --external drizzle-orm && bun build ./src/client/htmxBootstrap.ts --outdir dist/client --target browser --format esm && bun build ./src/embed/index.ts --outfile dist/embed/voice-widget.js --target browser --format iife --minify && bun build ./src/embed/index.ts --outdir dist/embed --target browser --format esm && tsc --emitDeclarationOnly --project tsconfig.json && absolute-manifest emit",
23
23
  "config": "absolute config",
24
24
  "format": "absolute prettier --write",
25
25
  "knip": "knip",
@@ -75,7 +75,12 @@
75
75
  "import": "./dist/embed/index.js",
76
76
  "types": "./dist/embed/index.d.ts"
77
77
  },
78
- "./embed/voice-widget.js": "./dist/embed/voice-widget.js"
78
+ "./embed/voice-widget.js": "./dist/embed/voice-widget.js",
79
+ "./manifest": {
80
+ "import": "./dist/manifest.js",
81
+ "types": "./dist/manifest.d.ts"
82
+ },
83
+ "./manifest.json": "./dist/manifest.json"
79
84
  },
80
85
  "typesVersions": {
81
86
  "*": {
@@ -102,6 +107,9 @@
102
107
  ],
103
108
  "embed": [
104
109
  "dist/embed/index.d.ts"
110
+ ],
111
+ "manifest": [
112
+ "dist/manifest.d.ts"
105
113
  ]
106
114
  }
107
115
  },
@@ -132,7 +140,9 @@
132
140
  }
133
141
  },
134
142
  "dependencies": {
135
- "@absolutejs/media": "0.0.1-beta.19"
143
+ "@absolutejs/manifest": "^0.1.0",
144
+ "@absolutejs/media": "0.0.1-beta.19",
145
+ "@sinclair/typebox": "^0.34.0"
136
146
  },
137
147
  "devDependencies": {
138
148
  "@absolutejs/absolute": "0.19.0-beta.1009",
@@ -157,5 +167,8 @@
157
167
  "typescript": "^5.9.3",
158
168
  "typescript-eslint": "^8.57.2",
159
169
  "vue": "3.5.27"
170
+ },
171
+ "absolutejs": {
172
+ "manifestContract": 1
160
173
  }
161
174
  }