@seams/voice-capture 0.0.0-stage → 0.67.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 (48) hide show
  1. package/LICENSE.md +201 -0
  2. package/README.md +174 -2
  3. package/dist/assets/NOTICE.txt +3 -0
  4. package/dist/assets/SILERO_LICENSE.txt +21 -0
  5. package/dist/assets/ort-wasm-simd-threaded.mjs +59 -0
  6. package/dist/assets/ort-wasm-simd-threaded.wasm +0 -0
  7. package/dist/assets/silero-vad.onnx +0 -0
  8. package/dist/assets/voice-activity-worklet.js +21 -0
  9. package/dist/browser/card.js +800 -0
  10. package/dist/browser/chunks/microphone-CU5gQG6w.js +1 -0
  11. package/dist/browser/chunks/ort.wasm.bundle.min-CWqlff_l.js +3 -0
  12. package/dist/browser/index.js +1 -0
  13. package/dist/esm/card/index.js +6 -0
  14. package/dist/esm/card/phraseCard.js +459 -0
  15. package/dist/esm/card/prompt.js +1074 -0
  16. package/dist/esm/card/ribbon.js +489 -0
  17. package/dist/esm/card/surface.js +224 -0
  18. package/dist/esm/card/types.js +0 -0
  19. package/dist/esm/card/visual.js +2 -0
  20. package/dist/esm/chunks/visual-Di3ojTED.js +1520 -0
  21. package/dist/esm/config.js +42 -0
  22. package/dist/esm/index.js +8 -0
  23. package/dist/esm/inference.js +193 -0
  24. package/dist/esm/microphone.js +428 -0
  25. package/dist/esm/microphoneInput.js +117 -0
  26. package/dist/esm/speechPause.js +33 -0
  27. package/dist/esm/voiceActivity.js +69 -0
  28. package/dist/esm/wav.js +36 -0
  29. package/dist/example/example.js +71 -0
  30. package/dist/example/index.html +29 -0
  31. package/dist/hosting.d.mts +4 -0
  32. package/dist/hosting.mjs +26 -0
  33. package/dist/types/card/index.d.ts +6 -0
  34. package/dist/types/card/phraseCard.d.ts +72 -0
  35. package/dist/types/card/prompt.d.ts +122 -0
  36. package/dist/types/card/ribbon.d.ts +62 -0
  37. package/dist/types/card/surface.d.ts +28 -0
  38. package/dist/types/card/types.d.ts +65 -0
  39. package/dist/types/card/visual.d.ts +25 -0
  40. package/dist/types/config.d.ts +47 -0
  41. package/dist/types/index.d.ts +7 -0
  42. package/dist/types/inference.d.ts +47 -0
  43. package/dist/types/microphone.d.ts +81 -0
  44. package/dist/types/microphoneInput.d.ts +19 -0
  45. package/dist/types/speechPause.d.ts +8 -0
  46. package/dist/types/voiceActivity.d.ts +15 -0
  47. package/dist/types/wav.d.ts +9 -0
  48. package/package.json +57 -4
