@seams/voiceid 0.0.0-stage → 0.25.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 (66) hide show
  1. package/LICENSE.md +201 -0
  2. package/README.md +104 -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 +516 -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-Bz9BDPI4.js +1 -0
  19. package/dist/public/sdk/voiceid/approval-CME3DxNc.js +1 -0
  20. package/dist/public/sdk/voiceid/aurora-int8.onnx +0 -0
  21. package/dist/public/sdk/voiceid/deviceBridge-DDviNxL6.js +1 -0
  22. package/dist/public/sdk/voiceid/enrollment-D8oQnM_e.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-DEkI70F2.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-CGnhhdt4.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/confirmationCommand.d.ts +11 -0
  36. package/dist/types/browser/deviceStore.d.ts +19 -0
  37. package/dist/types/browser/enrollment.d.ts +37 -0
  38. package/dist/types/browser/enrollmentStore.d.ts +91 -0
  39. package/dist/types/browser/inference.d.ts +39 -0
  40. package/dist/types/browser/microphone.d.ts +61 -0
  41. package/dist/types/browser/model.d.ts +8 -0
  42. package/dist/types/browser/phrase.d.ts +37 -0
  43. package/dist/types/browser/phraseVocabulary.d.ts +10 -0
  44. package/dist/types/browser/progress.d.ts +2 -0
  45. package/dist/types/browser/ribbon.d.ts +63 -0
  46. package/dist/types/browser/verificationError.d.ts +6 -0
  47. package/dist/types/browser/voiceActivity.d.ts +15 -0
  48. package/dist/types/capture/approval.d.ts +38 -0
  49. package/dist/types/capture/bridge.d.ts +12 -0
  50. package/dist/types/capture/deviceBridge.d.ts +29 -0
  51. package/dist/types/capture/prompt.d.ts +41 -0
  52. package/dist/types/capture/surface.d.ts +27 -0
  53. package/dist/types/capture/unlockFrame.d.ts +13 -0
  54. package/dist/types/capture/visual.d.ts +24 -0
  55. package/dist/types/descriptor.d.ts +4 -0
  56. package/dist/types/host.d.ts +4 -0
  57. package/dist/types/index.d.ts +2 -0
  58. package/dist/types/react/VoiceAssistantCard.d.ts +37 -0
  59. package/dist/types/react.d.ts +5 -0
  60. package/dist/types/session/activation.d.ts +61 -0
  61. package/dist/types/session/activationStore.d.ts +21 -0
  62. package/dist/types/session/httpClient.d.ts +39 -0
  63. package/dist/types/session/unlock.d.ts +4 -0
  64. package/dist/types/session/unlockFromDevice.d.ts +18 -0
  65. package/package.json +66 -4
  66. package/src/README.md +280 -0
