@vocalstack/js-sdk 2.0.0-alpha.0 → 2.0.0-alpha.2
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/README.md +7 -2
- package/SKILL.md +13 -6
- package/dist/errors.d.ts +0 -18
- package/dist/errors.js +0 -18
- package/dist/http.d.ts +0 -32
- package/dist/http.js +26 -24
- package/dist/index.d.ts +5 -19
- package/dist/index.js +4 -18
- package/dist/mic.d.ts +1 -49
- package/dist/mic.js +0 -22
- package/dist/realtime.d.ts +4 -59
- package/dist/realtime.js +192 -152
- package/dist/resources.d.ts +86 -211
- package/dist/resources.js +130 -178
- package/dist/subscription.d.ts +0 -17
- package/dist/subscription.js +1 -12
- package/dist/ws.d.ts +0 -3
- package/dist/ws.js +0 -3
- package/package.json +3 -7
- package/skills/vocalstack/SKILL.md +13 -6
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ The VocalStack speech API for Node (20 or newer) and browsers: one client,
|
|
|
4
4
|
`VocalStack`, with one group per resource, typed from the API's one OpenAPI
|
|
5
5
|
document (`/v1/openapi.json`, generated into `@vocalstack/contract`).
|
|
6
6
|
|
|
7
|
+
Read more about the [VocalStack JavaScript SDK](https://vocalstack.com/#developers) and our other SDKs on the [VocalStack website](https://vocalstack.com/).
|
|
8
|
+
|
|
7
9
|
```ts
|
|
8
10
|
import { VocalStack } from '@vocalstack/js-sdk';
|
|
9
11
|
|
|
@@ -21,14 +23,17 @@ const srt = await (await vs.recordings.export(job.recordingId, 'srt')).text();
|
|
|
21
23
|
|
|
22
24
|
## Install
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
```sh
|
|
27
|
+
npm install @vocalstack/js-sdk
|
|
28
|
+
```
|
|
25
29
|
|
|
26
30
|
## Guides
|
|
27
31
|
|
|
28
32
|
- [Quickstart](https://github.com/vocalstack/js-sdk/blob/main/docs/quickstart.md): An API key, the SDK, and your first transcript in a dozen lines.
|
|
29
33
|
- [Transcribe a file](https://github.com/vocalstack/js-sdk/blob/main/docs/transcribe-a-file.md): Upload a file, wait for its job, read the transcript and export it. Asynchronous; nothing to keep open.
|
|
30
|
-
- [Transcribe a link](https://github.com/vocalstack/js-sdk/blob/main/docs/transcribe-a-link.md): The server fetches a public URL or a
|
|
34
|
+
- [Transcribe a link](https://github.com/vocalstack/js-sdk/blob/main/docs/transcribe-a-link.md): The server fetches a public URL, a YouTube video or a recording on a video or audio site, then transcribes it like a file. Asynchronous, with a fetch phase first.
|
|
31
35
|
- [Transcribe a batch of files](https://github.com/vocalstack/js-sdk/blob/main/docs/transcribe-a-batch.md): Send many files or links as one batch, start it once, and read every transcript when it is done. Cheaper than one transcription per file.
|
|
36
|
+
- [Search your transcriptions](https://github.com/vocalstack/js-sdk/blob/main/docs/search-transcriptions.md): Find transcriptions by words in their name or link, site, language, kind, tag and date, keep the shared ones, and play each with its share code.
|
|
32
37
|
- [Stream a microphone](https://github.com/vocalstack/js-sdk/blob/main/docs/stream-a-microphone.md): Live audio in, transcript events out, over one WebSocket. Drafts replace the live tail; commits append.
|
|
33
38
|
- [Stream a file at full speed](https://github.com/vocalstack/js-sdk/blob/main/docs/stream-a-file.md): Send a file's bytes through a realtime session and read the transcript as fast as the server makes it. Nothing is stored.
|
|
34
39
|
- [Go live](https://github.com/vocalstack/js-sdk/blob/main/docs/go-live.md): Publish live audio to a stored transcription, and put it on a broadcast link listeners open. Control is HTTP; audio is one WebSocket.
|
package/SKILL.md
CHANGED
|
@@ -33,11 +33,8 @@ npm init -y && npm pkg set type=module # in a new folder: the programs below a
|
|
|
33
33
|
npm i @vocalstack/js-sdk # Node 20 or newer
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
(`<base>/docs`, or Developers -> API reference in the app) and `<base>/sdk/latest`
|
|
39
|
-
give the exact commands, which send `VOCALSTACK_API_KEY` to that host only.
|
|
40
|
-
Or install it from a clone of https://github.com/vocalstack/js-sdk. Build it
|
|
36
|
+
To run code newer than the npm release, install it from a clone of
|
|
37
|
+
https://github.com/vocalstack/js-sdk. Build it
|
|
41
38
|
first (`npm run build` also builds the bundled `contract/` package):
|
|
42
39
|
|
|
43
40
|
```sh
|
|
@@ -105,7 +102,7 @@ const vs = new VocalStack({ baseUrl: process.env.VOCALSTACK_BASE_URL, apiKey: pr
|
|
|
105
102
|
|
|
106
103
|
const tx = await vs.transcriptions.create({
|
|
107
104
|
file: process.argv[2], // a path (mp3, wav, flac, ogg, m4a, mp4 ...), a Blob or bytes
|
|
108
|
-
settings: { spokenLanguage: 'en', speakerLabels: 'on' }, // both optional; speakerLabels: '
|
|
105
|
+
settings: { spokenLanguage: 'en', speakerLabels: 'on' }, // both optional; speakerLabels: 'auto' (default) | 'on' | 'off'
|
|
109
106
|
// Off unless asked for: settings.summary, settings.title, settings.summaryTranslations,
|
|
110
107
|
// settings.voice (spoken translations), and shareLink: true (a public share link).
|
|
111
108
|
});
|
|
@@ -182,6 +179,16 @@ for (const h of hits) {
|
|
|
182
179
|
}
|
|
183
180
|
```
|
|
184
181
|
|
|
182
|
+
By meaning, in any language, even with none of the words (vector search). Each
|
|
183
|
+
hit's `passage` is its whole paragraph: give the passages to your own model as
|
|
184
|
+
context to answer questions about what was said (RAG). Pay as you go counts 60
|
|
185
|
+
an hour (`allowance`); Premium and Enterprise have no limit.
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
const { hits: found } = await vs.search.vector('how much does housing cost');
|
|
189
|
+
for (const h of found) console.log(h.similarity.toFixed(2), h.passage.mediaStart, h.passage.text);
|
|
190
|
+
```
|
|
191
|
+
|
|
185
192
|
## 6. A batch of files
|
|
186
193
|
|
|
187
194
|
Many files at once cost less as a batch: one create, one minimum charge for
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { ErrorCode } from '@vocalstack/contract';
|
|
2
|
-
/** The OAuth errors `vs.connect.token()` raises (RFC 6749 §5.2): the token
|
|
3
|
-
* endpoint answers `400 { error, error_description }`, not the envelope. */
|
|
4
2
|
export type ConnectTokenErrorCode = 'invalid_request' | 'invalid_grant' | 'unsupported_grant_type';
|
|
5
3
|
export interface VocalStackErrorOptions {
|
|
6
4
|
retryable?: boolean;
|
|
@@ -9,32 +7,16 @@ export interface VocalStackErrorOptions {
|
|
|
9
7
|
headers?: Headers;
|
|
10
8
|
requestId?: string;
|
|
11
9
|
}
|
|
12
|
-
/** Every failure this SDK surfaces: an HTTP error envelope, a socket
|
|
13
|
-
* `error` frame, or one the transport synthesizes (`capacity_exceeded`). */
|
|
14
10
|
export declare class VocalStackError extends Error {
|
|
15
|
-
/** The envelope's code; on `vs.connect.token()`, the OAuth `error`. */
|
|
16
11
|
readonly code: ErrorCode | ConnectTokenErrorCode;
|
|
17
12
|
readonly retryable: boolean;
|
|
18
|
-
/** HTTP status, when the error came from an HTTP response. */
|
|
19
13
|
readonly status?: number;
|
|
20
|
-
/** The envelope's `data.details`: for example `{ balance, required,
|
|
21
|
-
* plan, topUpUrl }` on `insufficient_credit`, code suggestions on a
|
|
22
|
-
* link conflict, `{ recordingId, takeover: true }` when another device
|
|
23
|
-
* is recording. */
|
|
24
14
|
readonly details?: unknown;
|
|
25
|
-
/** The response headers (`Retry-After`, `RateLimit-*`,
|
|
26
|
-
* `WWW-Authenticate`), when the error came from an HTTP response. */
|
|
27
15
|
readonly headers?: Headers;
|
|
28
|
-
/** The request's id (the envelope's `requestId`, else the `X-Request-Id`
|
|
29
|
-
* header): quote it when reporting a failure. */
|
|
30
16
|
readonly requestId?: string;
|
|
31
17
|
constructor(code: ErrorCode | ConnectTokenErrorCode, message: string, opts?: VocalStackErrorOptions);
|
|
32
|
-
/** Seconds the server asked the caller to wait (`Retry-After`), if it said. */
|
|
33
18
|
get retryAfter(): number | undefined;
|
|
34
19
|
}
|
|
35
20
|
export declare function capacityExceeded(message: string): VocalStackError;
|
|
36
21
|
export declare function fromEnvelope(body: unknown, status?: number, headers?: Headers): VocalStackError;
|
|
37
|
-
/** A token endpoint's `400 { error, error_description }` (RFC 6749 §5.2) as
|
|
38
|
-
* a `VocalStackError` whose `code` is the `error`; any other answer goes
|
|
39
|
-
* through the envelope. */
|
|
40
22
|
export declare function fromOAuth(body: unknown, status?: number, headers?: Headers): VocalStackError;
|
package/dist/errors.js
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
/** Every failure this SDK surfaces: an HTTP error envelope, a socket
|
|
2
|
-
* `error` frame, or one the transport synthesizes (`capacity_exceeded`). */
|
|
3
1
|
export class VocalStackError extends Error {
|
|
4
|
-
/** The envelope's code; on `vs.connect.token()`, the OAuth `error`. */
|
|
5
2
|
code;
|
|
6
3
|
retryable;
|
|
7
|
-
/** HTTP status, when the error came from an HTTP response. */
|
|
8
4
|
status;
|
|
9
|
-
/** The envelope's `data.details`: for example `{ balance, required,
|
|
10
|
-
* plan, topUpUrl }` on `insufficient_credit`, code suggestions on a
|
|
11
|
-
* link conflict, `{ recordingId, takeover: true }` when another device
|
|
12
|
-
* is recording. */
|
|
13
5
|
details;
|
|
14
|
-
/** The response headers (`Retry-After`, `RateLimit-*`,
|
|
15
|
-
* `WWW-Authenticate`), when the error came from an HTTP response. */
|
|
16
6
|
headers;
|
|
17
|
-
/** The request's id (the envelope's `requestId`, else the `X-Request-Id`
|
|
18
|
-
* header): quote it when reporting a failure. */
|
|
19
7
|
requestId;
|
|
20
8
|
constructor(code, message, opts = {}) {
|
|
21
9
|
super(message);
|
|
@@ -27,7 +15,6 @@ export class VocalStackError extends Error {
|
|
|
27
15
|
this.headers = opts.headers;
|
|
28
16
|
this.requestId = opts.requestId;
|
|
29
17
|
}
|
|
30
|
-
/** Seconds the server asked the caller to wait (`Retry-After`), if it said. */
|
|
31
18
|
get retryAfter() {
|
|
32
19
|
const v = this.headers?.get('retry-after');
|
|
33
20
|
if (v == null || v === '')
|
|
@@ -44,8 +31,6 @@ export function capacityExceeded(message) {
|
|
|
44
31
|
}
|
|
45
32
|
export function fromEnvelope(body, status, headers) {
|
|
46
33
|
const b = body;
|
|
47
|
-
// The envelope names the request; a header the page cannot read across
|
|
48
|
-
// origins (when it is not exposed) is the fallback.
|
|
49
34
|
const requestId = (b && typeof b.requestId === 'string' ? b.requestId : undefined) ?? headers?.get('x-request-id') ?? undefined;
|
|
50
35
|
if (b && typeof b.code === 'string') {
|
|
51
36
|
return new VocalStackError(b.code, b.message ?? b.code, {
|
|
@@ -58,9 +43,6 @@ export function fromEnvelope(body, status, headers) {
|
|
|
58
43
|
}
|
|
59
44
|
return new VocalStackError('internal', `unexpected error (status ${status})`, { status, headers, requestId });
|
|
60
45
|
}
|
|
61
|
-
/** A token endpoint's `400 { error, error_description }` (RFC 6749 §5.2) as
|
|
62
|
-
* a `VocalStackError` whose `code` is the `error`; any other answer goes
|
|
63
|
-
* through the envelope. */
|
|
64
46
|
export function fromOAuth(body, status, headers) {
|
|
65
47
|
const b = body;
|
|
66
48
|
if (status === 400 && b && typeof b.error === 'string') {
|
package/dist/http.d.ts
CHANGED
|
@@ -1,75 +1,43 @@
|
|
|
1
1
|
export type Query = Record<string, string | number | boolean | undefined | null>;
|
|
2
2
|
export interface ClientOptions {
|
|
3
|
-
/** API origin; `https://api.vocalstack.com` when absent. */
|
|
4
3
|
baseUrl?: string;
|
|
5
|
-
/** An API key (`vs_key_…`), on a server: keys are refused from web
|
|
6
|
-
* pages. Absent (and no `token`) = anonymous (a listener). */
|
|
7
4
|
apiKey?: string;
|
|
8
|
-
/** In a browser instead: a signed-in person's access token or a
|
|
9
|
-
* short-lived browser token (`vs_bt_…`), or a getter so a refreshed one
|
|
10
|
-
* is picked up per request. Not with `apiKey`. */
|
|
11
5
|
token?: string | (() => string | undefined | null | Promise<string | undefined | null>);
|
|
12
|
-
/** Act for an organization the user belongs to (`X-Acting-Principal`). */
|
|
13
6
|
actingPrincipal?: string;
|
|
14
|
-
/** A stable per-device id (correlation only, `X-Device-Id`). */
|
|
15
7
|
deviceId?: string;
|
|
16
8
|
fetch?: typeof fetch;
|
|
17
|
-
/** How often a `429 rate_limited`, or a `503 unavailable` on a call that
|
|
18
|
-
* is safe to repeat (GET, HEAD, DELETE, or one with an `Idempotency-Key`),
|
|
19
|
-
* is retried after its `Retry-After` (at most 60 s each). Default 2; 0
|
|
20
|
-
* turns it off. */
|
|
21
9
|
maxRetries?: number;
|
|
22
|
-
/** Default deadline for a realtime socket to report ready, in ms. */
|
|
23
10
|
readyDeadlineMs?: number;
|
|
24
11
|
}
|
|
25
|
-
/** Per-call options every method takes last. */
|
|
26
12
|
export interface CallOptions {
|
|
27
|
-
/** Sent as `Idempotency-Key` on the creates that honour it. */
|
|
28
13
|
idempotencyKey?: string;
|
|
29
14
|
signal?: AbortSignal;
|
|
30
15
|
}
|
|
31
|
-
/** How a listener reads something that is not theirs: a private
|
|
32
|
-
* broadcast's listener token, or a share link's code. */
|
|
33
16
|
export interface ReadAs {
|
|
34
17
|
listenerToken?: string;
|
|
35
18
|
shareCode?: string;
|
|
36
19
|
}
|
|
37
|
-
/** A listener-side read's query: the wire spells both as the options do
|
|
38
|
-
* (`?listenerToken=`, `?shareCode=`), so this only picks them out. */
|
|
39
20
|
export declare function listenerQuery(as: ReadAs | undefined): Query;
|
|
40
21
|
export interface RequestInit2 extends CallOptions {
|
|
41
22
|
query?: Query;
|
|
42
23
|
body?: unknown;
|
|
43
24
|
headers?: Record<string, string>;
|
|
44
|
-
/** Send no credential even when the client has one. */
|
|
45
25
|
anonymous?: boolean;
|
|
46
|
-
/** Never retry, whatever the answer (a single-use code). */
|
|
47
26
|
noRetry?: boolean;
|
|
48
|
-
/** Read a 400 as OAuth's `{ error, error_description }`. */
|
|
49
27
|
oauthErrors?: boolean;
|
|
50
28
|
}
|
|
51
29
|
export declare const DEFAULT_BASE_URL = "https://api.vocalstack.com";
|
|
52
30
|
export declare function qs(q: Query | undefined): string;
|
|
53
31
|
export declare const seg: (s: string | number) => string;
|
|
54
|
-
/** Whether a failed call is retried after its `Retry-After` (up to
|
|
55
|
-
* `maxRetries` times): `429 rate_limited` always (nothing ran), and
|
|
56
|
-
* `503 unavailable` (the database is waking) only when repeating the call
|
|
57
|
-
* cannot do the work twice: GET, HEAD and DELETE, or a call carrying an
|
|
58
|
-
* `Idempotency-Key`. */
|
|
59
32
|
export declare function retries(status: number, code: string, method: string, idempotencyKey?: string): boolean;
|
|
60
|
-
/** The one HTTP path every resource method goes through. */
|
|
61
33
|
export declare class Transport {
|
|
62
34
|
#private;
|
|
63
35
|
readonly baseUrl: string;
|
|
64
36
|
readonly opts: ClientOptions;
|
|
65
37
|
constructor(opts: ClientOptions);
|
|
66
|
-
/** What goes in `Authorization: Bearer`: the API key, else the token. */
|
|
67
38
|
bearer(): Promise<string | undefined>;
|
|
68
39
|
url(path: string, query?: Query): string;
|
|
69
40
|
wsUrl(path: string, query?: Query): string;
|
|
70
|
-
/** The raw response, after error mapping. Redirects are not followed
|
|
71
|
-
* (`redirect: 'manual'`), so a 302 comes back as itself. */
|
|
72
41
|
raw(method: string, path: string, init?: RequestInit2): Promise<Response>;
|
|
73
|
-
/** A JSON call; an empty body resolves to `undefined`. */
|
|
74
42
|
json<T>(method: string, path: string, init?: RequestInit2): Promise<T>;
|
|
75
43
|
}
|
package/dist/http.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { fromEnvelope, fromOAuth } from './errors.js';
|
|
2
|
-
/** A listener-side read's query: the wire spells both as the options do
|
|
3
|
-
* (`?listenerToken=`, `?shareCode=`), so this only picks them out. */
|
|
1
|
+
import { VocalStackError, fromEnvelope, fromOAuth } from './errors.js';
|
|
4
2
|
export function listenerQuery(as) {
|
|
5
3
|
return { listenerToken: as?.listenerToken, shareCode: as?.shareCode };
|
|
6
4
|
}
|
|
@@ -18,13 +16,7 @@ export function qs(q) {
|
|
|
18
16
|
}
|
|
19
17
|
export const seg = (s) => encodeURIComponent(String(s));
|
|
20
18
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
21
|
-
/** The wait before retrying `503 unavailable` when it names none. */
|
|
22
19
|
const UNAVAILABLE_WAIT_S = 2;
|
|
23
|
-
/** Whether a failed call is retried after its `Retry-After` (up to
|
|
24
|
-
* `maxRetries` times): `429 rate_limited` always (nothing ran), and
|
|
25
|
-
* `503 unavailable` (the database is waking) only when repeating the call
|
|
26
|
-
* cannot do the work twice: GET, HEAD and DELETE, or a call carrying an
|
|
27
|
-
* `Idempotency-Key`. */
|
|
28
20
|
export function retries(status, code, method, idempotencyKey) {
|
|
29
21
|
if (status === 429)
|
|
30
22
|
return true;
|
|
@@ -33,7 +25,6 @@ export function retries(status, code, method, idempotencyKey) {
|
|
|
33
25
|
const m = method.toUpperCase();
|
|
34
26
|
return m === 'GET' || m === 'HEAD' || m === 'DELETE' || !!idempotencyKey;
|
|
35
27
|
}
|
|
36
|
-
/** The one HTTP path every resource method goes through. */
|
|
37
28
|
export class Transport {
|
|
38
29
|
baseUrl;
|
|
39
30
|
opts;
|
|
@@ -45,7 +36,6 @@ export class Transport {
|
|
|
45
36
|
this.opts = opts;
|
|
46
37
|
this.#fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
47
38
|
}
|
|
48
|
-
/** What goes in `Authorization: Bearer`: the API key, else the token. */
|
|
49
39
|
async bearer() {
|
|
50
40
|
if (this.opts.apiKey)
|
|
51
41
|
return this.opts.apiKey;
|
|
@@ -59,8 +49,6 @@ export class Transport {
|
|
|
59
49
|
wsUrl(path, query) {
|
|
60
50
|
return `${this.baseUrl.replace(/^http/, 'ws')}${path}${qs(query)}`;
|
|
61
51
|
}
|
|
62
|
-
/** The raw response, after error mapping. Redirects are not followed
|
|
63
|
-
* (`redirect: 'manual'`), so a 302 comes back as itself. */
|
|
64
52
|
async raw(method, path, init = {}) {
|
|
65
53
|
const headers = { accept: 'application/json', ...init.headers };
|
|
66
54
|
const form = typeof FormData !== 'undefined' && init.body instanceof FormData;
|
|
@@ -81,13 +69,23 @@ export class Transport {
|
|
|
81
69
|
const body = init.body === undefined ? undefined : form ? init.body : urlencoded ? String(init.body) : JSON.stringify(init.body);
|
|
82
70
|
const maxRetries = init.noRetry ? 0 : (this.opts.maxRetries ?? 2);
|
|
83
71
|
for (let attempt = 0;; attempt++) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
}
|
|
91
89
|
if (res.ok || (res.status >= 300 && res.status < 400))
|
|
92
90
|
return res;
|
|
93
91
|
const text = await res.text().catch(() => '');
|
|
@@ -100,8 +98,6 @@ export class Transport {
|
|
|
100
98
|
}
|
|
101
99
|
const err = (init.oauthErrors ? fromOAuth : fromEnvelope)(json, res.status, res.headers);
|
|
102
100
|
if (retries(res.status, err.code, method, init.idempotencyKey) && attempt < maxRetries) {
|
|
103
|
-
// `unavailable` (a waking database) asks for Retry-After: 2; a
|
|
104
|
-
// missing header means the same.
|
|
105
101
|
const wait = err.retryAfter ?? (err.code === 'unavailable' ? UNAVAILABLE_WAIT_S : undefined);
|
|
106
102
|
if (wait !== undefined && wait <= 60) {
|
|
107
103
|
await sleep(wait * 1000);
|
|
@@ -111,12 +107,18 @@ export class Transport {
|
|
|
111
107
|
throw err;
|
|
112
108
|
}
|
|
113
109
|
}
|
|
114
|
-
/** A JSON call; an empty body resolves to `undefined`. */
|
|
115
110
|
async json(method, path, init = {}) {
|
|
116
111
|
const res = await this.raw(method, path, init);
|
|
117
112
|
if (res.status >= 300 && res.status < 400)
|
|
118
113
|
return { location: res.headers.get('location') };
|
|
119
114
|
const text = await res.text();
|
|
120
|
-
|
|
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
|
+
}
|
|
121
123
|
}
|
|
122
124
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The VocalStack SDK: one client, `VocalStack`, grouped by resource.
|
|
3
|
-
*
|
|
4
|
-
* import { VocalStack } from '@vocalstack/js-sdk';
|
|
5
|
-
* const vs = new VocalStack({ apiKey: process.env.VOCALSTACK_API_KEY });
|
|
6
|
-
*
|
|
7
|
-
* const tx = await vs.transcriptions.createFromUrl('https://example.com/talk.mp3');
|
|
8
|
-
* const job = await vs.jobs.wait(tx.job!.recordingId);
|
|
9
|
-
* const { sentences } = await vs.recordings.transcript(job.recordingId);
|
|
10
|
-
*
|
|
11
|
-
* What is what (the API's glossary): a realtime session is a signed token
|
|
12
|
-
* plus one socket, nothing stored (`vs.realtime`); a broadcast is the link
|
|
13
|
-
* listeners open; a transcription is a file, link, microphone or stream
|
|
14
|
-
* transcribed and stored; a recording is one take inside either; a job is
|
|
15
|
-
* a file or link recording's transcription work.
|
|
16
|
-
*/
|
|
17
1
|
import { type ClientOptions } from './http.js';
|
|
18
2
|
import { Realtime } from './realtime.js';
|
|
19
|
-
import { Batches, Billing, Broadcasts, Connect, Corrections, Events, Jobs, Languages, MeApi, Meta, Recordings, Rtmp, Search, ShareLinks, Transcriptions, Voices } from './resources.js';
|
|
3
|
+
import { Batches, Billing, Broadcasts, Connect, Corrections, Events, Jobs, Languages, MeApi, Meta, Recordings, Rtmp, Search, ShareLinks, Tags, Transcriptions, Voices } from './resources.js';
|
|
20
4
|
export type { ClientOptions, CallOptions, ReadAs, Query } from './http.js';
|
|
21
5
|
export { DEFAULT_BASE_URL } from './http.js';
|
|
22
6
|
export { VocalStackError } from './errors.js';
|
|
@@ -29,8 +13,9 @@ export type { ConnectOptions, TranscribeOptions, RealtimeTranscript } from './re
|
|
|
29
13
|
export { Subscription } from './subscription.js';
|
|
30
14
|
export type { SubscribeOptions, SubscriptionEvent } from './subscription.js';
|
|
31
15
|
export type { WsLike } from './ws.js';
|
|
32
|
-
export { Batches, Broadcasts, Connect, Corrections, Events, Jobs, Languages, MeApi, Meta, Recordings, Rtmp, Search, ShareLinks, Transcriptions, Voices, } from './resources.js';
|
|
33
|
-
export type {
|
|
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, VoicesChoice } from './resources.js';
|
|
18
|
+
export type { AddBatchFileInput, ConnectToken, ConnectTokenInput, CreateTranscriptionInput, LifecycleFilter, Me, PageOptions, RecordingPageOptions, SearchOptions, TranscriptionFilters, Upload, UsageChargesOptions, UsageOptions } from './resources.js';
|
|
34
19
|
export type * from '@vocalstack/contract';
|
|
35
20
|
export type VocalStackOptions = ClientOptions;
|
|
36
21
|
export declare class VocalStack {
|
|
@@ -43,6 +28,7 @@ export declare class VocalStack {
|
|
|
43
28
|
readonly jobs: Jobs;
|
|
44
29
|
readonly corrections: Corrections;
|
|
45
30
|
readonly shareLinks: ShareLinks;
|
|
31
|
+
readonly tags: Tags;
|
|
46
32
|
readonly voices: Voices;
|
|
47
33
|
readonly search: Search;
|
|
48
34
|
readonly me: MeApi;
|
package/dist/index.js
CHANGED
|
@@ -1,28 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The VocalStack SDK: one client, `VocalStack`, grouped by resource.
|
|
3
|
-
*
|
|
4
|
-
* import { VocalStack } from '@vocalstack/js-sdk';
|
|
5
|
-
* const vs = new VocalStack({ apiKey: process.env.VOCALSTACK_API_KEY });
|
|
6
|
-
*
|
|
7
|
-
* const tx = await vs.transcriptions.createFromUrl('https://example.com/talk.mp3');
|
|
8
|
-
* const job = await vs.jobs.wait(tx.job!.recordingId);
|
|
9
|
-
* const { sentences } = await vs.recordings.transcript(job.recordingId);
|
|
10
|
-
*
|
|
11
|
-
* What is what (the API's glossary): a realtime session is a signed token
|
|
12
|
-
* plus one socket, nothing stored (`vs.realtime`); a broadcast is the link
|
|
13
|
-
* listeners open; a transcription is a file, link, microphone or stream
|
|
14
|
-
* transcribed and stored; a recording is one take inside either; a job is
|
|
15
|
-
* a file or link recording's transcription work.
|
|
16
|
-
*/
|
|
17
1
|
import { Transport } from './http.js';
|
|
18
2
|
import { Realtime } from './realtime.js';
|
|
19
|
-
import { Batches, Billing, Broadcasts, Connect, Corrections, Events, Jobs, Languages, MeApi, Meta, Recordings, Rtmp, Search, ShareLinks, Transcriptions, Voices, } from './resources.js';
|
|
3
|
+
import { Batches, Billing, Broadcasts, Connect, Corrections, Events, Jobs, Languages, MeApi, Meta, Recordings, Rtmp, Search, ShareLinks, Tags, Transcriptions, Voices, } from './resources.js';
|
|
20
4
|
export { DEFAULT_BASE_URL } from './http.js';
|
|
21
5
|
export { VocalStackError } from './errors.js';
|
|
22
6
|
export { RealtimeSession } from './realtime.js';
|
|
23
7
|
export { Realtime, applySentence } from './realtime.js';
|
|
24
8
|
export { Subscription } from './subscription.js';
|
|
25
|
-
export { Batches, Broadcasts, Connect, Corrections, Events, Jobs, Languages, MeApi, Meta, Recordings, Rtmp, Search, ShareLinks, Transcriptions, Voices, } from './resources.js';
|
|
9
|
+
export { Batches, Broadcasts, Connect, Corrections, Events, Jobs, Languages, MeApi, Meta, Recordings, Rtmp, Search, ShareLinks, Tags, Transcriptions, Voices, playableFormat, } from './resources.js';
|
|
26
10
|
export class VocalStack {
|
|
27
11
|
baseUrl;
|
|
28
12
|
realtime;
|
|
@@ -33,6 +17,7 @@ export class VocalStack {
|
|
|
33
17
|
jobs;
|
|
34
18
|
corrections;
|
|
35
19
|
shareLinks;
|
|
20
|
+
tags;
|
|
36
21
|
voices;
|
|
37
22
|
search;
|
|
38
23
|
me;
|
|
@@ -53,6 +38,7 @@ export class VocalStack {
|
|
|
53
38
|
this.jobs = new Jobs(t);
|
|
54
39
|
this.corrections = new Corrections(t);
|
|
55
40
|
this.shareLinks = new ShareLinks(t);
|
|
41
|
+
this.tags = new Tags(t);
|
|
56
42
|
this.voices = new Voices(t);
|
|
57
43
|
this.search = new Search(t);
|
|
58
44
|
this.me = new MeApi(t);
|
package/dist/mic.d.ts
CHANGED
|
@@ -1,51 +1,16 @@
|
|
|
1
1
|
import type { RealtimeSession } from './realtime.js';
|
|
2
|
-
|
|
3
|
-
export type MicStallReason =
|
|
4
|
-
/** The input track ended: device unplugged, headset switched, permission revoked. */
|
|
5
|
-
'track_ended'
|
|
6
|
-
/** The track is muted by the OS/hardware (no samples until it unmutes). */
|
|
7
|
-
| 'track_muted'
|
|
8
|
-
/** The AudioContext was suspended (phone call, screen lock, route change). */
|
|
9
|
-
| 'context_suspended'
|
|
10
|
-
/** The AudioContext was closed underneath us. */
|
|
11
|
-
| 'context_closed'
|
|
12
|
-
/** Seconds of exact-zero samples: a silently denied or hardware-muted mic. */
|
|
13
|
-
| 'silent';
|
|
2
|
+
export type MicStallReason = 'track_ended' | 'track_muted' | 'context_suspended' | 'context_closed' | 'silent';
|
|
14
3
|
export interface MicCaptureOptions {
|
|
15
4
|
chunkSeconds?: number;
|
|
16
5
|
sampleRate?: number;
|
|
17
6
|
audio?: MediaTrackConstraints;
|
|
18
|
-
/**
|
|
19
|
-
* Called every ~0.5s with (pcm, wall): cumulative output samples
|
|
20
|
-
* produced and seconds since capture start. Wire it to
|
|
21
|
-
* `session.sendClock` so the server can detect capture loss (a
|
|
22
|
-
* suspended context or dead track produces no samples while wall
|
|
23
|
-
* time advances) and mark the transcript with honest gaps. Driven
|
|
24
|
-
* by a timer, not by sample arrival, so it keeps reporting while
|
|
25
|
-
* capture is stalled — that is the case it exists for.
|
|
26
|
-
*/
|
|
27
7
|
onClock?: (pcm: number, wall: number) => void;
|
|
28
|
-
/**
|
|
29
|
-
* Capture stopped producing audio. Recovery is attempted
|
|
30
|
-
* automatically (context resume, track re-acquire); `onRecover`
|
|
31
|
-
* fires when samples flow again. Use it to show a "microphone
|
|
32
|
-
* lost" state instead of "Listening".
|
|
33
|
-
*/
|
|
34
8
|
onStall?: (reason: MicStallReason) => void;
|
|
35
|
-
/** Audio is flowing again after a stall. */
|
|
36
9
|
onRecover?: () => void;
|
|
37
|
-
/**
|
|
38
|
-
* Exact-zero run length (seconds) that flags a silently denied or
|
|
39
|
-
* hardware-muted mic (real mics always carry a noise floor). Default
|
|
40
|
-
* 3; 0 disables.
|
|
41
|
-
*/
|
|
42
10
|
silentSeconds?: number;
|
|
43
11
|
}
|
|
44
|
-
/** The input device the capture is reading, as the browser reports it. */
|
|
45
12
|
export interface MicDevice {
|
|
46
|
-
/** The device's name, e.g. "MacBook Pro Microphone" ("" before permission is granted). */
|
|
47
13
|
label: string;
|
|
48
|
-
/** Its native rate (the capture resamples to `sampleRate`). */
|
|
49
14
|
sampleRate?: number;
|
|
50
15
|
channelCount?: number;
|
|
51
16
|
noiseSuppression?: boolean;
|
|
@@ -54,20 +19,7 @@ export interface MicDevice {
|
|
|
54
19
|
export interface MicCapture {
|
|
55
20
|
stop(): Promise<void>;
|
|
56
21
|
readonly sampleRate: number;
|
|
57
|
-
/** The microphone being read now (a re-acquired track may be another one). */
|
|
58
22
|
readonly device: MicDevice;
|
|
59
|
-
/** Hold the capture open but stop sending what the microphone hears.
|
|
60
|
-
*
|
|
61
|
-
* A side conversation should not have to end the session, and pausing
|
|
62
|
-
* is not the same thing: pausing closes the stream and the listeners
|
|
63
|
-
* lose it. Muting keeps every listener connected and the recording
|
|
64
|
-
* running, and simply says nothing.
|
|
65
|
-
*
|
|
66
|
-
* Muted chunks are emitted as SILENCE rather than dropped. The server
|
|
67
|
-
* measures media time by the audio it receives, so dropping would
|
|
68
|
-
* compress the timeline and every later timestamp would be wrong;
|
|
69
|
-
* silence keeps the clock honest and transcribes to nothing, which is
|
|
70
|
-
* exactly what happened. */
|
|
71
23
|
setMuted(muted: boolean): void;
|
|
72
24
|
readonly muted: boolean;
|
|
73
25
|
}
|
package/dist/mic.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
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
1
|
const WORKLET_SOURCE = `
|
|
6
2
|
class PCMProcessor extends AudioWorkletProcessor {
|
|
7
3
|
process(inputs) {
|
|
@@ -15,9 +11,7 @@ class PCMProcessor extends AudioWorkletProcessor {
|
|
|
15
11
|
}
|
|
16
12
|
registerProcessor('pcm-processor', PCMProcessor);
|
|
17
13
|
`;
|
|
18
|
-
/** Retry cadence for context resume / track re-acquire while stalled. */
|
|
19
14
|
const RECOVER_MS = 1000;
|
|
20
|
-
/** Capture clock heartbeat cadence. */
|
|
21
15
|
const CLOCK_MS = 500;
|
|
22
16
|
export async function captureMicrophone(onChunk, opts = {}) {
|
|
23
17
|
if (typeof navigator === 'undefined' || !navigator.mediaDevices) {
|
|
@@ -32,10 +26,6 @@ export async function captureMicrophone(onChunk, opts = {}) {
|
|
|
32
26
|
};
|
|
33
27
|
let muted = false;
|
|
34
28
|
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
29
|
let ctx;
|
|
40
30
|
try {
|
|
41
31
|
ctx = new AudioContext({ sampleRate: targetRate });
|
|
@@ -58,15 +48,11 @@ export async function captureMicrophone(onChunk, opts = {}) {
|
|
|
58
48
|
let buffered = [];
|
|
59
49
|
let readPos = 0;
|
|
60
50
|
let pending = null;
|
|
61
|
-
// Capture clock: cumulative resampled output samples vs wall time.
|
|
62
51
|
const t0 = performance.now();
|
|
63
52
|
let outSamples = 0;
|
|
64
|
-
// Zero-run detector state (input-rate samples).
|
|
65
53
|
const silentLimit = Math.round((opts.silentSeconds ?? 3) * inputRate);
|
|
66
54
|
let zeroRun = 0;
|
|
67
55
|
let silentFlagged = false;
|
|
68
|
-
// Stall bookkeeping: one active reason at a time; `onRecover` fires
|
|
69
|
-
// once when it clears.
|
|
70
56
|
let stopped = false;
|
|
71
57
|
let stall = null;
|
|
72
58
|
let recoverTimer = null;
|
|
@@ -130,27 +116,21 @@ export async function captureMicrophone(onChunk, opts = {}) {
|
|
|
130
116
|
}
|
|
131
117
|
if (out.length) {
|
|
132
118
|
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
119
|
if (stall !== null && stall !== 'silent')
|
|
136
120
|
clearStall();
|
|
137
121
|
pending = pending ? concat(pending, out) : Float32Array.from(out);
|
|
138
122
|
while (pending.length >= chunkSamples) {
|
|
139
123
|
const chunk = pending.slice(0, chunkSamples);
|
|
140
|
-
// Silence, not nothing: see `MicCapture.setMuted`.
|
|
141
124
|
onChunk(muted ? new Float32Array(chunk.length) : chunk);
|
|
142
125
|
pending = pending.slice(chunkSamples);
|
|
143
126
|
}
|
|
144
127
|
}
|
|
145
128
|
};
|
|
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
129
|
const clockTimer = opts.onClock
|
|
149
130
|
? setInterval(() => {
|
|
150
131
|
opts.onClock?.(outSamples, (performance.now() - t0) / 1000);
|
|
151
132
|
}, CLOCK_MS)
|
|
152
133
|
: null;
|
|
153
|
-
// --- recovery -------------------------------------------------------
|
|
154
134
|
const scheduleRecover = (fn) => {
|
|
155
135
|
if (stopped || recoverTimer)
|
|
156
136
|
return;
|
|
@@ -185,7 +165,6 @@ export async function captureMicrophone(onChunk, opts = {}) {
|
|
|
185
165
|
source = ctx.createMediaStreamSource(media);
|
|
186
166
|
source.connect(node);
|
|
187
167
|
watchTracks(media);
|
|
188
|
-
// `onRecover` fires from the first delivered block.
|
|
189
168
|
})
|
|
190
169
|
.catch(() => scheduleRecover(reacquireTrack));
|
|
191
170
|
};
|
|
@@ -201,7 +180,6 @@ export async function captureMicrophone(onChunk, opts = {}) {
|
|
|
201
180
|
if (!stopped)
|
|
202
181
|
setStall('track_muted');
|
|
203
182
|
});
|
|
204
|
-
// `unmute` needs no handler: the next block clears the stall.
|
|
205
183
|
}
|
|
206
184
|
};
|
|
207
185
|
watchTracks(media);
|