@@ -0,0 +1,224 @@
1
+ import { captureConfig } from "../config.js";
2
+ import { voiceActivityModel } from "../voiceActivity.js";
3
+ import { expectedSpeechMs } from "../speechPause.js";
4
+ import { VoiceMicrophoneInterruptedError } from "../microphoneInput.js";
5
+ import { recordVoice, voiceQualityMessage } from "../microphone.js";
6
+ import { PhraseCard } from "./phraseCard.js";
7
+ import { VoicePrompt, leaveSurface } from "./prompt.js";
8
+ //#region src/card/surface.ts
9
+ const LABEL = "Say this with your voice";
10
+ const PROMPT_LABEL = "Say this sentence";
11
+ const PROMPT_MESSAGE = "Read it naturally, then pause.";
12
+ /**
13
+ * Shows the given sentence in the prompt. The surface records clips with the quality check, and
14
+ * stays open so the caller can show its check and the result in the same card. A too quiet or
15
+ * interrupted recording stays on the surface for another try.
16
+ */
17
+ function openPhraseCard(input) {
18
+ return new CardSurface(input);
19
+ }
20
+ var CardSurface = class {
21
+ element;
22
+ prompt;
23
+ card;
24
+ unmounted;
25
+ outer;
26
+ detector;
27
+ recorder = null;
28
+ recording = null;
29
+ choice = null;
30
+ view = null;
31
+ started = false;
32
+ closed = false;
33
+ /** The voiced time of the whole sentence, after which a shorter pause ends a recording. */
34
+ expectedSpeechMs;
35
+ /** The speech detector has started; before that, a recording waits for it to load. */
36
+ speechDetectorReady = false;
37
+ constructor(input) {
38
+ const { sentence, frame = {} } = input;
39
+ const outer = input.signal;
40
+ this.outer = outer;
41
+ this.detector = input.detector;
42
+ this.expectedSpeechMs = expectedSpeechMs(sentence);
43
+ const handedOver = document.querySelector(".seams-voiceid-activation-surface[data-seams-voiceid-handoff]");
44
+ if (input.detector === "level") this.speechDetectorReady = true;
45
+ else voiceActivityModel.prepare().then(this.detectorStarted, ignoreEarlyLoad);
46
+ const parent = frame.parent ?? document.body;
47
+ this.element = document.createElement("section");
48
+ this.element.className = ["seams-voiceid-activation-surface", frame.className].filter(Boolean).join(" ");
49
+ this.element.setAttribute("role", "dialog");
50
+ this.element.setAttribute("aria-modal", "true");
51
+ this.element.setAttribute("aria-label", LABEL);
52
+ this.prompt = new VoicePrompt(document, LABEL, sentence);
53
+ this.prompt.title.textContent = PROMPT_LABEL;
54
+ const shown = /[.!?]$/.test(sentence) ? sentence : `${sentence}.`;
55
+ this.prompt.phrase.textContent = shown;
56
+ delete this.prompt.phrase.dataset.voiceidPhrase;
57
+ this.prompt.status.textContent = PROMPT_MESSAGE;
58
+ this.prompt.record.textContent = "Record";
59
+ if (frame.back) this.prompt.showBack();
60
+ const handedOverPrompt = handedOver?.querySelector(":scope > .seams-voiceid-prompt") ?? null;
61
+ const rows = handedOverPrompt ? handedOverRows(handedOverPrompt) : null;
62
+ if (handedOverPrompt) {
63
+ this.prompt.continueFrom(handedOverPrompt);
64
+ this.prompt.record.hidden = true;
65
+ }
66
+ this.card = new PhraseCard(this.prompt, {
67
+ label: PROMPT_LABEL,
68
+ sentence: shown,
69
+ message: PROMPT_MESSAGE,
70
+ action: "Record"
71
+ }, rows);
72
+ this.element.append(this.prompt.element);
73
+ this.prompt.record.addEventListener("click", this.press);
74
+ this.prompt.cancel.addEventListener("click", this.cancel);
75
+ this.element.addEventListener("cancel", this.cancel);
76
+ outer.addEventListener("abort", this.cancel);
77
+ if (handedOver) handedOver.replaceWith(this.element);
78
+ else parent.appendChild(this.element);
79
+ if (frame.hugged) this.element.classList.add("seams-voiceid-hugged");
80
+ this.unmounted = frame.mounted?.(this.element);
81
+ if (rows) this.prompt.element.focus({ preventScroll: true });
82
+ else this.prompt.record.focus();
83
+ if (outer.aborted) this.cancel();
84
+ }
85
+ record() {
86
+ if (this.closed) return Promise.resolve({ kind: "cancelled" });
87
+ this.settleChoice("replaced");
88
+ this.recording?.resolve({ kind: "cancelled" });
89
+ const recording = deferred();
90
+ this.recording = recording;
91
+ if (this.view) {
92
+ this.view = null;
93
+ this.card.showPrompt();
94
+ }
95
+ return recording.promise;
96
+ }
97
+ show(view) {
98
+ if (this.closed) return Promise.resolve("closed");
99
+ this.settleChoice("replaced");
100
+ this.view = view;
101
+ this.card.show(view);
102
+ return new Promise((resolve) => {
103
+ this.choice = resolve;
104
+ });
105
+ }
106
+ close() {
107
+ if (this.closed) return;
108
+ this.closed = true;
109
+ this.recorder?.abort();
110
+ this.recording?.resolve({ kind: "cancelled" });
111
+ this.settleChoice("closed");
112
+ this.unmounted?.();
113
+ this.outer.removeEventListener("abort", this.cancel);
114
+ this.element.removeEventListener("cancel", this.cancel);
115
+ this.prompt.record.removeEventListener("click", this.press);
116
+ this.prompt.cancel.removeEventListener("click", this.cancel);
117
+ this.card.dispose();
118
+ this.prompt.dispose();
119
+ leaveSurface(this.element);
120
+ }
121
+ /** The one button records on the prompt and answers a result. */
122
+ press = () => {
123
+ if (this.view) {
124
+ if (this.view.kind === "result" && !this.view.action.busy) this.settleChoice("action");
125
+ return;
126
+ }
127
+ if (!this.recording || this.started) return;
128
+ this.started = true;
129
+ this.prompt.record.disabled = true;
130
+ this.prompt.status.textContent = this.speechDetectorReady ? "Opening microphone…" : `Loading ${captureConfig().label}…`;
131
+ this.attempt(this.recording);
132
+ };
133
+ async attempt(recording) {
134
+ const recorder = new AbortController();
135
+ this.recorder = recorder;
136
+ try {
137
+ const recorded = await recordVoice(recorder.signal, this.capture, {
138
+ expectedSpeechMs: this.expectedSpeechMs,
139
+ detector: this.detector
140
+ });
141
+ if (this.recording !== recording || this.closed) {
142
+ recorded.samples?.fill(0);
143
+ return;
144
+ }
145
+ if (recorded.kind === "insufficient_audio") {
146
+ this.retry(voiceQualityMessage(recorded.quality));
147
+ return;
148
+ }
149
+ const endOfSpeechAt = recorded.lastSoundAtMs ?? performance.now();
150
+ this.started = false;
151
+ this.recording = null;
152
+ recording.resolve({
153
+ kind: "recorded",
154
+ samples: recorded.samples,
155
+ endOfSpeechAt
156
+ });
157
+ } catch (error) {
158
+ if (this.recording !== recording || this.closed) return;
159
+ if (error instanceof VoiceMicrophoneInterruptedError) {
160
+ this.retry(error.message);
161
+ return;
162
+ }
163
+ this.started = false;
164
+ this.recording = null;
165
+ recording.reject(error instanceof Error ? error : new Error(String(error)));
166
+ }
167
+ }
168
+ detectorStarted = () => {
169
+ this.speechDetectorReady = true;
170
+ };
171
+ /** The end of speech starts the check in place; every other capture event is the prompt's. */
172
+ capture = (event) => {
173
+ if (event.kind === "processing") this.card.beginCheck();
174
+ else this.prompt.capture(event);
175
+ };
176
+ retry(message) {
177
+ this.started = false;
178
+ this.prompt.record.textContent = "Try again";
179
+ this.prompt.ready(message);
180
+ this.prompt.record.focus();
181
+ }
182
+ settleChoice(choice) {
183
+ const settle = this.choice;
184
+ this.choice = null;
185
+ settle?.(choice);
186
+ }
187
+ /** A close during a check is not possible: the button is hidden while the caller works. */
188
+ cancel = () => {
189
+ if (this.view?.kind === "check" && !this.outer.aborted) return;
190
+ if (this.recording) {
191
+ this.recorder?.abort();
192
+ this.recording.resolve({ kind: "cancelled" });
193
+ this.recording = null;
194
+ this.started = false;
195
+ }
196
+ this.settleChoice("closed");
197
+ if (this.outer.aborted) this.close();
198
+ };
199
+ };
200
+ /** A failed early load runs again when Record needs the detector, and shows its error there. */
201
+ function ignoreEarlyLoad() {}
202
+ /** The rows that the handed-over card shows now. */
203
+ function handedOverRows(prompt) {
204
+ const read = (selector) => prompt.querySelector(`:scope > ${selector}`)?.textContent ?? "";
205
+ return {
206
+ label: read("h2"),
207
+ message: read(".seams-voiceid-status")
208
+ };
209
+ }
210
+ /** A promise with its two settle functions. */
211
+ function deferred() {
212
+ let resolve;
213
+ let reject;
214
+ return {
215
+ promise: new Promise((onResolve, onReject) => {
216
+ resolve = onResolve;
217
+ reject = onReject;
218
+ }),
219
+ resolve,
220
+ reject
221
+ };
222
+ }
223
+ //#endregion
224
+ export { openPhraseCard };
File without changes
@@ -0,0 +1,2 @@
1
+ import { t as VoiceVisual } from "../chunks/visual-Di3ojTED.js";
2
+ export { VoiceVisual };