@@ -0,0 +1,516 @@
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
+ const DISPLAY_PHRASES = new Map([["if you panic panic early", "if you panic, panic early"], ["life is short go long to hedge", "life is short, go long to hedge"]]);
318
+ var VoicePrompt = class {
319
+ element;
320
+ title;
321
+ phrase;
322
+ status;
323
+ record;
324
+ cancel;
325
+ progress;
326
+ visual;
327
+ disposed = false;
328
+ constructor(document, label, phrase, checking = "ring") {
329
+ let stylesheet = styles.get(document);
330
+ if (!stylesheet) {
331
+ stylesheet = createCspStylesheetManager({
332
+ doc: document,
333
+ baseCss: CSS,
334
+ nonce: getDefaultCspNonce
335
+ });
336
+ styles.set(document, stylesheet);
337
+ }
338
+ stylesheet.ensureBase();
339
+ this.element = document.createElement("section");
340
+ this.element.className = "seams-voiceid-prompt";
341
+ this.element.setAttribute("aria-label", label);
342
+ this.title = document.createElement("h2");
343
+ this.title.textContent = "Read aloud";
344
+ this.phrase = document.createElement("p");
345
+ this.phrase.className = "seams-voiceid-phrase";
346
+ this.phrase.dataset.voiceidPhrase = "";
347
+ this.phrase.setAttribute("aria-live", "polite");
348
+ this.setPhrase(phrase);
349
+ this.visual = new VoiceVisual(document, this.element, checking);
350
+ this.status = document.createElement("p");
351
+ this.status.className = "seams-voiceid-status";
352
+ this.status.setAttribute("role", "status");
353
+ this.record = document.createElement("button");
354
+ this.record.type = "button";
355
+ this.record.className = "seams-voiceid-primary";
356
+ this.record.textContent = "Record";
357
+ this.cancel = document.createElement("button");
358
+ this.cancel.type = "button";
359
+ this.cancel.className = "seams-voiceid-close";
360
+ const closeIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
361
+ closeIcon.setAttribute("viewBox", "0 0 24 24");
362
+ closeIcon.setAttribute("width", "20");
363
+ closeIcon.setAttribute("height", "20");
364
+ closeIcon.setAttribute("aria-hidden", "true");
365
+ const closePath = document.createElementNS("http://www.w3.org/2000/svg", "path");
366
+ closePath.setAttribute("d", "M6 6l12 12M18 6L6 18");
367
+ closePath.setAttribute("fill", "none");
368
+ closePath.setAttribute("stroke", "currentColor");
369
+ closePath.setAttribute("stroke-width", "1.75");
370
+ closePath.setAttribute("stroke-linecap", "round");
371
+ closeIcon.append(closePath);
372
+ this.cancel.append(closeIcon);
373
+ this.cancel.setAttribute("aria-label", "Cancel");
374
+ this.progress = document.createElement("div");
375
+ this.progress.className = "seams-voiceid-progress-slot";
376
+ const slot = document.createElement("div");
377
+ slot.className = "seams-voiceid-record-slot";
378
+ slot.append(this.record);
379
+ this.element.append(this.visual.glow, this.cancel, this.title, this.phrase, this.visual.element, this.status, slot, this.progress);
380
+ }
381
+ setPhrase(phrase) {
382
+ const displayPhrase = DISPLAY_PHRASES.get(phrase) ?? phrase;
383
+ this.phrase.textContent = displayPhrase.charAt(0).toUpperCase() + displayPhrase.slice(1) + ".";
384
+ this.phrase.dataset.voiceidPhrase = phrase;
385
+ }
386
+ capture = (event) => {
387
+ if (this.disposed) return;
388
+ switch (event.kind) {
389
+ case "listening":
390
+ delete this.status.dataset.voiceCaptureQuality;
391
+ this.element.dataset.capture = "listening";
392
+ this.record.hidden = true;
393
+ this.status.textContent = "Listening";
394
+ this.visual.listen();
395
+ return;
396
+ case "activity":
397
+ this.visual.add(event);
398
+ return;
399
+ case "measured": {
400
+ const quality = event.quality;
401
+ this.status.dataset.voiceCaptureQuality = JSON.stringify(quality);
402
+ if (quality.noise === "high") this.status.textContent = "Background noise is high. Move closer to the microphone.";
403
+ return;
404
+ }
405
+ case "processing":
406
+ this.element.dataset.capture = "processing";
407
+ this.lockLayout();
408
+ this.visual.checking();
409
+ this.status.textContent = "Checking your voice…";
410
+ return;
411
+ default: return assertNever(event);
412
+ }
413
+ };
414
+ /** Settle the visual when an enrollment sample is accepted. */
415
+ accepted() {
416
+ this.visual.accept();
417
+ }
418
+ /**
419
+ * Plays the unlock success in place and resolves when the check is drawn. Once
420
+ * the ring has formed it glides to the middle of `area` (the card by default).
421
+ */
422
+ async succeed(headline, message, area = this.element.getBoundingClientRect()) {
423
+ if (this.disposed) return;
424
+ this.element.dataset.capture = "succeeded";
425
+ this.record.hidden = true;
426
+ await this.visual.succeed(this.beginSuccess.bind(this, headline, message, area));
427
+ }
428
+ ready(message) {
429
+ this.unlockLayout();
430
+ this.visual.ready();
431
+ this.element.dataset.capture = "ready";
432
+ this.status.textContent = message;
433
+ this.record.hidden = false;
434
+ this.record.disabled = false;
435
+ }
436
+ dispose() {
437
+ this.disposed = true;
438
+ this.visual.stop();
439
+ }
440
+ /** The ribbon turns into the ring and the copy may change, so the card's rows hold their size. */
441
+ lockLayout() {
442
+ for (const part of [
443
+ this.element,
444
+ this.phrase,
445
+ this.status
446
+ ]) part.style.minHeight = `${part.getBoundingClientRect().height}px`;
447
+ this.element.style.height = this.element.style.minHeight;
448
+ }
449
+ /**
450
+ * The ring has formed: the reading copy fades where it is, only the ring glides
451
+ * to the middle of the free space, and the success copy arrives in place around
452
+ * it, headline first. The rows keep their locked size throughout.
453
+ */
454
+ async beginSuccess(headline, message, area) {
455
+ if (this.disposed) return;
456
+ const ring = this.visual.element.getBoundingClientRect();
457
+ const lift = (area.top + area.bottom - ring.top - ring.bottom) / 2;
458
+ this.visual.element.animate([{ translate: "0 0" }, { translate: `0 ${lift}px` }], {
459
+ duration: 600,
460
+ easing: "cubic-bezier(0.2, 0, 0, 1)",
461
+ fill: "forwards"
462
+ });
463
+ await Promise.all([
464
+ this.title,
465
+ this.phrase,
466
+ this.status
467
+ ].map(fadeOut));
468
+ if (this.disposed) return;
469
+ this.phrase.textContent = headline;
470
+ this.phrase.classList.add("seams-voiceid-success-headline");
471
+ this.status.textContent = message;
472
+ this.status.classList.add("seams-voiceid-success-message");
473
+ const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
474
+ for (const [index, part] of [this.phrase, this.status].entries()) {
475
+ part.style.translate = `0 ${lift}px`;
476
+ fadeIn(part, reduced, SUCCESS_COPY_DELAY_MS - FADE_OUT_MS + index * SUCCESS_COPY_STAGGER_MS);
477
+ }
478
+ }
479
+ unlockLayout() {
480
+ for (const part of [
481
+ this.element,
482
+ this.phrase,
483
+ this.status
484
+ ]) part.style.minHeight = "";
485
+ this.element.style.height = "";
486
+ }
487
+ };
488
+ const FADE_OUT_MS = 150;
489
+ const SUCCESS_COPY_DELAY_MS = 450;
490
+ const SUCCESS_COPY_STAGGER_MS = 40;
491
+ function fadeOut(element) {
492
+ return element.animate([{ opacity: 1 }, { opacity: 0 }], {
493
+ duration: FADE_OUT_MS,
494
+ easing: "ease-in",
495
+ fill: "forwards"
496
+ }).finished;
497
+ }
498
+ function fadeIn(element, reduced, delay) {
499
+ element.animate([{
500
+ opacity: 0,
501
+ transform: reduced ? "none" : "translateY(4px)"
502
+ }, {
503
+ opacity: 1,
504
+ transform: "none"
505
+ }], {
506
+ delay,
507
+ duration: 300,
508
+ easing: "cubic-bezier(0.2, 0, 0, 1)",
509
+ fill: "both"
510
+ });
511
+ }
512
+ function assertNever(value) {
513
+ throw new Error(`Unexpected capture event: ${String(value)}`);
514
+ }
515
+ //#endregion
516
+ 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 };