@seams/voiceid 0.0.0-stage → 0.24.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.
Files changed (65) hide show
  1. package/LICENSE.md +201 -0
  2. package/README.md +74 -2
  3. package/THIRD_PARTY_NOTICES.md +504 -0
  4. package/dist/compose-host.mjs +11 -0
  5. package/dist/esm/browser/ribbon.js +489 -0
  6. package/dist/esm/capture/prompt.js +515 -0
  7. package/dist/esm/capture/visual.js +87 -0
  8. package/dist/esm/descriptor.js +7 -0
  9. package/dist/esm/index.js +2 -0
  10. package/dist/esm/react/VoiceAssistantCard.css +313 -0
  11. package/dist/esm/react/VoiceAssistantCard.js +147 -0
  12. package/dist/esm/react.js +3 -0
  13. package/dist/hosting.d.mts +4 -0
  14. package/dist/hosting.mjs +91 -0
  15. package/dist/public/sdk/voiceid/AURORA_LICENSE.txt +202 -0
  16. package/dist/public/sdk/voiceid/NOTICE.txt +54 -0
  17. package/dist/public/sdk/voiceid/SILERO_LICENSE.txt +21 -0
  18. package/dist/public/sdk/voiceid/addVoiceId-CeBXVHqs.js +1 -0
  19. package/dist/public/sdk/voiceid/approval-CCbdLhTv.js +1 -0
  20. package/dist/public/sdk/voiceid/aurora-int8.onnx +0 -0
  21. package/dist/public/sdk/voiceid/deviceBridge-wRAbqdap.js +1 -0
  22. package/dist/public/sdk/voiceid/enrollment-DIRoCkx5.js +1 -0
  23. package/dist/public/sdk/voiceid/host.js +80 -0
  24. package/dist/public/sdk/voiceid/ort-wasm-simd-threaded.mjs +59 -0
  25. package/dist/public/sdk/voiceid/ort-wasm-simd-threaded.wasm +0 -0
  26. package/dist/public/sdk/voiceid/ort.wasm.bundle.min-7y-hlRN8.js +3 -0
  27. package/dist/public/sdk/voiceid/prompt-CCrXTRRx.js +606 -0
  28. package/dist/public/sdk/voiceid/redimnet-b1.onnx +0 -0
  29. package/dist/public/sdk/voiceid/silero-vad.onnx +0 -0
  30. package/dist/public/sdk/voiceid/unlock-E7YHqBGT.js +1 -0
  31. package/dist/public/sdk/voiceid/voice-activity-worklet.js +21 -0
  32. package/dist/types/activation/addVoiceId.d.ts +11 -0
  33. package/dist/types/activation/manageEnrollment.d.ts +4 -0
  34. package/dist/types/activation/ownerConfirmation.d.ts +17 -0
  35. package/dist/types/browser/deviceStore.d.ts +19 -0
  36. package/dist/types/browser/enrollment.d.ts +37 -0
  37. package/dist/types/browser/enrollmentStore.d.ts +91 -0
  38. package/dist/types/browser/inference.d.ts +39 -0
  39. package/dist/types/browser/microphone.d.ts +61 -0
  40. package/dist/types/browser/model.d.ts +8 -0
  41. package/dist/types/browser/phrase.d.ts +35 -0
  42. package/dist/types/browser/phraseVocabulary.d.ts +3 -0
  43. package/dist/types/browser/progress.d.ts +2 -0
  44. package/dist/types/browser/ribbon.d.ts +63 -0
  45. package/dist/types/browser/verificationError.d.ts +6 -0
  46. package/dist/types/browser/voiceActivity.d.ts +15 -0
  47. package/dist/types/capture/approval.d.ts +36 -0
  48. package/dist/types/capture/bridge.d.ts +12 -0
  49. package/dist/types/capture/deviceBridge.d.ts +29 -0
  50. package/dist/types/capture/prompt.d.ts +41 -0
  51. package/dist/types/capture/surface.d.ts +27 -0
  52. package/dist/types/capture/unlockFrame.d.ts +13 -0
  53. package/dist/types/capture/visual.d.ts +24 -0
  54. package/dist/types/descriptor.d.ts +4 -0
  55. package/dist/types/host.d.ts +4 -0
  56. package/dist/types/index.d.ts +2 -0
  57. package/dist/types/react/VoiceAssistantCard.d.ts +37 -0
  58. package/dist/types/react.d.ts +5 -0
  59. package/dist/types/session/activation.d.ts +61 -0
  60. package/dist/types/session/activationStore.d.ts +21 -0
  61. package/dist/types/session/httpClient.d.ts +39 -0
  62. package/dist/types/session/unlock.d.ts +4 -0
  63. package/dist/types/session/unlockFromDevice.d.ts +18 -0
  64. package/package.json +66 -4
  65. package/src/README.md +270 -0
