@vocalstack/js-sdk 1.0.13 → 2.0.0-alpha.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 (73) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +112 -69
  3. package/SKILL.md +252 -0
  4. package/dist/errors.d.ts +40 -0
  5. package/dist/errors.js +74 -0
  6. package/dist/file.d.ts +2 -0
  7. package/dist/file.js +38 -0
  8. package/dist/http.d.ts +75 -0
  9. package/dist/http.js +122 -0
  10. package/dist/index.d.ts +56 -2
  11. package/dist/index.js +65 -17
  12. package/dist/mic.d.ts +75 -0
  13. package/dist/mic.js +258 -0
  14. package/dist/realtime.d.ts +132 -0
  15. package/dist/realtime.js +450 -0
  16. package/dist/resources.d.ts +521 -0
  17. package/dist/resources.js +787 -0
  18. package/dist/subscription.d.ts +55 -0
  19. package/dist/subscription.js +164 -0
  20. package/dist/ws.d.ts +12 -0
  21. package/dist/ws.js +15 -0
  22. package/package.json +32 -36
  23. package/skills/vocalstack/SKILL.md +252 -0
  24. package/dist/RestApi.d.ts +0 -21
  25. package/dist/RestApi.js +0 -94
  26. package/dist/VocalStackApi.d.ts +0 -20
  27. package/dist/VocalStackApi.js +0 -51
  28. package/dist/WebsocketApi.d.ts +0 -38
  29. package/dist/WebsocketApi.js +0 -150
  30. package/dist/apis/Billing.d.ts +0 -59
  31. package/dist/apis/Billing.js +0 -21
  32. package/dist/apis/LiveTranscription.d.ts +0 -58
  33. package/dist/apis/LiveTranscription.js +0 -55
  34. package/dist/apis/Polyglot.d.ts +0 -113
  35. package/dist/apis/Polyglot.js +0 -68
  36. package/dist/apis/Security.d.ts +0 -25
  37. package/dist/apis/Security.js +0 -35
  38. package/dist/apis/Transcriptions.d.ts +0 -61
  39. package/dist/apis/Transcriptions.js +0 -43
  40. package/dist/apis/UrlTranscription.d.ts +0 -59
  41. package/dist/apis/UrlTranscription.js +0 -53
  42. package/dist/apis/index.d.ts +0 -6
  43. package/dist/apis/index.js +0 -22
  44. package/dist/esm/ApiVersion.d.ts +0 -2
  45. package/dist/esm/ApiVersion.js +0 -28
  46. package/dist/esm/RestApi.d.ts +0 -21
  47. package/dist/esm/RestApi.js +0 -91
  48. package/dist/esm/VocalStackApi.d.ts +0 -19
  49. package/dist/esm/VocalStackApi.js +0 -46
  50. package/dist/esm/WebsocketApi.d.ts +0 -38
  51. package/dist/esm/WebsocketApi.js +0 -146
  52. package/dist/esm/apis/Billing.d.ts +0 -59
  53. package/dist/esm/apis/Billing.js +0 -17
  54. package/dist/esm/apis/LiveTranscription.d.ts +0 -58
  55. package/dist/esm/apis/LiveTranscription.js +0 -51
  56. package/dist/esm/apis/Polyglot.d.ts +0 -113
  57. package/dist/esm/apis/Polyglot.js +0 -64
  58. package/dist/esm/apis/Security.d.ts +0 -25
  59. package/dist/esm/apis/Security.js +0 -31
  60. package/dist/esm/apis/Transcriptions.d.ts +0 -61
  61. package/dist/esm/apis/Transcriptions.js +0 -39
  62. package/dist/esm/apis/UrlTranscription.d.ts +0 -59
  63. package/dist/esm/apis/UrlTranscription.js +0 -49
  64. package/dist/esm/apis/index.d.ts +0 -6
  65. package/dist/esm/apis/index.js +0 -6
  66. package/dist/esm/generics.zod.d.ts +0 -77
  67. package/dist/esm/generics.zod.js +0 -45
  68. package/dist/esm/index.d.ts +0 -2
  69. package/dist/esm/index.js +0 -2
  70. package/dist/esm/version.d.ts +0 -1
  71. package/dist/esm/version.js +0 -1
  72. package/dist/generics.zod.d.ts +0 -77
  73. package/dist/generics.zod.js +0 -48
