@vocalstack/js-sdk 1.1.0 → 2.0.0-alpha.1
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.
- package/LICENSE +21 -21
- package/README.md +112 -65
- package/SKILL.md +249 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +56 -0
- package/dist/file.d.ts +2 -0
- package/dist/file.js +38 -0
- package/dist/http.d.ts +43 -0
- package/dist/http.js +124 -0
- package/dist/index.d.ts +42 -2
- package/dist/index.js +51 -17
- package/dist/mic.d.ts +27 -0
- package/dist/mic.js +236 -0
- package/dist/realtime.d.ts +77 -0
- package/dist/realtime.js +490 -0
- package/dist/resources.d.ts +374 -0
- package/dist/resources.js +712 -0
- package/dist/subscription.d.ts +38 -0
- package/dist/subscription.js +153 -0
- package/dist/ws.d.ts +9 -0
- package/dist/ws.js +12 -0
- package/package.json +32 -36
- package/skills/vocalstack/SKILL.md +249 -0
- package/dist/ApiVersion.d.ts +0 -2
- package/dist/ApiVersion.js +0 -32
- package/dist/RestApi.d.ts +0 -21
- package/dist/RestApi.js +0 -95
- package/dist/VocalStackApi.d.ts +0 -19
- package/dist/VocalStackApi.js +0 -60
- package/dist/WebsocketApi.d.ts +0 -38
- package/dist/WebsocketApi.js +0 -150
- package/dist/apis/Billing.d.ts +0 -59
- package/dist/apis/Billing.js +0 -21
- package/dist/apis/LiveTranscription.d.ts +0 -58
- package/dist/apis/LiveTranscription.js +0 -55
- package/dist/apis/Polyglot.d.ts +0 -113
- package/dist/apis/Polyglot.js +0 -68
- package/dist/apis/Security.d.ts +0 -25
- package/dist/apis/Security.js +0 -35
- package/dist/apis/Transcriptions.d.ts +0 -60
- package/dist/apis/Transcriptions.js +0 -38
- package/dist/apis/UrlTranscription.d.ts +0 -59
- package/dist/apis/UrlTranscription.js +0 -53
- package/dist/apis/index.d.ts +0 -6
- package/dist/apis/index.js +0 -22
- package/dist/esm/ApiVersion.d.ts +0 -2
- package/dist/esm/ApiVersion.js +0 -28
- package/dist/esm/RestApi.d.ts +0 -21
- package/dist/esm/RestApi.js +0 -91
- package/dist/esm/VocalStackApi.d.ts +0 -19
- package/dist/esm/VocalStackApi.js +0 -56
- package/dist/esm/WebsocketApi.d.ts +0 -38
- package/dist/esm/WebsocketApi.js +0 -146
- package/dist/esm/apis/Billing.d.ts +0 -59
- package/dist/esm/apis/Billing.js +0 -17
- package/dist/esm/apis/LiveTranscription.d.ts +0 -58
- package/dist/esm/apis/LiveTranscription.js +0 -51
- package/dist/esm/apis/Polyglot.d.ts +0 -113
- package/dist/esm/apis/Polyglot.js +0 -64
- package/dist/esm/apis/Security.d.ts +0 -25
- package/dist/esm/apis/Security.js +0 -31
- package/dist/esm/apis/Transcriptions.d.ts +0 -60
- package/dist/esm/apis/Transcriptions.js +0 -34
- package/dist/esm/apis/UrlTranscription.d.ts +0 -59
- package/dist/esm/apis/UrlTranscription.js +0 -49
- package/dist/esm/apis/index.d.ts +0 -6
- package/dist/esm/apis/index.js +0 -6
- package/dist/esm/generics.zod.d.ts +0 -83
- package/dist/esm/generics.zod.js +0 -47
- package/dist/esm/index.d.ts +0 -2
- package/dist/esm/index.js +0 -2
- package/dist/generics.zod.d.ts +0 -83
- package/dist/generics.zod.js +0 -50
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type Query = Record<string, string | number | boolean | undefined | null>;
|
|
2
|
+
export interface ClientOptions {
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
token?: string | (() => string | undefined | null | Promise<string | undefined | null>);
|
|
6
|
+
actingPrincipal?: string;
|
|
7
|
+
deviceId?: string;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
readyDeadlineMs?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface CallOptions {
|
|
13
|
+
idempotencyKey?: string;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
export interface ReadAs {
|
|
17
|
+
listenerToken?: string;
|
|
18
|
+
shareCode?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function listenerQuery(as: ReadAs | undefined): Query;
|
|
21
|
+
export interface RequestInit2 extends CallOptions {
|
|
22
|
+
query?: Query;
|
|
23
|
+
body?: unknown;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
anonymous?: boolean;
|
|
26
|
+
noRetry?: boolean;
|
|
27
|
+
oauthErrors?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare const DEFAULT_BASE_URL = "https://api.vocalstack.com";
|
|
30
|
+
export declare function qs(q: Query | undefined): string;
|
|
31
|
+
export declare const seg: (s: string | number) => string;
|
|
32
|
+
export declare function retries(status: number, code: string, method: string, idempotencyKey?: string): boolean;
|
|
33
|
+
export declare class Transport {
|
|
34
|
+
#private;
|
|
35
|
+
readonly baseUrl: string;
|
|
36
|
+
readonly opts: ClientOptions;
|
|
37
|
+
constructor(opts: ClientOptions);
|
|
38
|
+
bearer(): Promise<string | undefined>;
|
|
39
|
+
url(path: string, query?: Query): string;
|
|
40
|
+
wsUrl(path: string, query?: Query): string;
|
|
41
|
+
raw(method: string, path: string, init?: RequestInit2): Promise<Response>;
|
|
42
|
+
json<T>(method: string, path: string, init?: RequestInit2): Promise<T>;
|
|
43
|
+
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { VocalStackError, fromEnvelope, fromOAuth } from './errors.js';
|
|
2
|
+
export function listenerQuery(as) {
|
|
3
|
+
return { listenerToken: as?.listenerToken, shareCode: as?.shareCode };
|
|
4
|
+
}
|
|
5
|
+
export const DEFAULT_BASE_URL = 'https://api.vocalstack.com';
|
|
6
|
+
export function qs(q) {
|
|
7
|
+
if (!q)
|
|
8
|
+
return '';
|
|
9
|
+
const parts = [];
|
|
10
|
+
for (const [k, v] of Object.entries(q)) {
|
|
11
|
+
if (v === undefined || v === null || v === '')
|
|
12
|
+
continue;
|
|
13
|
+
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
14
|
+
}
|
|
15
|
+
return parts.length ? `?${parts.join('&')}` : '';
|
|
16
|
+
}
|
|
17
|
+
export const seg = (s) => encodeURIComponent(String(s));
|
|
18
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
19
|
+
const UNAVAILABLE_WAIT_S = 2;
|
|
20
|
+
export function retries(status, code, method, idempotencyKey) {
|
|
21
|
+
if (status === 429)
|
|
22
|
+
return true;
|
|
23
|
+
if (status !== 503 || code !== 'unavailable')
|
|
24
|
+
return false;
|
|
25
|
+
const m = method.toUpperCase();
|
|
26
|
+
return m === 'GET' || m === 'HEAD' || m === 'DELETE' || !!idempotencyKey;
|
|
27
|
+
}
|
|
28
|
+
export class Transport {
|
|
29
|
+
baseUrl;
|
|
30
|
+
opts;
|
|
31
|
+
#fetch;
|
|
32
|
+
constructor(opts) {
|
|
33
|
+
this.baseUrl = (opts.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
34
|
+
if (opts.apiKey && opts.token)
|
|
35
|
+
throw new TypeError('VocalStack: pass apiKey (a server) or token (a browser), not both');
|
|
36
|
+
this.opts = opts;
|
|
37
|
+
this.#fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
38
|
+
}
|
|
39
|
+
async bearer() {
|
|
40
|
+
if (this.opts.apiKey)
|
|
41
|
+
return this.opts.apiKey;
|
|
42
|
+
const t = this.opts.token;
|
|
43
|
+
const v = typeof t === 'function' ? await t() : t;
|
|
44
|
+
return v ?? undefined;
|
|
45
|
+
}
|
|
46
|
+
url(path, query) {
|
|
47
|
+
return `${this.baseUrl}${path}${qs(query)}`;
|
|
48
|
+
}
|
|
49
|
+
wsUrl(path, query) {
|
|
50
|
+
return `${this.baseUrl.replace(/^http/, 'ws')}${path}${qs(query)}`;
|
|
51
|
+
}
|
|
52
|
+
async raw(method, path, init = {}) {
|
|
53
|
+
const headers = { accept: 'application/json', ...init.headers };
|
|
54
|
+
const form = typeof FormData !== 'undefined' && init.body instanceof FormData;
|
|
55
|
+
const urlencoded = init.body instanceof URLSearchParams;
|
|
56
|
+
if (urlencoded)
|
|
57
|
+
headers['content-type'] = 'application/x-www-form-urlencoded';
|
|
58
|
+
else if (init.body !== undefined && !form)
|
|
59
|
+
headers['content-type'] = 'application/json';
|
|
60
|
+
const token = init.anonymous ? undefined : await this.bearer();
|
|
61
|
+
if (token)
|
|
62
|
+
headers.authorization = `Bearer ${token}`;
|
|
63
|
+
if (this.opts.actingPrincipal)
|
|
64
|
+
headers['x-acting-principal'] = this.opts.actingPrincipal;
|
|
65
|
+
if (this.opts.deviceId)
|
|
66
|
+
headers['x-device-id'] = this.opts.deviceId;
|
|
67
|
+
if (init.idempotencyKey)
|
|
68
|
+
headers['idempotency-key'] = init.idempotencyKey;
|
|
69
|
+
const body = init.body === undefined ? undefined : form ? init.body : urlencoded ? String(init.body) : JSON.stringify(init.body);
|
|
70
|
+
const maxRetries = init.noRetry ? 0 : (this.opts.maxRetries ?? 2);
|
|
71
|
+
for (let attempt = 0;; attempt++) {
|
|
72
|
+
let res;
|
|
73
|
+
try {
|
|
74
|
+
res = await this.#fetch(this.url(path, init.query), {
|
|
75
|
+
method,
|
|
76
|
+
headers,
|
|
77
|
+
body,
|
|
78
|
+
redirect: 'manual',
|
|
79
|
+
signal: init.signal,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
if (e?.name === 'AbortError')
|
|
84
|
+
throw e;
|
|
85
|
+
const err = new VocalStackError('unavailable', 'could not reach the VocalStack API', { retryable: true });
|
|
86
|
+
err.cause = e;
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
if (res.ok || (res.status >= 300 && res.status < 400))
|
|
90
|
+
return res;
|
|
91
|
+
const text = await res.text().catch(() => '');
|
|
92
|
+
let json = null;
|
|
93
|
+
try {
|
|
94
|
+
json = text ? JSON.parse(text) : null;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
json = null;
|
|
98
|
+
}
|
|
99
|
+
const err = (init.oauthErrors ? fromOAuth : fromEnvelope)(json, res.status, res.headers);
|
|
100
|
+
if (retries(res.status, err.code, method, init.idempotencyKey) && attempt < maxRetries) {
|
|
101
|
+
const wait = err.retryAfter ?? (err.code === 'unavailable' ? UNAVAILABLE_WAIT_S : undefined);
|
|
102
|
+
if (wait !== undefined && wait <= 60) {
|
|
103
|
+
await sleep(wait * 1000);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async json(method, path, init = {}) {
|
|
111
|
+
const res = await this.raw(method, path, init);
|
|
112
|
+
if (res.status >= 300 && res.status < 400)
|
|
113
|
+
return { location: res.headers.get('location') };
|
|
114
|
+
const text = await res.text();
|
|
115
|
+
if (!text)
|
|
116
|
+
return undefined;
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(text);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new VocalStackError('internal', 'the API answered with a body that is not JSON', { status: res.status, headers: res.headers, requestId: res.headers.get('x-request-id') ?? undefined });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { type ClientOptions } from './http.js';
|
|
2
|
+
import { Realtime } from './realtime.js';
|
|
3
|
+
import { Batches, Billing, Broadcasts, Connect, Corrections, Events, Jobs, Languages, MeApi, Meta, Recordings, Rtmp, Search, ShareLinks, Tags, Transcriptions, Voices } from './resources.js';
|
|
4
|
+
export type { ClientOptions, CallOptions, ReadAs, Query } from './http.js';
|
|
5
|
+
export { DEFAULT_BASE_URL } from './http.js';
|
|
6
|
+
export { VocalStackError } from './errors.js';
|
|
7
|
+
export type { ConnectTokenErrorCode, VocalStackErrorOptions } from './errors.js';
|
|
8
|
+
export { RealtimeSession } from './realtime.js';
|
|
9
|
+
export type { ConnectParams, SessionEvent } from './realtime.js';
|
|
10
|
+
export type { FileInput } from './file.js';
|
|
11
|
+
export { Realtime, applySentence } from './realtime.js';
|
|
12
|
+
export type { ConnectOptions, TranscribeOptions, RealtimeTranscript } from './realtime.js';
|
|
13
|
+
export { Subscription } from './subscription.js';
|
|
14
|
+
export type { SubscribeOptions, SubscriptionEvent } from './subscription.js';
|
|
15
|
+
export type { WsLike } from './ws.js';
|
|
16
|
+
export { Batches, Broadcasts, Connect, Corrections, Events, Jobs, Languages, MeApi, Meta, Recordings, Rtmp, Search, ShareLinks, Tags, Transcriptions, Voices, playableFormat, } from './resources.js';
|
|
17
|
+
export type { AudioKind, MediaAuth, MediaFormat, RenderVoice } from './resources.js';
|
|
18
|
+
export type { AddBatchFileInput, ConnectToken, ConnectTokenInput, CreateTranscriptionInput, LifecycleFilter, Me, PageOptions, RecordingPageOptions, SearchOptions, Upload, UsageChargesOptions, UsageOptions } from './resources.js';
|
|
19
|
+
export type * from '@vocalstack/contract';
|
|
20
|
+
export type VocalStackOptions = ClientOptions;
|
|
21
|
+
export declare class VocalStack {
|
|
22
|
+
readonly baseUrl: string;
|
|
23
|
+
readonly realtime: Realtime;
|
|
24
|
+
readonly broadcasts: Broadcasts;
|
|
25
|
+
readonly transcriptions: Transcriptions;
|
|
26
|
+
readonly batches: Batches;
|
|
27
|
+
readonly recordings: Recordings;
|
|
28
|
+
readonly jobs: Jobs;
|
|
29
|
+
readonly corrections: Corrections;
|
|
30
|
+
readonly shareLinks: ShareLinks;
|
|
31
|
+
readonly tags: Tags;
|
|
32
|
+
readonly voices: Voices;
|
|
33
|
+
readonly search: Search;
|
|
34
|
+
readonly me: MeApi;
|
|
35
|
+
readonly connect: Connect;
|
|
36
|
+
readonly billing: Billing;
|
|
37
|
+
readonly languages: Languages;
|
|
38
|
+
readonly rtmp: Rtmp;
|
|
39
|
+
readonly events: Events;
|
|
40
|
+
readonly meta: Meta;
|
|
41
|
+
constructor(opts?: ClientOptions);
|
|
42
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { Transport } from './http.js';
|
|
2
|
+
import { Realtime } from './realtime.js';
|
|
3
|
+
import { Batches, Billing, Broadcasts, Connect, Corrections, Events, Jobs, Languages, MeApi, Meta, Recordings, Rtmp, Search, ShareLinks, Tags, Transcriptions, Voices, } from './resources.js';
|
|
4
|
+
export { DEFAULT_BASE_URL } from './http.js';
|
|
5
|
+
export { VocalStackError } from './errors.js';
|
|
6
|
+
export { RealtimeSession } from './realtime.js';
|
|
7
|
+
export { Realtime, applySentence } from './realtime.js';
|
|
8
|
+
export { Subscription } from './subscription.js';
|
|
9
|
+
export { Batches, Broadcasts, Connect, Corrections, Events, Jobs, Languages, MeApi, Meta, Recordings, Rtmp, Search, ShareLinks, Tags, Transcriptions, Voices, playableFormat, } from './resources.js';
|
|
10
|
+
export class VocalStack {
|
|
11
|
+
baseUrl;
|
|
12
|
+
realtime;
|
|
13
|
+
broadcasts;
|
|
14
|
+
transcriptions;
|
|
15
|
+
batches;
|
|
16
|
+
recordings;
|
|
17
|
+
jobs;
|
|
18
|
+
corrections;
|
|
19
|
+
shareLinks;
|
|
20
|
+
tags;
|
|
21
|
+
voices;
|
|
22
|
+
search;
|
|
23
|
+
me;
|
|
24
|
+
connect;
|
|
25
|
+
billing;
|
|
26
|
+
languages;
|
|
27
|
+
rtmp;
|
|
28
|
+
events;
|
|
29
|
+
meta;
|
|
30
|
+
constructor(opts = {}) {
|
|
31
|
+
const t = new Transport(opts);
|
|
32
|
+
this.baseUrl = t.baseUrl;
|
|
33
|
+
this.realtime = new Realtime(t);
|
|
34
|
+
this.broadcasts = new Broadcasts(t);
|
|
35
|
+
this.transcriptions = new Transcriptions(t);
|
|
36
|
+
this.batches = new Batches(t);
|
|
37
|
+
this.recordings = new Recordings(t);
|
|
38
|
+
this.jobs = new Jobs(t);
|
|
39
|
+
this.corrections = new Corrections(t);
|
|
40
|
+
this.shareLinks = new ShareLinks(t);
|
|
41
|
+
this.tags = new Tags(t);
|
|
42
|
+
this.voices = new Voices(t);
|
|
43
|
+
this.search = new Search(t);
|
|
44
|
+
this.me = new MeApi(t);
|
|
45
|
+
this.connect = new Connect(t);
|
|
46
|
+
this.billing = new Billing(t);
|
|
47
|
+
this.languages = new Languages(t);
|
|
48
|
+
this.rtmp = new Rtmp(t);
|
|
49
|
+
this.events = new Events(t);
|
|
50
|
+
this.meta = new Meta(t);
|
|
7
51
|
}
|
|
8
|
-
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./apis/index"), exports);
|
|
18
|
-
__exportStar(require("./generics.zod"), exports);
|
|
52
|
+
}
|
package/dist/mic.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RealtimeSession } from './realtime.js';
|
|
2
|
+
export type MicStallReason = 'track_ended' | 'track_muted' | 'context_suspended' | 'context_closed' | 'silent';
|
|
3
|
+
export interface MicCaptureOptions {
|
|
4
|
+
chunkSeconds?: number;
|
|
5
|
+
sampleRate?: number;
|
|
6
|
+
audio?: MediaTrackConstraints;
|
|
7
|
+
onClock?: (pcm: number, wall: number) => void;
|
|
8
|
+
onStall?: (reason: MicStallReason) => void;
|
|
9
|
+
onRecover?: () => void;
|
|
10
|
+
silentSeconds?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface MicDevice {
|
|
13
|
+
label: string;
|
|
14
|
+
sampleRate?: number;
|
|
15
|
+
channelCount?: number;
|
|
16
|
+
noiseSuppression?: boolean;
|
|
17
|
+
echoCancellation?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface MicCapture {
|
|
20
|
+
stop(): Promise<void>;
|
|
21
|
+
readonly sampleRate: number;
|
|
22
|
+
readonly device: MicDevice;
|
|
23
|
+
setMuted(muted: boolean): void;
|
|
24
|
+
readonly muted: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function captureMicrophone(onChunk: (pcm: Float32Array) => void, opts?: MicCaptureOptions): Promise<MicCapture>;
|
|
27
|
+
export declare function streamMicrophone(session: RealtimeSession, opts?: MicCaptureOptions): Promise<MicCapture>;
|
package/dist/mic.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
const WORKLET_SOURCE = `
|
|
2
|
+
class PCMProcessor extends AudioWorkletProcessor {
|
|
3
|
+
process(inputs) {
|
|
4
|
+
const input = inputs[0];
|
|
5
|
+
if (!input || input.length === 0) return true;
|
|
6
|
+
const ch = input[0];
|
|
7
|
+
if (!ch || ch.length === 0) return true;
|
|
8
|
+
this.port.postMessage(ch.slice(0));
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
registerProcessor('pcm-processor', PCMProcessor);
|
|
13
|
+
`;
|
|
14
|
+
const RECOVER_MS = 1000;
|
|
15
|
+
const CLOCK_MS = 500;
|
|
16
|
+
export async function captureMicrophone(onChunk, opts = {}) {
|
|
17
|
+
if (typeof navigator === 'undefined' || !navigator.mediaDevices) {
|
|
18
|
+
throw new Error('captureMicrophone is browser-only (no mediaDevices)');
|
|
19
|
+
}
|
|
20
|
+
const targetRate = opts.sampleRate ?? 16000;
|
|
21
|
+
const chunkSamples = Math.round((opts.chunkSeconds ?? 0.25) * targetRate);
|
|
22
|
+
const constraints = opts.audio ?? {
|
|
23
|
+
channelCount: 1,
|
|
24
|
+
echoCancellation: true,
|
|
25
|
+
noiseSuppression: true,
|
|
26
|
+
};
|
|
27
|
+
let muted = false;
|
|
28
|
+
let media = await navigator.mediaDevices.getUserMedia({ audio: constraints });
|
|
29
|
+
let ctx;
|
|
30
|
+
try {
|
|
31
|
+
ctx = new AudioContext({ sampleRate: targetRate });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
ctx = new AudioContext();
|
|
35
|
+
}
|
|
36
|
+
const workletUrl = URL.createObjectURL(new Blob([WORKLET_SOURCE], { type: 'application/javascript' }));
|
|
37
|
+
try {
|
|
38
|
+
await ctx.audioWorklet.addModule(workletUrl);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
URL.revokeObjectURL(workletUrl);
|
|
42
|
+
}
|
|
43
|
+
const node = new AudioWorkletNode(ctx, 'pcm-processor');
|
|
44
|
+
let source = ctx.createMediaStreamSource(media);
|
|
45
|
+
source.connect(node);
|
|
46
|
+
const inputRate = ctx.sampleRate;
|
|
47
|
+
const passThrough = inputRate === targetRate;
|
|
48
|
+
let buffered = [];
|
|
49
|
+
let readPos = 0;
|
|
50
|
+
let pending = null;
|
|
51
|
+
const t0 = performance.now();
|
|
52
|
+
let outSamples = 0;
|
|
53
|
+
const silentLimit = Math.round((opts.silentSeconds ?? 3) * inputRate);
|
|
54
|
+
let zeroRun = 0;
|
|
55
|
+
let silentFlagged = false;
|
|
56
|
+
let stopped = false;
|
|
57
|
+
let stall = null;
|
|
58
|
+
let recoverTimer = null;
|
|
59
|
+
const setStall = (reason) => {
|
|
60
|
+
if (stopped || stall === reason)
|
|
61
|
+
return;
|
|
62
|
+
stall = reason;
|
|
63
|
+
opts.onStall?.(reason);
|
|
64
|
+
};
|
|
65
|
+
const clearStall = () => {
|
|
66
|
+
if (stall === null)
|
|
67
|
+
return;
|
|
68
|
+
stall = null;
|
|
69
|
+
opts.onRecover?.();
|
|
70
|
+
};
|
|
71
|
+
node.port.onmessage = (ev) => {
|
|
72
|
+
const block = ev.data;
|
|
73
|
+
if (silentLimit > 0) {
|
|
74
|
+
let allZero = true;
|
|
75
|
+
for (const v of block) {
|
|
76
|
+
if (v !== 0) {
|
|
77
|
+
allZero = false;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (allZero) {
|
|
82
|
+
zeroRun += block.length;
|
|
83
|
+
if (!silentFlagged && zeroRun >= silentLimit) {
|
|
84
|
+
silentFlagged = true;
|
|
85
|
+
setStall('silent');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
zeroRun = 0;
|
|
90
|
+
if (silentFlagged) {
|
|
91
|
+
silentFlagged = false;
|
|
92
|
+
if (stall === 'silent')
|
|
93
|
+
clearStall();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
let out;
|
|
98
|
+
if (passThrough) {
|
|
99
|
+
out = block;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
for (const v of block)
|
|
103
|
+
buffered.push(v);
|
|
104
|
+
const step = inputRate / targetRate;
|
|
105
|
+
const res = [];
|
|
106
|
+
while (readPos + 1 < buffered.length) {
|
|
107
|
+
const i = Math.floor(readPos);
|
|
108
|
+
const frac = readPos - i;
|
|
109
|
+
res.push(buffered[i] * (1 - frac) + buffered[i + 1] * frac);
|
|
110
|
+
readPos += step;
|
|
111
|
+
}
|
|
112
|
+
const keepFrom = Math.max(0, Math.floor(readPos) - 1);
|
|
113
|
+
buffered = buffered.slice(keepFrom);
|
|
114
|
+
readPos -= keepFrom;
|
|
115
|
+
out = res;
|
|
116
|
+
}
|
|
117
|
+
if (out.length) {
|
|
118
|
+
outSamples += out.length;
|
|
119
|
+
if (stall !== null && stall !== 'silent')
|
|
120
|
+
clearStall();
|
|
121
|
+
pending = pending ? concat(pending, out) : Float32Array.from(out);
|
|
122
|
+
while (pending.length >= chunkSamples) {
|
|
123
|
+
const chunk = pending.slice(0, chunkSamples);
|
|
124
|
+
onChunk(muted ? new Float32Array(chunk.length) : chunk);
|
|
125
|
+
pending = pending.slice(chunkSamples);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const clockTimer = opts.onClock
|
|
130
|
+
? setInterval(() => {
|
|
131
|
+
opts.onClock?.(outSamples, (performance.now() - t0) / 1000);
|
|
132
|
+
}, CLOCK_MS)
|
|
133
|
+
: null;
|
|
134
|
+
const scheduleRecover = (fn) => {
|
|
135
|
+
if (stopped || recoverTimer)
|
|
136
|
+
return;
|
|
137
|
+
recoverTimer = setTimeout(() => {
|
|
138
|
+
recoverTimer = null;
|
|
139
|
+
fn();
|
|
140
|
+
}, RECOVER_MS);
|
|
141
|
+
};
|
|
142
|
+
const resumeContext = () => {
|
|
143
|
+
if (stopped || ctx.state !== 'suspended')
|
|
144
|
+
return;
|
|
145
|
+
void ctx.resume().catch(() => { }).then(() => {
|
|
146
|
+
if (!stopped && ctx.state === 'suspended')
|
|
147
|
+
scheduleRecover(resumeContext);
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
const reacquireTrack = () => {
|
|
151
|
+
if (stopped)
|
|
152
|
+
return;
|
|
153
|
+
navigator.mediaDevices
|
|
154
|
+
.getUserMedia({ audio: constraints })
|
|
155
|
+
.then((next) => {
|
|
156
|
+
if (stopped) {
|
|
157
|
+
for (const t of next.getTracks())
|
|
158
|
+
t.stop();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
source.disconnect();
|
|
162
|
+
for (const t of media.getTracks())
|
|
163
|
+
t.stop();
|
|
164
|
+
media = next;
|
|
165
|
+
source = ctx.createMediaStreamSource(media);
|
|
166
|
+
source.connect(node);
|
|
167
|
+
watchTracks(media);
|
|
168
|
+
})
|
|
169
|
+
.catch(() => scheduleRecover(reacquireTrack));
|
|
170
|
+
};
|
|
171
|
+
const watchTracks = (stream) => {
|
|
172
|
+
for (const track of stream.getAudioTracks()) {
|
|
173
|
+
track.addEventListener('ended', () => {
|
|
174
|
+
if (stopped)
|
|
175
|
+
return;
|
|
176
|
+
setStall('track_ended');
|
|
177
|
+
scheduleRecover(reacquireTrack);
|
|
178
|
+
});
|
|
179
|
+
track.addEventListener('mute', () => {
|
|
180
|
+
if (!stopped)
|
|
181
|
+
setStall('track_muted');
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
watchTracks(media);
|
|
186
|
+
ctx.addEventListener('statechange', () => {
|
|
187
|
+
if (stopped)
|
|
188
|
+
return;
|
|
189
|
+
if (ctx.state === 'suspended') {
|
|
190
|
+
setStall('context_suspended');
|
|
191
|
+
resumeContext();
|
|
192
|
+
}
|
|
193
|
+
else if (ctx.state === 'closed') {
|
|
194
|
+
setStall('context_closed');
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
sampleRate: targetRate,
|
|
199
|
+
get device() {
|
|
200
|
+
const t = media.getAudioTracks()[0];
|
|
201
|
+
const st = (t?.getSettings?.() ?? {});
|
|
202
|
+
return { label: t?.label ?? '', sampleRate: st.sampleRate, channelCount: st.channelCount, noiseSuppression: st.noiseSuppression, echoCancellation: st.echoCancellation };
|
|
203
|
+
},
|
|
204
|
+
get muted() {
|
|
205
|
+
return muted;
|
|
206
|
+
},
|
|
207
|
+
setMuted(on) {
|
|
208
|
+
muted = !!on;
|
|
209
|
+
},
|
|
210
|
+
async stop() {
|
|
211
|
+
stopped = true;
|
|
212
|
+
if (clockTimer)
|
|
213
|
+
clearInterval(clockTimer);
|
|
214
|
+
if (recoverTimer)
|
|
215
|
+
clearTimeout(recoverTimer);
|
|
216
|
+
node.port.onmessage = null;
|
|
217
|
+
source.disconnect();
|
|
218
|
+
node.disconnect();
|
|
219
|
+
for (const t of media.getTracks())
|
|
220
|
+
t.stop();
|
|
221
|
+
await ctx.close();
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export async function streamMicrophone(session, opts = {}) {
|
|
226
|
+
return captureMicrophone((pcm) => session.sendAudio(pcm), {
|
|
227
|
+
onClock: (pcm, wall) => session.sendClock(pcm, wall),
|
|
228
|
+
...opts,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function concat(a, b) {
|
|
232
|
+
const out = new Float32Array(a.length + b.length);
|
|
233
|
+
out.set(a, 0);
|
|
234
|
+
out.set(b, a.length);
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Capabilities, CommittedSegment, CreateSessionResponse, Fix, LatencyMode, SentenceSegment, ServerFrame, SessionOptions, SessionStats, SpeakerLabels, WarmRequest } 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
|
+
onLoading?(stage: 'download' | 'model'): void;
|
|
14
|
+
onFrame?(frame: ServerFrame): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function engineLost(closeCode?: number): VocalStackError;
|
|
17
|
+
export declare function upgradeRefused(status: number | undefined, body: string): VocalStackError;
|
|
18
|
+
export declare const RESTART_CLOSE = 1012;
|
|
19
|
+
export declare class RealtimeSession implements AsyncIterable<SessionEvent> {
|
|
20
|
+
#private;
|
|
21
|
+
readonly sessionId: string;
|
|
22
|
+
private constructor();
|
|
23
|
+
static connect(params: ConnectParams): Promise<RealtimeSession>;
|
|
24
|
+
sendAudio(chunk: Float32Array | ArrayBuffer | Uint8Array): void;
|
|
25
|
+
sendFile(source: AsyncIterable<Uint8Array>, chunkBytes?: number): Promise<void>;
|
|
26
|
+
pipe(source: AsyncIterable<Float32Array | ArrayBuffer | Uint8Array> | ReadableStream<Uint8Array>): Promise<void>;
|
|
27
|
+
sendClock(pcm: number, wall: number): void;
|
|
28
|
+
update(changes: {
|
|
29
|
+
latencyMode?: LatencyMode;
|
|
30
|
+
spokenLanguage?: string;
|
|
31
|
+
speakerLabels?: SpeakerLabels;
|
|
32
|
+
translate?: string[];
|
|
33
|
+
}): void;
|
|
34
|
+
eof(): void;
|
|
35
|
+
cancel(): void;
|
|
36
|
+
get doneReason(): 'eof' | 'max_duration' | 'cancelled' | null;
|
|
37
|
+
get stats(): SessionStats | null;
|
|
38
|
+
close(): void;
|
|
39
|
+
[Symbol.asyncIterator](): AsyncIterator<SessionEvent>;
|
|
40
|
+
[Symbol.dispose](): void;
|
|
41
|
+
}
|
|
42
|
+
export interface ConnectOptions {
|
|
43
|
+
onLoading?(stage: 'download' | 'model'): void;
|
|
44
|
+
onFrame?(frame: ServerFrame): void;
|
|
45
|
+
readyDeadlineMs?: number;
|
|
46
|
+
}
|
|
47
|
+
export interface TranscribeOptions extends Omit<SessionOptions, 'input' | 'audio'> {
|
|
48
|
+
onSession?(session: RealtimeSession): void;
|
|
49
|
+
onProgress?(progress: {
|
|
50
|
+
processedS: number;
|
|
51
|
+
totalS?: number;
|
|
52
|
+
}): void;
|
|
53
|
+
onCommit?(segment: CommittedSegment): void;
|
|
54
|
+
onTranslated?(seq: number, translations: Record<string, string>, sources?: Record<string, string>): void;
|
|
55
|
+
onAmended?(seq: number, text: string, speaker: number, fixes: Fix[]): void;
|
|
56
|
+
onSpeakerSettled?(seq: number, speaker: number, verdict: string, confidence?: number): void;
|
|
57
|
+
onSentence?(sentence: SentenceSegment): void;
|
|
58
|
+
readyDeadlineMs?: number;
|
|
59
|
+
}
|
|
60
|
+
export interface RealtimeTranscript {
|
|
61
|
+
segments: CommittedSegment[];
|
|
62
|
+
sentences: SentenceSegment[];
|
|
63
|
+
text: string;
|
|
64
|
+
doneReason: 'eof' | 'max_duration' | 'cancelled' | null;
|
|
65
|
+
stats: SessionStats | null;
|
|
66
|
+
}
|
|
67
|
+
export declare class Realtime {
|
|
68
|
+
#private;
|
|
69
|
+
constructor(t: Transport);
|
|
70
|
+
createSession(options?: SessionOptions, call?: CallOptions): Promise<CreateSessionResponse>;
|
|
71
|
+
connect(options?: SessionOptions, connect?: ConnectOptions): Promise<RealtimeSession>;
|
|
72
|
+
capabilities(): Promise<Capabilities>;
|
|
73
|
+
warm(req: WarmRequest): Promise<void>;
|
|
74
|
+
transcribeFile(file: FileInput, opts?: TranscribeOptions): Promise<RealtimeTranscript>;
|
|
75
|
+
transcribeUrl(url: string, opts?: TranscribeOptions): Promise<RealtimeTranscript>;
|
|
76
|
+
}
|
|
77
|
+
export declare function applySentence(list: SentenceSegment[], sentence: SentenceSegment): SentenceSegment[];
|