@@ -0,0 +1,515 @@
1
+ import { VoiceVisual } from "./visual.js";
2
+ import { createCspStylesheetManager, getDefaultCspNonce } from "@seams/wallet/voiceid-host";
3
+ //#region src/capture/prompt.ts
4
+ const CSS = `
5
+ .seams-voiceid-activation-surface { width: min(100%, 26.25rem); margin: 0 auto; background: transparent; }
6
+
7
+ .seams-voiceid-prompt {
8
+ box-sizing: border-box;
9
+ position: relative;
10
+ width: min(100%, 26.25rem);
11
+ margin: 0 auto;
12
+ padding: 3.5rem 1.5rem 1.5rem;
13
+ display: grid;
14
+ grid-template-rows: auto minmax(6rem, auto) 5rem minmax(3.375rem, auto) 5rem;
15
+ border: 1px solid var(--seams-colors-borderPrimary, #303b41);
16
+ border-radius: var(--seams-shape-card, 16px);
17
+ background: var(--seams-colors-colorBackground, #192024);
18
+ color: var(--seams-colors-textPrimary, #eff2f3);
19
+ text-align: center;
20
+ font-family: var(--seams-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
21
+ -webkit-font-smoothing: antialiased;
22
+ }
23
+ .seams-voiceid-prompt {
24
+ transition: opacity 180ms var(--seams-ease-out, cubic-bezier(0.23, 1, 0.32, 1)), transform 180ms var(--seams-ease-out, cubic-bezier(0.23, 1, 0.32, 1));
25
+ }
26
+ @starting-style {
27
+ .seams-voiceid-prompt { opacity: 0; transform: translateY(4px); }
28
+ }
29
+ .seams-voiceid-progress-slot { grid-row: 5; grid-column: 1; min-width: 0; align-self: center; }
30
+ .seams-voiceid-progress-slot:empty { visibility: hidden; }
31
+ .seams-voiceid-activation-surface[data-preparation="loading"] .seams-voiceid-record-slot { visibility: hidden; }
32
+ @media (prefers-reduced-motion: reduce) {
33
+ .seams-voiceid-prompt { transform: none; transition: opacity 150ms ease; }
34
+ @starting-style { .seams-voiceid-prompt { transform: none; } }
35
+ .seams-voiceid-ribbon { transition: none !important; }
36
+ }
37
+ .seams-voiceid-prompt h2 {
38
+ font-size: 0.85rem;
39
+ line-height: 1.4;
40
+ font-weight: 500;
41
+ color: var(--seams-colors-textSecondary, #9cabb3);
42
+ margin: 0.5rem 0 1rem;
43
+ }
44
+ .seams-voiceid-prompt.seams-voiceid-management {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 0.75rem;
48
+ padding: 1.25rem 1.25rem 1.375rem;
49
+ border-radius: var(--seams-shape-card, 1.5rem);
50
+ text-align: start;
51
+ font-size: 0.875rem;
52
+ line-height: 1.5;
53
+ }
54
+ .seams-voiceid-management-header {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 0.25rem;
58
+ margin-inline-start: -0.5rem;
59
+ }
60
+ .seams-voiceid-management h2 {
61
+ margin: 0;
62
+ color: var(--seams-colors-textPrimary, #eff2f3);
63
+ font-size: 1.375rem;
64
+ font-weight: 600;
65
+ letter-spacing: -0.02em;
66
+ line-height: 1.25;
67
+ }
68
+ .seams-voiceid-management .seams-voiceid-management-intro {
69
+ margin: -0.125rem 0 0.25rem;
70
+ color: var(--seams-colors-textSecondary, #9cabb3);
71
+ }
72
+ .seams-voiceid-management button {
73
+ appearance: none;
74
+ margin: 0;
75
+ color: var(--seams-colors-textPrimary, #eff2f3);
76
+ font: inherit;
77
+ transition: background-color 140ms ease;
78
+ }
79
+ .seams-voiceid-management button:disabled { opacity: 0.5; cursor: default; }
80
+ .seams-voiceid-management .seams-voiceid-management-back {
81
+ display: grid;
82
+ place-items: center;
83
+ width: 2.25rem;
84
+ height: 2.25rem;
85
+ padding: 0;
86
+ border: 0;
87
+ border-radius: var(--seams-shape-control, 10px);
88
+ background: transparent;
89
+ }
90
+ .seams-voiceid-management-enrollment {
91
+ display: grid;
92
+ grid-template-columns: minmax(0, 1fr) auto;
93
+ align-items: center;
94
+ gap: 0.125rem 0.75rem;
95
+ padding: 0.75rem 0.875rem;
96
+ border-radius: 0.75rem;
97
+ background: var(--seams-colors-surface, #202b31);
98
+ color: var(--seams-colors-textSecondary, #9cabb3);
99
+ }
100
+ .seams-voiceid-management-summary { text-align: end; }
101
+ .seams-voiceid-management-value { color: var(--seams-colors-textPrimary, #eff2f3); font-weight: 500; }
102
+ .seams-voiceid-management-enrollment-detail:not(:empty)::before { content: ' · '; }
103
+ .seams-voiceid-management-enrollment[data-enrollment="adapted"] > .seams-voiceid-management-summary {
104
+ grid-area: 2 / 1;
105
+ text-align: start;
106
+ font-size: 0.8125rem;
107
+ }
108
+ .seams-voiceid-management-enrollment > .seams-voiceid-management-reset { grid-area: 1 / 2 / 3; }
109
+ .seams-voiceid-management-enrollment[data-state] .seams-voiceid-management-value { display: none; }
110
+ .seams-voiceid-management-enrollment[data-state] .seams-voiceid-management-enrollment-detail::before { content: none; }
111
+ .seams-voiceid-management .seams-voiceid-management-reset {
112
+ padding: 0.375rem 0.75rem;
113
+ border: 1px solid var(--seams-colors-borderPrimary, #303b41);
114
+ border-radius: var(--seams-shape-control, 10px);
115
+ background: transparent;
116
+ font-size: 0.8125rem;
117
+ font-weight: 600;
118
+ }
119
+ .seams-voiceid-management .seams-voiceid-management-action {
120
+ display: grid;
121
+ grid-template-columns: minmax(0, 1fr) auto;
122
+ align-items: center;
123
+ gap: 0.75rem;
124
+ width: 100%;
125
+ min-height: 44px;
126
+ padding: 0.75rem 0.875rem;
127
+ border: 1px solid var(--seams-colors-borderPrimary, #303b41);
128
+ border-radius: 0.75rem;
129
+ background: transparent;
130
+ text-align: start;
131
+ }
132
+ .seams-voiceid-management-action > svg { color: var(--seams-colors-textSecondary, #9cabb3); }
133
+ .seams-voiceid-management-action-heading {
134
+ display: flex;
135
+ flex-wrap: wrap;
136
+ align-items: center;
137
+ gap: 0.25rem 0.5rem;
138
+ font-weight: 600;
139
+ }
140
+ .seams-voiceid-management-chip {
141
+ padding: 0.125rem 0.5rem;
142
+ border-radius: 6px;
143
+ background: color-mix(in srgb, var(--seams-colors-textPrimary, #eff2f3), transparent 93%);
144
+ font-size: 0.75rem;
145
+ font-weight: 500;
146
+ }
147
+ .seams-voiceid-management-action-detail {
148
+ display: block;
149
+ margin-top: 0.125rem;
150
+ color: var(--seams-colors-textSecondary, #9cabb3);
151
+ font-size: 0.8125rem;
152
+ }
153
+ .seams-voiceid-management .seams-voiceid-management-action[data-state="pending"] {
154
+ opacity: 1;
155
+ border-color: var(--seams-colors-borderHover, #46545b);
156
+ background: var(--seams-colors-surface, #202b31);
157
+ }
158
+ .seams-voiceid-management-action[data-state="pending"] > svg { visibility: hidden; }
159
+ .seams-voiceid-management [data-state="pending"] .seams-voiceid-management-action-detail,
160
+ .seams-voiceid-management [data-state="pending"] .seams-voiceid-management-enrollment-detail {
161
+ color: var(--seams-colors-textPrimary, #eff2f3);
162
+ }
163
+ .seams-voiceid-management [data-state="error"] .seams-voiceid-management-action-detail,
164
+ .seams-voiceid-management [data-state="error"] .seams-voiceid-management-enrollment-detail {
165
+ color: var(--seams-colors-error, oklch(0.74 0.166 19));
166
+ }
167
+ .seams-voiceid-management .seams-voiceid-management-section {
168
+ margin: 0.5rem 0 -0.25rem;
169
+ padding-inline-start: 0.125rem;
170
+ color: var(--seams-colors-textSecondary, #9cabb3);
171
+ font-size: 0.6875rem;
172
+ font-weight: 600;
173
+ letter-spacing: 0.08em;
174
+ text-transform: uppercase;
175
+ }
176
+ /* The status line is read by assistive technology; a message for a hidden row is also shown. */
177
+ .seams-voiceid-prompt.seams-voiceid-management > [role="status"] {
178
+ position: absolute;
179
+ width: 1px;
180
+ height: 1px;
181
+ margin: -1px;
182
+ overflow: hidden;
183
+ clip-path: inset(50%);
184
+ white-space: nowrap;
185
+ }
186
+ .seams-voiceid-prompt.seams-voiceid-management > [role="status"][data-placement="dialog"] {
187
+ position: relative;
188
+ width: auto;
189
+ height: auto;
190
+ margin: 0;
191
+ overflow: visible;
192
+ clip-path: none;
193
+ white-space: normal;
194
+ color: var(--seams-colors-error, oklch(0.74 0.166 19));
195
+ font-size: 0.8125rem;
196
+ overflow-wrap: anywhere;
197
+ }
198
+ [dir='rtl'] .seams-voiceid-management svg { transform: scaleX(-1); }
199
+ @media (hover: hover) {
200
+ .seams-voiceid-management button:hover:not(:disabled) {
201
+ background: var(--seams-colors-surface2, var(--seams-colors-surface, #202b31));
202
+ }
203
+ }
204
+ .seams-voiceid-prompt .seams-voiceid-phrase {
205
+ font-size: clamp(1.75rem, 6vw, 2.25rem);
206
+ font-weight: 600;
207
+ line-height: 1.17;
208
+ letter-spacing: -0.035em;
209
+ text-wrap: balance;
210
+ max-width: 21rem;
211
+ align-self: center;
212
+ margin: 0 auto;
213
+ }
214
+ .seams-voiceid-ribbon {
215
+ display: block;
216
+ width: 308px;
217
+ max-width: 100%;
218
+ height: 64px;
219
+ margin: auto;
220
+ overflow: visible;
221
+ }
222
+ .seams-voiceid-ribbon .seams-voiceid-ribbon-check { stroke: var(--seams-colors-colorBackground, #192024); }
223
+ .seams-voiceid-prompt .seams-voiceid-phrase.seams-voiceid-success-headline {
224
+ display: grid;
225
+ align-content: end;
226
+ font-size: 18px;
227
+ letter-spacing: normal;
228
+ line-height: 1.4;
229
+ }
230
+ .seams-voiceid-prompt .seams-voiceid-status.seams-voiceid-success-message { font-size: 0.8125rem; }
231
+ /* The glow is decorative and cannot intercept record, retry or cancel controls. */
232
+ .seams-voiceid-glow { position: absolute; inset: 0; pointer-events: none; border-radius: inherit; z-index: 0; }
233
+ .seams-voiceid-beam { width: 100%; height: 100%; border-radius: inherit; }
234
+ .seams-voiceid-prompt > :not(.seams-voiceid-glow) { position: relative; z-index: 1; }
235
+ .seams-voiceid-prompt > .seams-voiceid-close { position: absolute; }
236
+ .seams-voiceid-prompt .seams-voiceid-status {
237
+ font-size: 0.75rem;
238
+ line-height: 1.5;
239
+ min-height: 3.375rem;
240
+ color: var(--seams-colors-textSecondary, #9cabb3);
241
+ margin: 0;
242
+ }
243
+ .seams-voiceid-prompt button {
244
+ font: inherit;
245
+ cursor: pointer;
246
+ touch-action: manipulation;
247
+ }
248
+ .seams-voiceid-prompt button:focus-visible {
249
+ outline: 2px solid currentColor;
250
+ outline-offset: 2px;
251
+ }
252
+ .seams-voiceid-prompt .seams-voiceid-close {
253
+ position: absolute;
254
+ right: 0.75rem;
255
+ top: 0.75rem;
256
+ width: 2.5rem;
257
+ height: 2.5rem;
258
+ background: transparent;
259
+ border: 0;
260
+ border-radius: var(--seams-shape-control, 10px);
261
+ color: inherit;
262
+ display: grid;
263
+ place-items: center;
264
+ color: var(--seams-colors-textSecondary, #9cabb3);
265
+ }
266
+ .seams-voiceid-prompt .seams-voiceid-record-slot {
267
+ grid-row: 5;
268
+ grid-column: 1;
269
+ align-self: center;
270
+ width: 100%;
271
+ box-sizing: border-box;
272
+ }
273
+ .seams-voiceid-prompt .seams-voiceid-primary {
274
+ width: 100%;
275
+ min-height: var(--seams-shape-controlHeight, 44px);
276
+ padding: 0.75rem 1rem;
277
+ font-size: 0.875rem;
278
+ font-weight: 500;
279
+ border: 0;
280
+ border-radius: var(--seams-shape-control, 10px);
281
+ background: var(--seams-colors-textPrimary, #eff2f3);
282
+ color: var(--seams-colors-colorBackground, #192024);
283
+ }
284
+ .seams-voiceid-prompt .seams-voiceid-primary:disabled {
285
+ opacity: 0.5;
286
+ cursor: wait;
287
+ }
288
+ .seams-voiceid-prompt button[hidden],
289
+ .seams-voiceid-prompt [hidden] {
290
+ display: none !important;
291
+ }
292
+ .seams-voiceid-prompt .seams-voiceid-model-download {
293
+ font-size: 0.75rem;
294
+ line-height: 1.5;
295
+ }
296
+ .seams-voiceid-prompt .seams-voiceid-model-download progress {
297
+ appearance: none;
298
+ display: block;
299
+ width: 100%;
300
+ height: 4px;
301
+ border: 0;
302
+ border-radius: 2px;
303
+ overflow: hidden;
304
+ background: var(--seams-colors-surface, #202b31);
305
+ accent-color: var(--seams-colors-textPrimary, #eff2f3);
306
+ }
307
+ .seams-voiceid-model-download p { margin: 0 0 0.75rem; color: var(--seams-colors-textSecondary, #9cabb3); font-variant-numeric: tabular-nums; }
308
+ .seams-voiceid-model-download progress::-webkit-progress-bar { background: var(--seams-colors-surface, #202b31); }
309
+ .seams-voiceid-model-download progress::-webkit-progress-value { background: var(--seams-colors-textPrimary, #eff2f3); border-radius: 2px; }
310
+ .seams-voiceid-model-download progress::-moz-progress-bar { background: var(--seams-colors-textPrimary, #eff2f3); }
311
+ @media (hover: hover) and (pointer: fine) {
312
+ .seams-voiceid-close:hover { background: var(--seams-colors-surface, #202b31); }
313
+ .seams-voiceid-primary:hover:not(:disabled) { opacity: 0.9; }
314
+ }
315
+ `;
316
+ const styles = /* @__PURE__ */ new WeakMap();
317
+ var VoicePrompt = class {
318
+ element;
319
+ title;
320
+ phrase;
321
+ status;
322
+ record;
323
+ cancel;
324
+ progress;
325
+ visual;
326
+ disposed = false;
327
+ constructor(document, label, phrase, checking = "ring") {
328
+ let stylesheet = styles.get(document);
329
+ if (!stylesheet) {
330
+ stylesheet = createCspStylesheetManager({
331
+ doc: document,
332
+ baseCss: CSS,
333
+ nonce: getDefaultCspNonce
334
+ });
335
+ styles.set(document, stylesheet);
336
+ }
337
+ stylesheet.ensureBase();
338
+ this.element = document.createElement("section");
339
+ this.element.className = "seams-voiceid-prompt";
340
+ this.element.setAttribute("aria-label", label);
341
+ this.title = document.createElement("h2");
342
+ this.title.textContent = "Read aloud";
343
+ this.phrase = document.createElement("p");
344
+ this.phrase.className = "seams-voiceid-phrase";
345
+ this.phrase.dataset.voiceidPhrase = "";
346
+ this.phrase.setAttribute("aria-live", "polite");
347
+ this.setPhrase(phrase);
348
+ this.visual = new VoiceVisual(document, this.element, checking);
349
+ this.status = document.createElement("p");
350
+ this.status.className = "seams-voiceid-status";
351
+ this.status.setAttribute("role", "status");
352
+ this.record = document.createElement("button");
353
+ this.record.type = "button";
354
+ this.record.className = "seams-voiceid-primary";
355
+ this.record.textContent = "Record";
356
+ this.cancel = document.createElement("button");
357
+ this.cancel.type = "button";
358
+ this.cancel.className = "seams-voiceid-close";
359
+ const closeIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
360
+ closeIcon.setAttribute("viewBox", "0 0 24 24");
361
+ closeIcon.setAttribute("width", "20");
362
+ closeIcon.setAttribute("height", "20");
363
+ closeIcon.setAttribute("aria-hidden", "true");
364
+ const closePath = document.createElementNS("http://www.w3.org/2000/svg", "path");
365
+ closePath.setAttribute("d", "M6 6l12 12M18 6L6 18");
366
+ closePath.setAttribute("fill", "none");
367
+ closePath.setAttribute("stroke", "currentColor");
368
+ closePath.setAttribute("stroke-width", "1.75");
369
+ closePath.setAttribute("stroke-linecap", "round");
370
+ closeIcon.append(closePath);
371
+ this.cancel.append(closeIcon);
372
+ this.cancel.setAttribute("aria-label", "Cancel");
373
+ this.progress = document.createElement("div");
374
+ this.progress.className = "seams-voiceid-progress-slot";
375
+ const slot = document.createElement("div");
376
+ slot.className = "seams-voiceid-record-slot";
377
+ slot.append(this.record);
378
+ this.element.append(this.visual.glow, this.cancel, this.title, this.phrase, this.visual.element, this.status, slot, this.progress);
379
+ }
380
+ setPhrase(phrase) {
381
+ const displayPhrase = phrase === "if you panic panic early" ? "if you panic, panic early" : phrase;
382
+ this.phrase.textContent = displayPhrase.charAt(0).toUpperCase() + displayPhrase.slice(1) + ".";
383
+ this.phrase.dataset.voiceidPhrase = phrase;
384
+ }
385
+ capture = (event) => {
386
+ if (this.disposed) return;
387
+ switch (event.kind) {
388
+ case "listening":
389
+ delete this.status.dataset.voiceCaptureQuality;
390
+ this.element.dataset.capture = "listening";
391
+ this.record.hidden = true;
392
+ this.status.textContent = "Listening";
393
+ this.visual.listen();
394
+ return;
395
+ case "activity":
396
+ this.visual.add(event);
397
+ return;
398
+ case "measured": {
399
+ const quality = event.quality;
400
+ this.status.dataset.voiceCaptureQuality = JSON.stringify(quality);
401
+ if (quality.noise === "high") this.status.textContent = "Background noise is high. Move closer to the microphone.";
402
+ return;
403
+ }
404
+ case "processing":
405
+ this.element.dataset.capture = "processing";
406
+ this.lockLayout();
407
+ this.visual.checking();
408
+ this.status.textContent = "Checking your voice…";
409
+ return;
410
+ default: return assertNever(event);
411
+ }
412
+ };
413
+ /** Settle the visual when an enrollment sample is accepted. */
414
+ accepted() {
415
+ this.visual.accept();
416
+ }
417
+ /**
418
+ * Plays the unlock success in place and resolves when the check is drawn. Once
419
+ * the ring has formed it glides to the middle of `area` (the card by default).
420
+ */
421
+ async succeed(headline, message, area = this.element.getBoundingClientRect()) {
422
+ if (this.disposed) return;
423
+ this.element.dataset.capture = "succeeded";
424
+ this.record.hidden = true;
425
+ await this.visual.succeed(this.beginSuccess.bind(this, headline, message, area));
426
+ }
427
+ ready(message) {
428
+ this.unlockLayout();
429
+ this.visual.ready();
430
+ this.element.dataset.capture = "ready";
431
+ this.status.textContent = message;
432
+ this.record.hidden = false;
433
+ this.record.disabled = false;
434
+ }
435
+ dispose() {
436
+ this.disposed = true;
437
+ this.visual.stop();
438
+ }
439
+ /** The ribbon turns into the ring and the copy may change, so the card's rows hold their size. */
440
+ lockLayout() {
441
+ for (const part of [
442
+ this.element,
443
+ this.phrase,
444
+ this.status
445
+ ]) part.style.minHeight = `${part.getBoundingClientRect().height}px`;
446
+ this.element.style.height = this.element.style.minHeight;
447
+ }
448
+ /**
449
+ * The ring has formed: the reading copy fades where it is, only the ring glides
450
+ * to the middle of the free space, and the success copy arrives in place around
451
+ * it, headline first. The rows keep their locked size throughout.
452
+ */
453
+ async beginSuccess(headline, message, area) {
454
+ if (this.disposed) return;
455
+ const ring = this.visual.element.getBoundingClientRect();
456
+ const lift = (area.top + area.bottom - ring.top - ring.bottom) / 2;
457
+ this.visual.element.animate([{ translate: "0 0" }, { translate: `0 ${lift}px` }], {
458
+ duration: 600,
459
+ easing: "cubic-bezier(0.2, 0, 0, 1)",
460
+ fill: "forwards"
461
+ });
462
+ await Promise.all([
463
+ this.title,
464
+ this.phrase,
465
+ this.status
466
+ ].map(fadeOut));
467
+ if (this.disposed) return;
468
+ this.phrase.textContent = headline;
469
+ this.phrase.classList.add("seams-voiceid-success-headline");
470
+ this.status.textContent = message;
471
+ this.status.classList.add("seams-voiceid-success-message");
472
+ const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
473
+ for (const [index, part] of [this.phrase, this.status].entries()) {
474
+ part.style.translate = `0 ${lift}px`;
475
+ fadeIn(part, reduced, SUCCESS_COPY_DELAY_MS - FADE_OUT_MS + index * SUCCESS_COPY_STAGGER_MS);
476
+ }
477
+ }
478
+ unlockLayout() {
479
+ for (const part of [
480
+ this.element,
481
+ this.phrase,
482
+ this.status
483
+ ]) part.style.minHeight = "";
484
+ this.element.style.height = "";
485
+ }
486
+ };
487
+ const FADE_OUT_MS = 150;
488
+ const SUCCESS_COPY_DELAY_MS = 450;
489
+ const SUCCESS_COPY_STAGGER_MS = 40;
490
+ function fadeOut(element) {
491
+ return element.animate([{ opacity: 1 }, { opacity: 0 }], {
492
+ duration: FADE_OUT_MS,
493
+ easing: "ease-in",
494
+ fill: "forwards"
495
+ }).finished;
496
+ }
497
+ function fadeIn(element, reduced, delay) {
498
+ element.animate([{
499
+ opacity: 0,
500
+ transform: reduced ? "none" : "translateY(4px)"
501
+ }, {
502
+ opacity: 1,
503
+ transform: "none"
504
+ }], {
505
+ delay,
506
+ duration: 300,
507
+ easing: "cubic-bezier(0.2, 0, 0, 1)",
508
+ fill: "both"
509
+ });
510
+ }
511
+ function assertNever(value) {
512
+ throw new Error(`Unexpected capture event: ${String(value)}`);
513
+ }
514
+ //#endregion
515
+ export { VoicePrompt };
@@ -0,0 +1,87 @@
1
+ import { VoiceRibbon } from "../browser/ribbon.js";
2
+ import { VoiceBeam } from "voice-glow";
3
+ import { getDefaultCspNonce } from "@seams/wallet/voiceid-host";
4
+ import { createElement, render } from "preact";
5
+ //#region src/capture/visual.ts
6
+ const VoiceBeamView = VoiceBeam;
7
+ /** Shared by enrollment, unlock and signing. The recorder remains the only audio owner. */
8
+ var VoiceVisual = class {
9
+ element;
10
+ glow;
11
+ ribbon;
12
+ prompt;
13
+ themeObserver;
14
+ phase = "idle";
15
+ disposed = false;
16
+ constructor(document, prompt, checking) {
17
+ this.prompt = prompt;
18
+ this.ribbon = new VoiceRibbon(document, checking);
19
+ this.element = this.ribbon.element;
20
+ this.glow = document.createElement("div");
21
+ this.glow.className = "seams-voiceid-glow";
22
+ this.glow.setAttribute("aria-hidden", "true");
23
+ this.render();
24
+ const nonce = getDefaultCspNonce();
25
+ if (nonce) for (const style of this.glow.querySelectorAll("style")) style.nonce = nonce;
26
+ this.themeObserver = new MutationObserver(this.render);
27
+ this.themeObserver.observe(document.documentElement, {
28
+ attributes: true,
29
+ attributeFilter: ["data-theme"]
30
+ });
31
+ this.themeObserver.observe(document.body, {
32
+ attributes: true,
33
+ attributeFilter: ["data-theme"]
34
+ });
35
+ }
36
+ listen() {
37
+ this.setPhase("listening");
38
+ }
39
+ add(event) {
40
+ this.ribbon.add(event);
41
+ }
42
+ checking() {
43
+ this.setPhase("processing");
44
+ }
45
+ accept() {
46
+ this.setPhase("idle");
47
+ }
48
+ ready() {
49
+ this.setPhase("idle");
50
+ }
51
+ /** The glow settles while the ribbon closes into the success seal; resolves when the check is drawn. */
52
+ succeed(begin) {
53
+ if (this.disposed) return Promise.resolve();
54
+ this.phase = "idle";
55
+ this.render();
56
+ return this.ribbon.succeed(begin);
57
+ }
58
+ stop() {
59
+ if (this.disposed) return;
60
+ this.disposed = true;
61
+ this.themeObserver.disconnect();
62
+ this.ribbon.dispose();
63
+ render(null, this.glow);
64
+ }
65
+ setPhase(phase) {
66
+ if (this.disposed) return;
67
+ this.phase = phase;
68
+ this.ribbon.setPhase(phase);
69
+ this.render();
70
+ }
71
+ render = () => {
72
+ if (this.disposed) return;
73
+ render(createElement(VoiceBeamView, {
74
+ className: "seams-voiceid-beam",
75
+ theme: (this.prompt.closest("[data-theme]")?.getAttribute("data-theme") ?? this.prompt.ownerDocument.documentElement.getAttribute("data-theme")) === "light" ? "light" : "dark",
76
+ level: this.ribbon.readLevel,
77
+ processing: this.phase === "processing",
78
+ active: this.phase !== "idle",
79
+ staticColors: true,
80
+ colorVariant: "ocean",
81
+ borderRadius: Number.parseFloat(getComputedStyle(this.prompt).borderRadius) || 16,
82
+ children: null
83
+ }), this.glow);
84
+ };
85
+ };
86
+ //#endregion
87
+ export { VoiceVisual };
@@ -0,0 +1,7 @@
1
+ //#region src/descriptor.ts
2
+ /** A small opt-in descriptor with no capture, model, or host implementation. */
3
+ function voiceIdAuthMethod() {
4
+ return { kind: "voiceid" };
5
+ }
6
+ //#endregion
7
+ export { voiceIdAuthMethod };
@@ -0,0 +1,2 @@
1
+ import { voiceIdAuthMethod } from "./descriptor.js";
2
+ export { voiceIdAuthMethod };