@absolutejs/voice 0.0.22-beta.632 → 0.0.22-beta.634

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
+ }
@@ -6408,7 +6408,10 @@ var createEmptyCurrentTurn = () => ({
6408
6408
  silenceStartedAt: undefined,
6409
6409
  transcripts: []
6410
6410
  });
6411
- var cloneTranscript = (transcript) => ({ ...transcript });
6411
+ var cloneTranscript = (transcript) => ({
6412
+ ...transcript,
6413
+ words: transcript.words?.map((word) => ({ ...word }))
6414
+ });
6412
6415
  var encodeBase64 = (chunk) => Buffer2.from(chunk).toString("base64");
6413
6416
  var countWords2 = (text) => text.trim().split(/\s+/).filter(Boolean).length;
6414
6417
  var normalizeText2 = (text) => text.trim().replace(/\s+/g, " ");
@@ -6453,9 +6456,12 @@ var calculateMeanConfidence = (transcripts) => {
6453
6456
  }
6454
6457
  return sum / total;
6455
6458
  };
6459
+ var collectTranscriptWords = (transcripts) => transcripts.flatMap((transcript) => transcript.words ?? []);
6460
+ var collectWordConfidences = (transcripts) => collectTranscriptWords(transcripts).map((word) => word.confidence).filter((confidence) => typeof confidence === "number" && Number.isFinite(confidence));
6456
6461
  var createTurnQuality = (transcripts, source, fallbackUsed, fallbackDiagnostics, correctionDiagnostics, costEstimate) => {
6457
6462
  const sampledTranscripts = transcripts.filter((transcript) => typeof transcript.confidence === "number");
6458
6463
  const confidenceSampleCount = sampledTranscripts.length;
6464
+ const wordConfidences = collectWordConfidences(transcripts);
6459
6465
  return {
6460
6466
  averageConfidence: confidenceSampleCount > 0 ? sampledTranscripts.reduce((sum, transcript) => sum + transcript.confidence, 0) / confidenceSampleCount : undefined,
6461
6467
  confidenceSampleCount,
@@ -6466,7 +6472,9 @@ var createTurnQuality = (transcripts, source, fallbackUsed, fallbackDiagnostics,
6466
6472
  finalTranscriptCount: transcripts.filter((transcript) => transcript.isFinal).length,
6467
6473
  partialTranscriptCount: transcripts.filter((transcript) => !transcript.isFinal).length,
6468
6474
  selectedTranscriptCount: transcripts.length,
6469
- source
6475
+ source,
6476
+ lowestWordConfidence: wordConfidences.length > 0 ? Math.min(...wordConfidences) : undefined,
6477
+ wordConfidenceSampleCount: wordConfidences.length
6470
6478
  };
6471
6479
  };
6472
6480
  var createTurnCostEstimate = (input) => {
@@ -6483,20 +6491,38 @@ var createTurnCostEstimate = (input) => {
6483
6491
  };
6484
6492
  };
6485
6493
  var normalizeCorrectionText = (text) => normalizeText2(text);
6486
- var isFallbackNeeded = (candidate, config) => {
6494
+ var evaluateFallbackNeed = (candidate, config) => {
6487
6495
  const trimmed = normalizeText2(candidate.text);
6488
6496
  const wordCount = countWords2(trimmed);
6497
+ const averageConfidence = calculateMeanConfidence(candidate.transcripts);
6498
+ const words = collectTranscriptWords(candidate.transcripts);
6499
+ const wordConfidences = collectWordConfidences(candidate.transcripts);
6500
+ const lowestWordConfidence = wordConfidences.length > 0 ? Math.min(...wordConfidences) : undefined;
6501
+ const resolvedCandidate = {
6502
+ averageConfidence,
6503
+ lowestWordConfidence,
6504
+ text: candidate.text,
6505
+ transcripts: candidate.transcripts,
6506
+ wordCount,
6507
+ words
6508
+ };
6489
6509
  if (config.trigger === "always") {
6490
- return true;
6510
+ return { needed: true, reason: "always" };
6491
6511
  }
6492
- if (config.trigger === "empty-turn") {
6493
- return wordCount < config.minTextLength;
6512
+ if (wordCount < config.minTextLength && (config.trigger === "empty-turn" || config.trigger === "empty-or-low-confidence")) {
6513
+ return { needed: true, reason: "empty-turn" };
6494
6514
  }
6495
- const averageConfidence = calculateMeanConfidence(candidate.transcripts);
6496
- if (config.trigger === "low-confidence") {
6497
- return averageConfidence > 0 && averageConfidence < config.confidenceThreshold;
6515
+ if (config.riskPolicy?.(resolvedCandidate)) {
6516
+ return { needed: true, reason: "risk-policy" };
6517
+ }
6518
+ if (config.wordConfidenceThreshold !== undefined && lowestWordConfidence !== undefined && lowestWordConfidence < config.wordConfidenceThreshold) {
6519
+ return { needed: true, reason: "word-confidence" };
6520
+ }
6521
+ const checksConfidence = config.trigger === "low-confidence" || config.trigger === "empty-or-low-confidence";
6522
+ if (checksConfidence && averageConfidence > 0 && averageConfidence < config.confidenceThreshold) {
6523
+ return { needed: true, reason: "transcript-confidence" };
6498
6524
  }
6499
- return averageConfidence > 0 && averageConfidence < config.confidenceThreshold || wordCount < config.minTextLength;
6525
+ return { needed: false, reason: undefined };
6500
6526
  };
6501
6527
  var selectBetterTurnText = (candidate, fallback) => {
6502
6528
  if (!fallback.text) {
@@ -6615,7 +6641,9 @@ var createVoiceSession = (options) => {
6615
6641
  minTextLength: options.sttFallback.minTextLength ?? DEFAULT_FALLBACK_MIN_TEXT_LENGTH,
6616
6642
  replayWindowMs: options.sttFallback.replayWindowMs ?? DEFAULT_FALLBACK_REPLAY_MS,
6617
6643
  settleMs: options.sttFallback.settleMs ?? DEFAULT_FALLBACK_SETTLE_MS,
6618
- trigger: options.sttFallback.trigger ?? "empty-or-low-confidence"
6644
+ trigger: options.sttFallback.trigger ?? "empty-or-low-confidence",
6645
+ wordConfidenceThreshold: options.sttFallback.wordConfidenceThreshold,
6646
+ riskPolicy: options.sttFallback.riskPolicy
6619
6647
  } : undefined;
6620
6648
  const appendTrace = async (input) => {
6621
6649
  await options.trace?.append({
@@ -7762,7 +7790,8 @@ var createVoiceSession = (options) => {
7762
7790
  text: primaryText,
7763
7791
  transcripts: primaryTranscripts
7764
7792
  };
7765
- if (!isFallbackNeeded(candidate, sttFallback)) {
7793
+ const fallbackNeed = evaluateFallbackNeed(candidate, sttFallback);
7794
+ if (!fallbackNeed.needed) {
7766
7795
  return null;
7767
7796
  }
7768
7797
  fallbackAttemptsForCurrentTurn += 1;
@@ -7889,7 +7918,8 @@ var createVoiceSession = (options) => {
7889
7918
  primaryWordCount: primaryCandidate.wordCount,
7890
7919
  selected: selection.winner.text === fallbackCandidate.text,
7891
7920
  selectionReason: selection.reason,
7892
- trigger: sttFallback.trigger
7921
+ trigger: sttFallback.trigger,
7922
+ triggerReason: fallbackNeed.reason
7893
7923
  };
7894
7924
  if (selection.winner.text === primaryCandidate.text) {
7895
7925
  return {
@@ -10251,7 +10281,9 @@ var resolveBenchmarkFallbackConfig = (config) => {
10251
10281
  minTextLength: config.minTextLength ?? 2,
10252
10282
  replayWindowMs: config.replayWindowMs ?? 8000,
10253
10283
  settleMs: config.settleMs ?? 220,
10254
- trigger: config.trigger ?? "empty-or-low-confidence"
10284
+ trigger: config.trigger ?? "empty-or-low-confidence",
10285
+ wordConfidenceThreshold: config.wordConfidenceThreshold,
10286
+ riskPolicy: config.riskPolicy
10255
10287
  };
10256
10288
  };
10257
10289
  var chunkAudio2 = (audio, bytesPerChunk) => {
@@ -14426,7 +14458,9 @@ var resolveSTTFallbackConfig = (config) => {
14426
14458
  minTextLength: config.minTextLength ?? 2,
14427
14459
  replayWindowMs: config.replayWindowMs ?? 8000,
14428
14460
  settleMs: config.settleMs ?? 220,
14429
- trigger: config.trigger ?? "empty-or-low-confidence"
14461
+ trigger: config.trigger ?? "empty-or-low-confidence",
14462
+ wordConfidenceThreshold: config.wordConfidenceThreshold,
14463
+ riskPolicy: config.riskPolicy
14430
14464
  };
14431
14465
  };
14432
14466
  var normalizePhraseHints = (hints) => (hints ?? []).map((hint) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.632",
3
+ "version": "0.0.22-beta.634",
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
  }