package/dist/mic.js ADDED
@@ -0,0 +1,258 @@
1
+ // The worklet runs on the browser's realtime audio thread and does one
2
+ // allocation-light thing: post every 128-frame block to the main thread.
3
+ // MessagePort queues are unbounded and never overwrite, so a busy main
4
+ // thread (a big transcript re-render) delivers audio late, never drops it.
5
+ const WORKLET_SOURCE = `
6
+ class PCMProcessor extends AudioWorkletProcessor {
7
+ process(inputs) {
8
+ const input = inputs[0];
9
+ if (!input || input.length === 0) return true;
10
+ const ch = input[0];
11
+ if (!ch || ch.length === 0) return true;
12
+ this.port.postMessage(ch.slice(0));
13
+ return true;
14
+ }
15
+ }
16
+ registerProcessor('pcm-processor', PCMProcessor);
17
+ `;
18
+ /** Retry cadence for context resume / track re-acquire while stalled. */
19
+ const RECOVER_MS = 1000;
20
+ /** Capture clock heartbeat cadence. */
21
+ const CLOCK_MS = 500;
22
+ export async function captureMicrophone(onChunk, opts = {}) {
23
+ if (typeof navigator === 'undefined' || !navigator.mediaDevices) {
24
+ throw new Error('captureMicrophone is browser-only (no mediaDevices)');
25
+ }
26
+ const targetRate = opts.sampleRate ?? 16000;
27
+ const chunkSamples = Math.round((opts.chunkSeconds ?? 0.25) * targetRate);
28
+ const constraints = opts.audio ?? {
29
+ channelCount: 1,
30
+ echoCancellation: true,
31
+ noiseSuppression: true,
32
+ };
33
+ let muted = false;
34
+ let media = await navigator.mediaDevices.getUserMedia({ audio: constraints });
35
+ // Ask for the target rate directly: Chrome/Firefox/Safari 14.1+ then
36
+ // resample on the audio thread and our converter is a pass-through.
37
+ // Browsers that refuse the option fall back to the default rate and the
38
+ // linear-interpolation resampler below.
39
+ let ctx;
40
+ try {
41
+ ctx = new AudioContext({ sampleRate: targetRate });
42
+ }
43
+ catch {
44
+ ctx = new AudioContext();
45
+ }
46
+ const workletUrl = URL.createObjectURL(new Blob([WORKLET_SOURCE], { type: 'application/javascript' }));
47
+ try {
48
+ await ctx.audioWorklet.addModule(workletUrl);
49
+ }
50
+ finally {
51
+ URL.revokeObjectURL(workletUrl);
52
+ }
53
+ const node = new AudioWorkletNode(ctx, 'pcm-processor');
54
+ let source = ctx.createMediaStreamSource(media);
55
+ source.connect(node);
56
+ const inputRate = ctx.sampleRate;
57
+ const passThrough = inputRate === targetRate;
58
+ let buffered = [];
59
+ let readPos = 0;
60
+ let pending = null;
61
+ // Capture clock: cumulative resampled output samples vs wall time.
62
+ const t0 = performance.now();
63
+ let outSamples = 0;
64
+ // Zero-run detector state (input-rate samples).
65
+ const silentLimit = Math.round((opts.silentSeconds ?? 3) * inputRate);
66
+ let zeroRun = 0;
67
+ let silentFlagged = false;
68
+ // Stall bookkeeping: one active reason at a time; `onRecover` fires
69
+ // once when it clears.
70
+ let stopped = false;
71
+ let stall = null;
72
+ let recoverTimer = null;
73
+ const setStall = (reason) => {
74
+ if (stopped || stall === reason)
75
+ return;
76
+ stall = reason;
77
+ opts.onStall?.(reason);
78
+ };
79
+ const clearStall = () => {
80
+ if (stall === null)
81
+ return;
82
+ stall = null;
83
+ opts.onRecover?.();
84
+ };
85
+ node.port.onmessage = (ev) => {
86
+ const block = ev.data;
87
+ if (silentLimit > 0) {
88
+ let allZero = true;
89
+ for (const v of block) {
90
+ if (v !== 0) {
91
+ allZero = false;
92
+ break;
93
+ }
94
+ }
95
+ if (allZero) {
96
+ zeroRun += block.length;
97
+ if (!silentFlagged && zeroRun >= silentLimit) {
98
+ silentFlagged = true;
99
+ setStall('silent');
100
+ }
101
+ }
102
+ else {
103
+ zeroRun = 0;
104
+ if (silentFlagged) {
105
+ silentFlagged = false;
106
+ if (stall === 'silent')
107
+ clearStall();
108
+ }
109
+ }
110
+ }
111
+ let out;
112
+ if (passThrough) {
113
+ out = block;
114
+ }
115
+ else {
116
+ for (const v of block)
117
+ buffered.push(v);
118
+ const step = inputRate / targetRate;
119
+ const res = [];
120
+ while (readPos + 1 < buffered.length) {
121
+ const i = Math.floor(readPos);
122
+ const frac = readPos - i;
123
+ res.push(buffered[i] * (1 - frac) + buffered[i + 1] * frac);
124
+ readPos += step;
125
+ }
126
+ const keepFrom = Math.max(0, Math.floor(readPos) - 1);
127
+ buffered = buffered.slice(keepFrom);
128
+ readPos -= keepFrom;
129
+ out = res;
130
+ }
131
+ if (out.length) {
132
+ outSamples += out.length;
133
+ // Samples flowing again clears a track/context stall; the silent
134
+ // stall clears only on a nonzero sample (handled above).
135
+ if (stall !== null && stall !== 'silent')
136
+ clearStall();
137
+ pending = pending ? concat(pending, out) : Float32Array.from(out);
138
+ while (pending.length >= chunkSamples) {
139
+ const chunk = pending.slice(0, chunkSamples);
140
+ // Silence, not nothing: see `MicCapture.setMuted`.
141
+ onChunk(muted ? new Float32Array(chunk.length) : chunk);
142
+ pending = pending.slice(chunkSamples);
143
+ }
144
+ }
145
+ };
146
+ // Heartbeat on a timer so a stalled capture is still reported: pcm
147
+ // stops while wall advances, and the server declares the gap.
148
+ const clockTimer = opts.onClock
149
+ ? setInterval(() => {
150
+ opts.onClock?.(outSamples, (performance.now() - t0) / 1000);
151
+ }, CLOCK_MS)
152
+ : null;
153
+ // --- recovery -------------------------------------------------------
154
+ const scheduleRecover = (fn) => {
155
+ if (stopped || recoverTimer)
156
+ return;
157
+ recoverTimer = setTimeout(() => {
158
+ recoverTimer = null;
159
+ fn();
160
+ }, RECOVER_MS);
161
+ };
162
+ const resumeContext = () => {
163
+ if (stopped || ctx.state !== 'suspended')
164
+ return;
165
+ void ctx.resume().catch(() => { }).then(() => {
166
+ if (!stopped && ctx.state === 'suspended')
167
+ scheduleRecover(resumeContext);
168
+ });
169
+ };
170
+ const reacquireTrack = () => {
171
+ if (stopped)
172
+ return;
173
+ navigator.mediaDevices
174
+ .getUserMedia({ audio: constraints })
175
+ .then((next) => {
176
+ if (stopped) {
177
+ for (const t of next.getTracks())
178
+ t.stop();
179
+ return;
180
+ }
181
+ source.disconnect();
182
+ for (const t of media.getTracks())
183
+ t.stop();
184
+ media = next;
185
+ source = ctx.createMediaStreamSource(media);
186
+ source.connect(node);
187
+ watchTracks(media);
188
+ // `onRecover` fires from the first delivered block.
189
+ })
190
+ .catch(() => scheduleRecover(reacquireTrack));
191
+ };
192
+ const watchTracks = (stream) => {
193
+ for (const track of stream.getAudioTracks()) {
194
+ track.addEventListener('ended', () => {
195
+ if (stopped)
196
+ return;
197
+ setStall('track_ended');
198
+ scheduleRecover(reacquireTrack);
199
+ });
200
+ track.addEventListener('mute', () => {
201
+ if (!stopped)
202
+ setStall('track_muted');
203
+ });
204
+ // `unmute` needs no handler: the next block clears the stall.
205
+ }
206
+ };
207
+ watchTracks(media);
208
+ ctx.addEventListener('statechange', () => {
209
+ if (stopped)
210
+ return;
211
+ if (ctx.state === 'suspended') {
212
+ setStall('context_suspended');
213
+ resumeContext();
214
+ }
215
+ else if (ctx.state === 'closed') {
216
+ setStall('context_closed');
217
+ }
218
+ });
219
+ return {
220
+ sampleRate: targetRate,
221
+ get device() {
222
+ const t = media.getAudioTracks()[0];
223
+ const st = (t?.getSettings?.() ?? {});
224
+ return { label: t?.label ?? '', sampleRate: st.sampleRate, channelCount: st.channelCount, noiseSuppression: st.noiseSuppression, echoCancellation: st.echoCancellation };
225
+ },
226
+ get muted() {
227
+ return muted;
228
+ },
229
+ setMuted(on) {
230
+ muted = !!on;
231
+ },
232
+ async stop() {
233
+ stopped = true;
234
+ if (clockTimer)
235
+ clearInterval(clockTimer);
236
+ if (recoverTimer)
237
+ clearTimeout(recoverTimer);
238
+ node.port.onmessage = null;
239
+ source.disconnect();
240
+ node.disconnect();
241
+ for (const t of media.getTracks())
242
+ t.stop();
243
+ await ctx.close();
244
+ },
245
+ };
246
+ }
247
+ export async function streamMicrophone(session, opts = {}) {
248
+ return captureMicrophone((pcm) => session.sendAudio(pcm), {
249
+ onClock: (pcm, wall) => session.sendClock(pcm, wall),
250
+ ...opts,
251
+ });
252
+ }
253
+ function concat(a, b) {
254
+ const out = new Float32Array(a.length + b.length);
255
+ out.set(a, 0);
256
+ out.set(b, a.length);
257
+ return out;
258
+ }
@@ -0,0 +1,132 @@
1
+ import type { Capabilities, CommittedSegment, CreateSessionResponse, Fix, LatencyMode, SentenceSegment, ServerFrame, SessionOptions, SessionStats, SpeakerLabels } from '@vocalstack/contract';
2
+ import { VocalStackError } from './errors.js';
3
+ import { type FileInput } from './file.js';
4
+ import type { CallOptions, Transport } from './http.js';
5
+ export type SessionEvent = Extract<ServerFrame, {
6
+ type: 'transcript.committed' | 'transcript.draft' | 'transcript.words' | 'transcript.translated' | 'transcript.amended' | 'transcript.latency' | 'transcript.speaker' | 'transcript.sentence' | 'transcript.progress';
7
+ }>;
8
+ export interface ConnectParams {
9
+ sessionId: string;
10
+ streamUrl: string;
11
+ options?: SessionOptions;
12
+ readyDeadlineMs?: number;
13
+ /** Fires on every `session.loading` frame while the engine warms up
14
+ * (model download / load after a machine wake) — lets a UI show
15
+ * "warming up" instead of a generic connecting state. Each frame also
16
+ * re-arms the ready deadline, so a long cold load only fails once the
17
+ * engine goes silent, not merely slow. */
18
+ onLoading?(stage: 'download' | 'model'): void;
19
+ /** Every server text frame as it arrives, parsed, before the iterator
20
+ * sees it: the lifecycle frames the iterator keeps to itself
21
+ * (`session.loading`, `session.ready`, `session.done`, `error`) and the
22
+ * transcript frames alike. For logs and protocol views; the iterator
23
+ * is still the way to read the transcript. */
24
+ onFrame?(frame: ServerFrame): void;
25
+ }
26
+ /** A refused socket upgrade as an error. A 503 carrying the error body
27
+ * with `unavailable` (the database is waking; retry shortly) says so; any
28
+ * other 503 — a bare one from a saturated proxy, or no body at all — is
29
+ * `capacity_exceeded`. So is a 502 or 504 without the API's error body:
30
+ * the proxy in front of the engines found none free in time. Every other
31
+ * status is its own envelope. */
32
+ export declare function upgradeRefused(status: number | undefined, body: string): VocalStackError;
33
+ export declare class RealtimeSession implements AsyncIterable<SessionEvent> {
34
+ #private;
35
+ readonly sessionId: string;
36
+ private constructor();
37
+ static connect(params: ConnectParams): Promise<RealtimeSession>;
38
+ sendAudio(chunk: Float32Array | ArrayBuffer | Uint8Array): void;
39
+ sendFile(source: AsyncIterable<Uint8Array>, chunkBytes?: number): Promise<void>;
40
+ pipe(source: AsyncIterable<Float32Array | ArrayBuffer | Uint8Array> | ReadableStream<Uint8Array>): Promise<void>;
41
+ /**
42
+ * Capture clock heartbeat: `pcm` = cumulative samples captured so far
43
+ * (16kHz-equivalent), `wall` = seconds since capture start. Lets the
44
+ * server detect capture loss (throttled tab, device switch) and mark
45
+ * the transcript with honest gaps instead of gluing across them.
46
+ * `streamMicrophone`/`captureMicrophone` call this automatically.
47
+ */
48
+ sendClock(pcm: number, wall: number): void;
49
+ /**
50
+ * Options update during the realtime session. Omitted fields are unchanged.
51
+ * `latencyMode` switches the decode model policy from the next
52
+ * engine step; `spokenLanguage` changes the transcription language from the
53
+ * next engine step (`''` or `'auto'` = auto-detect); `speakerLabels`
54
+ * turns speaker labelling on (`'on'` or `'auto'`) or off (`'off'`) from the next
55
+ * engine step — enabling labels speakers from that point on;
56
+ * `translate` replaces the whole target-language list (`[]` turns
57
+ * translation off). Applies to subsequent transcript —
58
+ * already-delivered segments and settled sentences are not
59
+ * re-transcribed, re-attributed or re-translated.
60
+ */
61
+ update(changes: {
62
+ latencyMode?: LatencyMode;
63
+ spokenLanguage?: string;
64
+ speakerLabels?: SpeakerLabels;
65
+ translate?: string[];
66
+ }): void;
67
+ eof(): void;
68
+ cancel(): void;
69
+ get doneReason(): 'eof' | 'max_duration' | 'cancelled' | null;
70
+ get stats(): SessionStats | null;
71
+ close(): void;
72
+ [Symbol.asyncIterator](): AsyncIterator<SessionEvent>;
73
+ [Symbol.dispose](): void;
74
+ }
75
+ export interface ConnectOptions {
76
+ /** See {@link ConnectParams.onLoading}: engine warm-up progress. */
77
+ onLoading?(stage: 'download' | 'model'): void;
78
+ /** See {@link ConnectParams.onFrame}: every server frame, lifecycle included. */
79
+ onFrame?(frame: ServerFrame): void;
80
+ /** Overrides the client-wide `readyDeadlineMs` for this socket. */
81
+ readyDeadlineMs?: number;
82
+ }
83
+ export interface TranscribeOptions extends Omit<SessionOptions, 'input' | 'audio'> {
84
+ /** The realtime session, once connected (to `cancel()` it, say). */
85
+ onSession?(session: RealtimeSession): void;
86
+ onProgress?(progress: {
87
+ processedS: number;
88
+ totalS?: number;
89
+ }): void;
90
+ onCommit?(segment: CommittedSegment): void;
91
+ /** A committed segment's translation arrived; the segment (by `seq`)
92
+ * already has its `translations` filled in. */
93
+ onTranslated?(seq: number, translations: Record<string, string>, sources?: Record<string, string>): void;
94
+ /** The engine rewrote a committed segment; it is patched in place first. */
95
+ onAmended?(seq: number, text: string, speaker: number, fixes: Fix[]): void;
96
+ /** A tentative speaker label (`speakerProvisional`) settled: `verdict` is
97
+ * "confirmed", "amended" or "unverified". The segment is patched first. */
98
+ onSpeakerSettled?(seq: number, speaker: number, verdict: string, confidence?: number): void;
99
+ /** Every sentence upsert (`sentences.enabled`); the returned `sentences`
100
+ * list is kept up to date (dropped sentences are removed). */
101
+ onSentence?(sentence: SentenceSegment): void;
102
+ readyDeadlineMs?: number;
103
+ }
104
+ export interface RealtimeTranscript {
105
+ segments: CommittedSegment[];
106
+ /** Sentence upserts folded into their final state (empty unless
107
+ * `sentences.enabled`). */
108
+ sentences: SentenceSegment[];
109
+ text: string;
110
+ doneReason: 'eof' | 'max_duration' | 'cancelled' | null;
111
+ stats: SessionStats | null;
112
+ }
113
+ /** `vs.realtime`: a realtime session is a signed token plus one socket.
114
+ * Nothing is stored: the transcript is what the socket sends. */
115
+ export declare class Realtime {
116
+ #private;
117
+ constructor(t: Transport);
118
+ /** `POST /v1/realtime/sessions`: validate the options, get a signed `streamUrl`. */
119
+ createSession(options?: SessionOptions, call?: CallOptions): Promise<CreateSessionResponse>;
120
+ /** `createSession`, then open the socket and wait for `session.ready`. */
121
+ connect(options?: SessionOptions, connect?: ConnectOptions): Promise<RealtimeSession>;
122
+ /** `GET /v1/realtime/capabilities`: languages, audio formats, limits. */
123
+ capabilities(): Promise<Capabilities>;
124
+ /** Stream an encoded file (mp3, wav, flac, ogg, m4a…) through a realtime
125
+ * session and collect the transcript. Nothing is stored; for a stored,
126
+ * shareable transcription use `vs.transcriptions.create`. */
127
+ transcribeFile(file: FileInput, opts?: TranscribeOptions): Promise<RealtimeTranscript>;
128
+ /** The same for a link the engine fetches itself. */
129
+ transcribeUrl(url: string, opts?: TranscribeOptions): Promise<RealtimeTranscript>;
130
+ }
131
+ /** Upsert one sentence frame into a list kept in sentenceId order. */
132
+ export declare function applySentence(list: SentenceSegment[], sentence: SentenceSegment): SentenceSegment[];