@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,28 @@
1
+ import { type VoiceRecordingOptions } from '../microphone';
2
+ import type { PhraseSurface } from './types';
3
+ /** What the host page adds around the card. Every field is optional. */
4
+ export type PhraseCardFrame = Readonly<{
5
+ /** The element that gets the card. Default: the document body. */
6
+ parent?: HTMLElement;
7
+ /** More class names for the surface element, such as the host's theme class. */
8
+ className?: string;
9
+ /** Shows the Back control of the prompt. */
10
+ back?: boolean;
11
+ /** The host sizes its frame to the surface, so the card follows the frame. */
12
+ hugged?: boolean;
13
+ /** Runs when the surface is in the document. Its return value runs when the surface closes. */
14
+ mounted?(element: HTMLElement): (() => void) | void;
15
+ }>;
16
+ export type PhraseCardInput = Readonly<{
17
+ sentence: string;
18
+ signal: AbortSignal;
19
+ frame?: PhraseCardFrame;
20
+ /** The speech detector of the recordings. Default: `model`. */
21
+ detector?: VoiceRecordingOptions['detector'];
22
+ }>;
23
+ /**
24
+ * Shows the given sentence in the prompt. The surface records clips with the quality check, and
25
+ * stays open so the caller can show its check and the result in the same card. A too quiet or
26
+ * interrupted recording stays on the surface for another try.
27
+ */
28
+ export declare function openPhraseCard(input: PhraseCardInput): PhraseSurface;
@@ -0,0 +1,65 @@
1
+ /** One clip of a shown sentence: 16 kHz mono samples that passed the quality check. */
2
+ export type PhraseRecording = Readonly<{
3
+ kind: 'recorded';
4
+ samples: Float32Array;
5
+ /**
6
+ * `performance.now()` of the last voiced frame: the person stopped speaking then, before
7
+ * the pause that ends the recording.
8
+ */
9
+ endOfSpeechAt: number;
10
+ }> | Readonly<{
11
+ kind: 'cancelled';
12
+ }>;
13
+ /** One segment of the line that shows a check: waiting, working, passed, failed or held. */
14
+ export type PhraseSegmentState = 'idle' | 'run' | 'done' | 'fail' | 'wait' | 'skip';
15
+ /** Text in which some words are marked, such as words that a verifier heard differently. */
16
+ export type PhraseText = string | readonly (string | Readonly<{
17
+ mark: string;
18
+ }>)[];
19
+ /**
20
+ * A check after the recording: the sentence stays and dims, the action row goes, and the line
21
+ * becomes segments. Segments that change from `run` to `done` fill one after another.
22
+ */
23
+ export type PhraseCheckView = Readonly<{
24
+ kind: 'check';
25
+ label: string;
26
+ caption: string;
27
+ /** The second line. Several lines change every 1.3 s and stay on the last one. */
28
+ details: readonly string[];
29
+ segments: readonly PhraseSegmentState[];
30
+ }>;
31
+ /** A result or a question: a headline takes the sentence's place, with one action. */
32
+ export type PhraseResultView = Readonly<{
33
+ kind: 'result';
34
+ label: string;
35
+ headline: string;
36
+ caption: PhraseText;
37
+ detail: string;
38
+ /** A link after the detail, such as a transaction in a block explorer. */
39
+ link?: Readonly<{
40
+ text: string;
41
+ href: string;
42
+ }>;
43
+ /** `busy` shows the action dimmed while something outside the surface finishes. */
44
+ action: Readonly<{
45
+ label: string;
46
+ icon?: 'passkey';
47
+ busy?: boolean;
48
+ }>;
49
+ /** `seal`: the line closes into the success seal. */
50
+ line: 'seal' | readonly PhraseSegmentState[];
51
+ }>;
52
+ export type PhraseView = PhraseCheckView | PhraseResultView;
53
+ /** `replaced`: another view or a recording took the surface first. */
54
+ export type PhraseChoice = 'action' | 'closed' | 'replaced';
55
+ /**
56
+ * The phrase surface stays open after a recording, so the caller can show its check and the
57
+ * result in the same card. The caller closes it.
58
+ */
59
+ export interface PhraseSurface {
60
+ /** Shows the sentence with Record and records one clip. A later call records again. */
61
+ record(): Promise<PhraseRecording>;
62
+ /** A check view settles only when it is replaced; a result settles on its action or close. */
63
+ show(view: PhraseView): Promise<PhraseChoice>;
64
+ close(): void;
65
+ }
@@ -0,0 +1,25 @@
1
+ import type { VoiceActivityEvent } from '../microphone';
2
+ import { type VoiceCheckingStyle } from './ribbon';
3
+ /** Shared by enrollment, unlock and signing. The recorder remains the only audio owner. */
4
+ export declare class VoiceVisual {
5
+ readonly element: SVGSVGElement;
6
+ readonly glow: HTMLDivElement;
7
+ private readonly ribbon;
8
+ private readonly prompt;
9
+ private readonly themeObserver;
10
+ private phase;
11
+ private disposed;
12
+ constructor(document: Document, prompt: HTMLElement, checking: VoiceCheckingStyle);
13
+ listen(): void;
14
+ add(event: VoiceActivityEvent): void;
15
+ checking(): void;
16
+ accept(): void;
17
+ /** The glow keeps working while the line rests flat, for a status line that takes its place. */
18
+ flatten(): void;
19
+ ready(): void;
20
+ /** The glow settles while the ribbon closes into the success seal; resolves when the check is drawn. */
21
+ succeed(begin: () => void): Promise<void>;
22
+ stop(): void;
23
+ private setPhase;
24
+ private readonly render;
25
+ }
@@ -0,0 +1,47 @@
1
+ /** The microphone that the person chose: the system default, or one exact device. */
2
+ export type VoiceMicrophonePreference = {
3
+ readonly kind: 'system_default';
4
+ readonly deviceId?: never;
5
+ } | {
6
+ readonly kind: 'device';
7
+ readonly deviceId: string;
8
+ };
9
+ /** Where the page keeps the chosen microphone. `read` returns what `save` got, or anything else. */
10
+ export type VoiceMicrophonePreferenceStore = Readonly<{
11
+ read(): Promise<unknown>;
12
+ save(preference: VoiceMicrophonePreference): Promise<void>;
13
+ }>;
14
+ export type VoiceCaptureConfig = Readonly<{
15
+ /**
16
+ * The folder URL that serves the files of `@seams/voice-capture/hosting`: the speech detection
17
+ * model, the audio worklet and the model engine. A relative URL is read against the page.
18
+ */
19
+ assetBase: string | URL;
20
+ /** Keeps the chosen microphone between visits. Default: it lasts until the page reloads. */
21
+ microphonePreference?: VoiceMicrophonePreferenceStore;
22
+ /** The product name in messages for the person, such as "VoiceID". */
23
+ label?: string;
24
+ /** The place that the person must keep open, such as "the Wallet". */
25
+ place?: string;
26
+ /** The site that serves the asset files, such as "this Wallet". */
27
+ site?: string;
28
+ /** What the person can do when a file cannot load. It is one sentence. */
29
+ recoveryHint?: string;
30
+ /** The name of the browser cache that keeps the model files. */
31
+ cacheName?: string;
32
+ /** The nonce of the page's style policy, for the style elements of the phrase card. */
33
+ styleNonce?: string | (() => string | undefined);
34
+ }>;
35
+ type ResolvedConfig = Required<Omit<VoiceCaptureConfig, 'assetBase' | 'microphonePreference' | 'styleNonce'>> & Readonly<{
36
+ assetBase: URL | null;
37
+ microphonePreference: VoiceMicrophonePreferenceStore | null;
38
+ styleNonce: string | (() => string | undefined) | null;
39
+ }>;
40
+ /** Sets where the assets are and how the page names itself. Call it once, before a recording. */
41
+ export declare function configureVoiceCapture(config: VoiceCaptureConfig): void;
42
+ export declare function captureConfig(): ResolvedConfig;
43
+ /** The nonce for a style element, or undefined when the page has none. */
44
+ export declare function styleNonce(): string | undefined;
45
+ /** The URL of one asset file. It throws before `configureVoiceCapture` set the folder. */
46
+ export declare function assetUrl(fileName: string): URL;
47
+ export {};
@@ -0,0 +1,7 @@
1
+ export { configureVoiceCapture, type VoiceCaptureConfig, type VoiceMicrophonePreference, type VoiceMicrophonePreferenceStore, } from './config';
2
+ export { recordVoice, requireVoiceForeground, requireVoiceUserAction, voiceQualityMessage, InsufficientVoiceAudioError, type VoiceActivityEvent, type VoiceCaptureEvent, type VoiceCaptureObserver, type VoiceCaptureQuality, type VoiceRecordingOptions, type VoiceRecordingResult, } from './microphone';
3
+ export { openVoiceMicrophone, rememberVoiceMicrophone, useSystemVoiceMicrophone, VoiceMicrophoneInterruptedError, type VoiceMicrophone, } from './microphoneInput';
4
+ export { endPauseMs, expectedSpeechMs } from './speechPause';
5
+ export { createVoiceActivity, levelDetector, voiceActivityModel, type SpeechDetector, } from './voiceActivity';
6
+ export { BrowserInferenceModel, VoiceAssetLoadError, voiceInferenceEngine, type ModelAsset, type VoiceAssetProgress, } from './inference';
7
+ export { MAX_SAMPLES, pcm16Wav, SAMPLE_RATE } from './wav';
@@ -0,0 +1,47 @@
1
+ import type { InferenceSession } from 'onnxruntime-web';
2
+ /** One file under the asset folder, with the exact bytes that the page accepts. */
3
+ export type ModelAsset = Readonly<{
4
+ fileName: string;
5
+ sha256: string;
6
+ bytes: number;
7
+ /** The name in messages for the person. A function reads the settings when it is needed. */
8
+ label: string | (() => string);
9
+ }>;
10
+ export type VoiceAssetProgress = {
11
+ kind: 'checking_cache';
12
+ } | {
13
+ kind: 'downloading';
14
+ received: number;
15
+ } | {
16
+ kind: 'ready';
17
+ };
18
+ export declare class VoiceAssetLoadError extends Error {
19
+ readonly kind: 'connection' | 'unavailable' | 'integrity' | 'initialization';
20
+ constructor(kind: 'connection' | 'unavailable' | 'integrity' | 'initialization', message: string, options?: ErrorOptions);
21
+ }
22
+ declare class BrowserInferenceAsset {
23
+ private readonly asset;
24
+ private pending;
25
+ private progress;
26
+ private readonly listeners;
27
+ constructor(asset: ModelAsset);
28
+ get size(): number;
29
+ download(onProgress?: (progress: VoiceAssetProgress) => void): Promise<ArrayBuffer>;
30
+ private readonly failed;
31
+ private publish;
32
+ private valid;
33
+ private bytes;
34
+ private fetchBytes;
35
+ }
36
+ export declare const voiceInferenceEngine: BrowserInferenceAsset;
37
+ export declare class BrowserInferenceModel {
38
+ readonly asset: BrowserInferenceAsset;
39
+ private session;
40
+ constructor(asset: ModelAsset);
41
+ prepare(): Promise<InferenceSession>;
42
+ /** Runs the model on one row of samples, as its `samples` input. Dispose the outputs. */
43
+ run(samples: Float32Array): Promise<InferenceSession.ReturnType>;
44
+ private readonly failed;
45
+ private load;
46
+ }
47
+ export {};
@@ -0,0 +1,81 @@
1
+ import { type VoiceMicrophone } from './microphoneInput';
2
+ /** One 32 ms frame as the recorder judged it. No capture event carries the stream. */
3
+ export type VoiceActivityEvent = Readonly<{
4
+ kind: 'activity';
5
+ rms: number;
6
+ /** The speech detector and the level check both found speech. */
7
+ speech: boolean;
8
+ stream?: never;
9
+ }>;
10
+ export type VoiceCaptureEvent = {
11
+ readonly kind: 'opening';
12
+ readonly stream?: never;
13
+ } | {
14
+ readonly kind: 'listening';
15
+ readonly stream?: never;
16
+ } | VoiceActivityEvent | {
17
+ readonly kind: 'processing';
18
+ readonly stream?: never;
19
+ } | {
20
+ readonly kind: 'measured';
21
+ readonly quality: VoiceCaptureQuality;
22
+ readonly stream?: never;
23
+ };
24
+ export type VoiceCaptureObserver = (event: VoiceCaptureEvent) => void;
25
+ export type VoiceCaptureQuality = Readonly<{
26
+ attemptId: string;
27
+ capturedAtMs: number;
28
+ durationMs: number;
29
+ recordingMs: number;
30
+ endedBy: 'pause' | 'limit' | 'stream_ended';
31
+ voicedMs: number;
32
+ peakFrameRms: number;
33
+ levelThreshold: number;
34
+ requiredVoicedMs: number;
35
+ microphone: string;
36
+ microphoneSelection: VoiceMicrophone['selection'];
37
+ processing: VoiceMicrophone['processing'];
38
+ backgroundRms: number;
39
+ speechToBackgroundDb: number | null;
40
+ noise: 'high' | 'normal' | 'unknown';
41
+ }>;
42
+ export type VoiceRecordingResult = {
43
+ kind: 'captured';
44
+ samples: Float32Array;
45
+ quality: VoiceCaptureQuality;
46
+ /** `performance.now()` of the last voiced frame, or null when no speech was detected. */
47
+ lastSoundAtMs: number | null;
48
+ } | {
49
+ kind: 'insufficient_audio';
50
+ quality: VoiceCaptureQuality;
51
+ samples?: never;
52
+ };
53
+ export declare function voiceQualityMessage(quality: VoiceCaptureQuality): string;
54
+ /** Carries a typed capture rejection across the SDK's exception-based UI action boundary. */
55
+ export declare class InsufficientVoiceAudioError extends Error {
56
+ readonly quality: VoiceCaptureQuality;
57
+ constructor(quality: VoiceCaptureQuality);
58
+ }
59
+ export type VoiceRecordingOptions = Readonly<{
60
+ /**
61
+ * The voiced time of the whole expected sentence, when the caller knows the sentence
62
+ * (`expectedSpeechMs`): after that much speech, a shorter pause ends the recording.
63
+ */
64
+ expectedSpeechMs?: number | null;
65
+ /**
66
+ * `model` (default): the speech detection model finds the speech. It needs the model and the
67
+ * model engine from the asset folder. `level`: only the sound level counts. It needs no model,
68
+ * and it cannot tell speech from other loud sound.
69
+ */
70
+ detector?: 'model' | 'level';
71
+ }>;
72
+ /** Throws unless the page is in front and the person just acted on it (a click or a key). */
73
+ export declare function requireVoiceUserAction(): void;
74
+ /** Throws unless the page is visible and has the focus. */
75
+ export declare function requireVoiceForeground(): void;
76
+ /**
77
+ * Records one utterance at 16 kHz. The recording ends after a pause, or after 8 s. A recording
78
+ * with less than 0.6 s of speech is `insufficient_audio`. The samples hold the speech with 0.2 s
79
+ * of margin on each side.
80
+ */
81
+ export declare function recordVoice(signal: AbortSignal, onCapture: VoiceCaptureObserver, options?: VoiceRecordingOptions): Promise<VoiceRecordingResult>;
@@ -0,0 +1,19 @@
1
+ import { type VoiceMicrophonePreference } from './config';
2
+ export type VoiceMicrophone = Readonly<{
3
+ stream: MediaStream;
4
+ preference: VoiceMicrophonePreference;
5
+ selection: 'default' | 'preferred' | 'replacement';
6
+ label: string;
7
+ processing: Readonly<{
8
+ noiseSuppression: boolean | null;
9
+ echoCancellation: boolean | string | null;
10
+ autoGainControl: boolean | null;
11
+ sampleRate: number | null;
12
+ }>;
13
+ }>;
14
+ export declare function openVoiceMicrophone(signal: AbortSignal): Promise<VoiceMicrophone>;
15
+ export declare function rememberVoiceMicrophone(microphone: VoiceMicrophone): Promise<void>;
16
+ export declare function useSystemVoiceMicrophone(): Promise<void>;
17
+ export declare class VoiceMicrophoneInterruptedError extends Error {
18
+ constructor();
19
+ }
@@ -0,0 +1,8 @@
1
+ /** The silence that ends a recording after `voicedMs` of speech. */
2
+ export declare function endPauseMs(voicedMs: number, expectedSpeechMs: number | null): number;
3
+ /**
4
+ * The least voiced time of the whole `sentence` as people read it aloud. Digits, dots and dashes
5
+ * are words of their own: "Send 0.01 NEAR to seams-v1.testnet" is read as 13 words ("send zero
6
+ * point zero one near to seams dash v one dot testnet").
7
+ */
8
+ export declare function expectedSpeechMs(sentence: string): number;
@@ -0,0 +1,15 @@
1
+ import { BrowserInferenceModel } from './inference';
2
+ export declare const voiceActivityModel: BrowserInferenceModel;
3
+ /** Judges 32 ms frames of 16 kHz audio. `clear` ends one stream of frames. */
4
+ export type SpeechDetector = Readonly<{
5
+ /** The probability from 0 to 1 that the 512-sample frame holds speech. */
6
+ probability(frame: Float32Array): Promise<number>;
7
+ clear(): void;
8
+ }>;
9
+ /** The speech detection model. It downloads the model and the model engine on first use. */
10
+ export declare function createVoiceActivity(): Promise<SpeechDetector>;
11
+ /**
12
+ * A detector with no model and no download: every frame counts as speech, so only the level
13
+ * check of the recorder decides. It cannot tell speech from other loud sound.
14
+ */
15
+ export declare const levelDetector: SpeechDetector;
@@ -0,0 +1,9 @@
1
+ export declare const SAMPLE_RATE = 16000;
2
+ /** The Speak-to-Sign verifier refuses audio longer than 8 s. */
3
+ export declare const MAX_SAMPLES: number;
4
+ /**
5
+ * 16 kHz mono PCM16 with the plain 44-byte header that the Speak-to-Sign verifier writes for its
6
+ * canonical WAV, so the SHA-256 of these bytes is the audio digest of a verdict. Samples after
7
+ * 8 s are left out.
8
+ */
9
+ export declare function pcm16Wav(samples: Float32Array): Uint8Array<ArrayBuffer>;
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@seams/voice-capture",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.67.0",
4
+ "type": "module",
5
+ "description": "Records one spoken sentence in the browser, with speech detection, a quality check and a phrase card",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/seams-tech/speak-to-sign.git",
10
+ "directory": "packages/voice-capture"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/types/index.d.ts",
15
+ "import": "./dist/esm/index.js"
16
+ },
17
+ "./card": {
18
+ "types": "./dist/types/card/index.d.ts",
19
+ "import": "./dist/esm/card/index.js"
20
+ },
21
+ "./hosting": {
22
+ "types": "./dist/hosting.d.mts",
23
+ "import": "./dist/hosting.mjs"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "dist/",
29
+ "README.md",
30
+ "LICENSE.md",
31
+ "!dist/**/*.map"
32
+ ],
33
+ "sideEffects": false,
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "build": "node scripts/build.mjs && tsrs -p tsconfig.build.json",
39
+ "type-check": "tsrs --noEmit",
40
+ "test": "playwright test",
41
+ "example": "node scripts/serve-example.mjs"
42
+ },
43
+ "dependencies": {
44
+ "onnxruntime-web": "1.30.0",
45
+ "preact": "10.29.8"
46
+ },
47
+ "devDependencies": {
48
+ "@playwright/test": "1.56.1",
49
+ "@types/node": "24.2.0",
50
+ "@types/react": "19.2.18",
51
+ "rolldown": "1.0.0-rc.9",
52
+ "tsrs": "0.18.0-ts7.1.0-dev.20260929",
53
+ "voice-glow": "0.2.1"
54
+ },
55
+ "engines": {
56
+ "node": ">=24"
57
+ },
58
+ "packageManager": "pnpm@11.11.0+sha512.4463f65fd80ed80d69bc1d4bf163ee94f605c7380fc318bb5b2ebe15f8cd12d49c51a4d59e951b401e764d3b6ca751cbf51bc50ed7001a6bcb4935e684c34882"
59
+ }