@vocalstack/js-sdk 1.1.0 → 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/ApiVersion.d.ts +0 -2
  25. package/dist/ApiVersion.js +0 -32
  26. package/dist/RestApi.d.ts +0 -21
  27. package/dist/RestApi.js +0 -95
  28. package/dist/VocalStackApi.d.ts +0 -19
  29. package/dist/VocalStackApi.js +0 -60
  30. package/dist/WebsocketApi.d.ts +0 -38
  31. package/dist/WebsocketApi.js +0 -150
  32. package/dist/apis/Billing.d.ts +0 -59
  33. package/dist/apis/Billing.js +0 -21
  34. package/dist/apis/LiveTranscription.d.ts +0 -58
  35. package/dist/apis/LiveTranscription.js +0 -55
  36. package/dist/apis/Polyglot.d.ts +0 -113
  37. package/dist/apis/Polyglot.js +0 -68
  38. package/dist/apis/Security.d.ts +0 -25
  39. package/dist/apis/Security.js +0 -35
  40. package/dist/apis/Transcriptions.d.ts +0 -60
  41. package/dist/apis/Transcriptions.js +0 -38
  42. package/dist/apis/UrlTranscription.d.ts +0 -59
  43. package/dist/apis/UrlTranscription.js +0 -53
  44. package/dist/apis/index.d.ts +0 -6
  45. package/dist/apis/index.js +0 -22
  46. package/dist/esm/ApiVersion.d.ts +0 -2
  47. package/dist/esm/ApiVersion.js +0 -28
  48. package/dist/esm/RestApi.d.ts +0 -21
  49. package/dist/esm/RestApi.js +0 -91
  50. package/dist/esm/VocalStackApi.d.ts +0 -19
  51. package/dist/esm/VocalStackApi.js +0 -56
  52. package/dist/esm/WebsocketApi.d.ts +0 -38
  53. package/dist/esm/WebsocketApi.js +0 -146
  54. package/dist/esm/apis/Billing.d.ts +0 -59
  55. package/dist/esm/apis/Billing.js +0 -17
  56. package/dist/esm/apis/LiveTranscription.d.ts +0 -58
  57. package/dist/esm/apis/LiveTranscription.js +0 -51
  58. package/dist/esm/apis/Polyglot.d.ts +0 -113
  59. package/dist/esm/apis/Polyglot.js +0 -64
  60. package/dist/esm/apis/Security.d.ts +0 -25
  61. package/dist/esm/apis/Security.js +0 -31
  62. package/dist/esm/apis/Transcriptions.d.ts +0 -60
  63. package/dist/esm/apis/Transcriptions.js +0 -34
  64. package/dist/esm/apis/UrlTranscription.d.ts +0 -59
  65. package/dist/esm/apis/UrlTranscription.js +0 -49
  66. package/dist/esm/apis/index.d.ts +0 -6
  67. package/dist/esm/apis/index.js +0 -6
  68. package/dist/esm/generics.zod.d.ts +0 -83
  69. package/dist/esm/generics.zod.js +0 -47
  70. package/dist/esm/index.d.ts +0 -2
  71. package/dist/esm/index.js +0 -2
  72. package/dist/generics.zod.d.ts +0 -83
  73. package/dist/generics.zod.js +0 -50
@@ -0,0 +1,55 @@
1
+ import type { ServerFrame, SubscriberFrame, TranscriptSnapshot } from '@vocalstack/contract';
2
+ export interface SubscribeOptions {
3
+ /** Read (and hear) the recording in this language. */
4
+ lang?: string;
5
+ /** Which voice speaks `lang` for this listener: a voice id from
6
+ * `vs.voices.available(lang)`. Omitted means the recording's
7
+ * `settings.voiceByLang` pin, then the engine's pick. */
8
+ voice?: string;
9
+ /** A private broadcast's listener token (the one VocalStack's listener page holds). */
10
+ listenerToken?: string;
11
+ /** Read a transcription through its share link. */
12
+ shareCode?: string;
13
+ /** With a token the socket is an owner subscriber (full frames, audience
14
+ * counts); `false` listens as a plain listener. */
15
+ asOwner?: boolean;
16
+ /** Live original audio (binary Opus frames) from the start. */
17
+ audio?: boolean;
18
+ }
19
+ export type SubscriptionEvent = {
20
+ type: 'snapshot';
21
+ snapshot: TranscriptSnapshot;
22
+ reason: 'initial' | 'gap';
23
+ } | {
24
+ type: 'frame';
25
+ frame: ServerFrame | SubscriberFrame;
26
+ seq?: number;
27
+ } | {
28
+ type: 'audio';
29
+ bytes: ArrayBuffer;
30
+ } | {
31
+ type: 'closed';
32
+ reason?: string;
33
+ };
34
+ type Snapshotter = (after: number | undefined) => Promise<TranscriptSnapshot>;
35
+ /** A recording's live fan-out (`vs.recordings.subscribe`). It resolves
36
+ * once the backfill snapshot is in place; live frames follow through the
37
+ * async iterator, with gaps detected on the cursor and healed by
38
+ * re-snapshotting. */
39
+ export declare class Subscription implements AsyncIterable<SubscriptionEvent> {
40
+ #private;
41
+ readonly recordingId: string;
42
+ /** Highest durable cursor applied (snapshot or frame). */
43
+ cursor: number;
44
+ snapshot: TranscriptSnapshot | null;
45
+ private constructor();
46
+ /** @internal */
47
+ static open(recordingId: string, url: string, bearer: string | undefined, opts: SubscribeOptions, snap: Snapshotter): Promise<Subscription>;
48
+ /** Switch the target language (`null` = as spoken). */
49
+ setLanguage(lang: string | null): void;
50
+ /** Opt in or out of live original audio. */
51
+ setAudio(enabled: boolean): void;
52
+ close(): void;
53
+ [Symbol.asyncIterator](): AsyncIterator<SubscriptionEvent>;
54
+ }
55
+ export {};
@@ -0,0 +1,164 @@
1
+ import { VocalStackError } from './errors.js';
2
+ import { openWebSocket, WS_OPEN } from './ws.js';
3
+ /** A recording's live fan-out (`vs.recordings.subscribe`). It resolves
4
+ * once the backfill snapshot is in place; live frames follow through the
5
+ * async iterator, with gaps detected on the cursor and healed by
6
+ * re-snapshotting. */
7
+ export class Subscription {
8
+ recordingId;
9
+ /** Highest durable cursor applied (snapshot or frame). */
10
+ cursor = -1;
11
+ snapshot = null;
12
+ #snap;
13
+ #opts;
14
+ #ws;
15
+ #queue = [];
16
+ #wake = null;
17
+ #closed = false;
18
+ #healing = false;
19
+ #buffering = [];
20
+ constructor(recordingId, ws, opts, snap) {
21
+ this.recordingId = recordingId;
22
+ this.#ws = ws;
23
+ this.#opts = opts;
24
+ this.#snap = snap;
25
+ }
26
+ /** @internal */
27
+ static async open(recordingId, url, bearer, opts, snap) {
28
+ const ws = await openWebSocket(url, bearer);
29
+ ws.binaryType = 'arraybuffer';
30
+ const sub = new Subscription(recordingId, ws, opts, snap);
31
+ await new Promise((resolve, reject) => {
32
+ const timer = setTimeout(() => reject(new VocalStackError('engine_unavailable', 'no subscribe.ready', { retryable: true })), 15_000);
33
+ ws.addEventListener('message', (ev) => {
34
+ if (typeof ev.data !== 'string') {
35
+ sub.#push({ type: 'audio', bytes: ev.data });
36
+ return;
37
+ }
38
+ let frame;
39
+ try {
40
+ frame = JSON.parse(ev.data);
41
+ }
42
+ catch {
43
+ return;
44
+ }
45
+ if (frame.type === 'subscribe.ready') {
46
+ clearTimeout(timer);
47
+ resolve();
48
+ return;
49
+ }
50
+ sub.#onFrame(frame);
51
+ });
52
+ ws.addEventListener('close', (ev) => {
53
+ clearTimeout(timer);
54
+ sub.#push({ type: 'closed', reason: ev?.reason });
55
+ sub.#closed = true;
56
+ sub.#wake?.();
57
+ reject(new VocalStackError('engine_unavailable', 'socket closed before subscribe.ready', { retryable: true }));
58
+ });
59
+ ws.addEventListener('error', () => {
60
+ clearTimeout(timer);
61
+ reject(new VocalStackError('engine_unavailable', 'subscriber socket error', { retryable: true }));
62
+ });
63
+ });
64
+ if (opts.audio)
65
+ sub.setAudio(true);
66
+ await sub.#resnapshot('initial');
67
+ return sub;
68
+ }
69
+ async #resnapshot(reason) {
70
+ const snap = await this.#snap(reason === 'gap' ? this.cursor : undefined);
71
+ this.snapshot = reason === 'initial' || !this.snapshot ? snap : mergeSnapshot(this.snapshot, snap);
72
+ this.cursor = Math.max(this.cursor, snap.lastSeq);
73
+ this.#push({ type: 'snapshot', snapshot: snap, reason });
74
+ const buffered = this.#buffering ?? [];
75
+ this.#buffering = null;
76
+ for (const b of buffered)
77
+ this.#onFrame(b.frame);
78
+ }
79
+ #onFrame(frame) {
80
+ // The relay cursor is `cursor`; segment-scoped frames use `seq` for the segment.
81
+ const seq = typeof frame.cursor === 'number' ? frame.cursor : typeof frame.seq === 'number' ? frame.seq : undefined;
82
+ if (this.#buffering) {
83
+ this.#buffering.push({ seq, frame });
84
+ return;
85
+ }
86
+ if (seq !== undefined) {
87
+ if (seq <= this.cursor)
88
+ return; // already in the snapshot
89
+ if (seq > this.cursor + 1 && !this.#healing) {
90
+ this.#healing = true;
91
+ this.#buffering = [{ seq, frame }];
92
+ this.#resnapshot('gap')
93
+ .catch(() => { })
94
+ .finally(() => {
95
+ this.#healing = false;
96
+ });
97
+ return;
98
+ }
99
+ this.cursor = seq;
100
+ if (frame.type === 'noop')
101
+ return;
102
+ }
103
+ this.#push({ type: 'frame', frame, seq });
104
+ }
105
+ #push(ev) {
106
+ this.#queue.push(ev);
107
+ this.#wake?.();
108
+ }
109
+ /** Switch the target language (`null` = as spoken). */
110
+ setLanguage(lang) {
111
+ // A gap heal re-reads the snapshot in the language being read now.
112
+ this.#opts.lang = lang ?? undefined;
113
+ this.#send({ type: 'subscribe.update', lang: lang ?? 'none' });
114
+ }
115
+ /** Opt in or out of live original audio. */
116
+ setAudio(enabled) {
117
+ this.#send({ type: 'subscribe.update', audio: enabled });
118
+ }
119
+ #send(frame) {
120
+ if (this.#ws.readyState === WS_OPEN)
121
+ this.#ws.send(JSON.stringify(frame));
122
+ }
123
+ close() {
124
+ if (this.#closed)
125
+ return;
126
+ this.#closed = true;
127
+ try {
128
+ this.#ws.close();
129
+ }
130
+ catch {
131
+ // already closed
132
+ }
133
+ this.#wake?.();
134
+ }
135
+ async *[Symbol.asyncIterator]() {
136
+ for (;;) {
137
+ if (this.#queue.length) {
138
+ yield this.#queue.shift();
139
+ continue;
140
+ }
141
+ if (this.#closed)
142
+ return;
143
+ await new Promise((r) => {
144
+ this.#wake = r;
145
+ });
146
+ this.#wake = null;
147
+ }
148
+ }
149
+ }
150
+ function mergeSnapshot(base, delta) {
151
+ const segs = new Map(base.segments.map((s) => [s.seq, s]));
152
+ for (const s of delta.segments)
153
+ segs.set(s.seq, s);
154
+ const sents = new Map(base.sentences.map((s) => [s.sentenceId, s]));
155
+ for (const s of delta.sentences)
156
+ sents.set(s.sentenceId, s);
157
+ return {
158
+ ...delta,
159
+ segments: [...segs.values()].sort((a, b) => a.seq - b.seq),
160
+ sentences: [...sents.values()].sort((a, b) => a.sentenceId - b.sentenceId),
161
+ speakers: delta.speakers.length ? delta.speakers : base.speakers,
162
+ spans: delta.spans.length ? delta.spans : base.spans,
163
+ };
164
+ }
package/dist/ws.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ export interface WsLike {
2
+ binaryType: string;
3
+ readyState: number;
4
+ send(data: string | ArrayBufferLike | Uint8Array): void;
5
+ close(code?: number, reason?: string): void;
6
+ addEventListener(type: string, listener: (ev: any) => void): void;
7
+ }
8
+ export declare const WS_OPEN = 1;
9
+ /** Open a socket. `bearer` is the caller's credential: Node sends it as
10
+ * an `Authorization` header (an API key is refused in a URL); a browser
11
+ * cannot set socket headers, so it rides in `access_token` instead. */
12
+ export declare function openWebSocket(url: string, bearer?: string): Promise<WsLike>;
package/dist/ws.js ADDED
@@ -0,0 +1,15 @@
1
+ export const WS_OPEN = 1;
2
+ const isNode = () => typeof globalThis.process?.versions?.node === 'string' &&
3
+ !globalThis.Deno &&
4
+ !globalThis.Bun;
5
+ /** Open a socket. `bearer` is the caller's credential: Node sends it as
6
+ * an `Authorization` header (an API key is refused in a URL); a browser
7
+ * cannot set socket headers, so it rides in `access_token` instead. */
8
+ export async function openWebSocket(url, bearer) {
9
+ if (!isNode() && typeof globalThis.WebSocket === 'function') {
10
+ const u = bearer ? `${url}${url.includes('?') ? '&' : '?'}access_token=${encodeURIComponent(bearer)}` : url;
11
+ return new globalThis.WebSocket(u);
12
+ }
13
+ const { WebSocket: NodeWebSocket } = await import('ws');
14
+ return new NodeWebSocket(url, bearer ? { headers: { authorization: `Bearer ${bearer}` } } : undefined);
15
+ }
package/package.json CHANGED
@@ -1,48 +1,44 @@
1
1
  {
2
2
  "name": "@vocalstack/js-sdk",
3
- "version": "1.1.0",
4
- "description": "A JavaScript API Client and SDK for VocalStack",
5
- "author": "VocalStack",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+ssh://git@github.com/vocalstack/js-sdk.git"
9
- },
3
+ "version": "2.0.0-alpha.0",
4
+ "description": "VocalStack speech API SDK for Node and browsers: one client (VocalStack) grouped by resource, typed from the generated contract, with realtime WebSocket streaming.",
5
+ "license": "MIT",
6
+ "type": "module",
10
7
  "engines": {
11
- "node": ">=16.20.2",
12
- "npm": ">=8.19.4"
8
+ "node": ">=20"
13
9
  },
14
10
  "main": "./dist/index.js",
15
- "typings": "./dist/index.d.ts",
16
- "module": "./dist/esm/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./mic": {
18
+ "types": "./dist/mic.d.ts",
19
+ "default": "./dist/mic.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "SKILL.md",
25
+ "skills"
26
+ ],
17
27
  "scripts": {
18
- "lint": "eslint",
19
- "build": "rm -rf ./dist && npm i && tsc && tsc -p tsconfig.esm.json",
20
- "docs": "rm -rf ./documentation && npm run --prefix ./../website script:docs -- $(node -e \"console.log(process.cwd())\")",
21
- "prepublish": "npm run docs",
22
- "prepare": "tsc -p tsconfig.esm.json"
28
+ "build": "npm run build --prefix contract && tsc -p tsconfig.json",
29
+ "test": "node --test test/*.test.mjs"
23
30
  },
24
31
  "dependencies": {
25
- "ws": "^8.18.0",
26
- "zod": "^3.23.8"
32
+ "@vocalstack/contract": "1.0.0-alpha.0",
33
+ "ws": "^8.18.0"
27
34
  },
28
35
  "devDependencies": {
29
- "@types/node": "^22.9.0",
30
- "eslint": "^8.57.0",
31
- "eslint-config-prettier": "^9.1.0",
32
- "eslint-plugin-prettier": "^5.1.3",
33
- "typescript": "^5.3.3"
36
+ "@types/node": "^22.0.0",
37
+ "@types/ws": "^8.5.0",
38
+ "typescript": "^5.6.0"
34
39
  },
35
- "publishConfig": {
36
- "access": "restricted"
37
- },
38
- "keywords": [
39
- "vocalstack",
40
- "api",
41
- "sdk",
42
- "transcription",
43
- "livestream",
44
- "ASR",
45
- "speech-to-text",
46
- "speech-to-speech"
47
- ]
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/vocalstack/js-sdk.git"
43
+ }
48
44
  }
@@ -0,0 +1,252 @@
1
+ ---
2
+ name: vocalstack
3
+ description: Use when writing Node or TypeScript code that calls the VocalStack speech API with @vocalstack/js-sdk, for example to transcribe an audio or video URL or a local file, stream live audio for realtime transcription, fetch or export a transcript (JSON, SRT, XLSX, PDF), or search transcripts. Covers installing, API keys, base URLs, five copy-paste programs and error handling (401, 402, 403, 429).
4
+ ---
5
+
6
+ # VocalStack TypeScript SDK
7
+
8
+ The VocalStack API transcribes speech (files, links, live audio), with speaker
9
+ labels and translation. `@vocalstack/js-sdk` has one client, `VocalStack`, with
10
+ one group per resource: `vs.transcriptions`, `vs.jobs`, `vs.recordings`,
11
+ `vs.broadcasts`, `vs.search`, `vs.realtime` and a few more (listed at the end).
12
+
13
+ ## What is what
14
+
15
+ - A **transcription** is a file, a link, a microphone or a stream, transcribed
16
+ and stored. Create one with `vs.transcriptions.create(...)`.
17
+ - A **recording** is one take inside it. Its id (`recordingId`) is what you
18
+ read the transcript and exports with: `vs.recordings.transcript(recordingId)`.
19
+ - A **job** is a file or link recording's transcription work:
20
+ `vs.jobs.wait(recordingId)` polls it until it is `done` or `failed`
21
+ (`{ onProgress: (job) => … }` sees each read: `job.state`, `job.progress.pct`).
22
+ - A **batch** is many files transcribed together for less than one
23
+ transcription each: `vs.batches.create()`, `addFile`/`addUrl` per file,
24
+ `start`, then `wait` (section 6).
25
+ - A **broadcast** is a link listeners open to follow a live recording.
26
+ - A **realtime session** (`vs.realtime`) is a socket you stream audio into and
27
+ read the transcript from. Nothing is stored. "Session" means only this.
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ npm init -y && npm pkg set type=module # in a new folder: the programs below are ESM
33
+ npm i @vocalstack/js-sdk # Node 20 or newer
34
+ ```
35
+
36
+ Before the package is published, any account installs
37
+ the private build its environment serves: the API reference's Install section
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
41
+ first (`npm run build` also builds the bundled `contract/` package):
42
+
43
+ ```sh
44
+ git clone https://github.com/vocalstack/js-sdk && (cd js-sdk && npm install && npm run build)
45
+ npm i ./js-sdk
46
+ ```
47
+
48
+ Use ESM (`"type": "module"` in package.json, or `.mjs` files).
49
+
50
+ Speaker numbers start at 0. A speaker of `-1` means speaker labels were off
51
+ for that transcription, so print the text without a speaker.
52
+
53
+ ## Auth and base URL
54
+
55
+ - Read the key from `VOCALSTACK_API_KEY` and pass it as `apiKey`. It goes out
56
+ as `Authorization: Bearer <key>`, also on sockets.
57
+ - An API key starts with `vs_key_`. There is one kind of key: every call is
58
+ real usage on the account's tier and quotas.
59
+ - Keys are for servers. Never put a key in browser code or in a URL. The API
60
+ refuses a key sent from a web page (`Origin` header) or in a query string.
61
+ - Scopes: `read` (read transcripts, search, export), `write` and
62
+ `recordings:publish` (create transcriptions, stream live audio), or `*`.
63
+ - Base URL: `https://api.vocalstack.com` (the default). Override it with
64
+ `VOCALSTACK_BASE_URL` (for example, a local or staging server).
65
+
66
+ Every program below starts like this:
67
+
68
+ ```js
69
+ import { VocalStack } from '@vocalstack/js-sdk';
70
+ const vs = new VocalStack({
71
+ baseUrl: process.env.VOCALSTACK_BASE_URL, // absent: https://api.vocalstack.com
72
+ apiKey: process.env.VOCALSTACK_API_KEY,
73
+ });
74
+ ```
75
+
76
+ ## 1. Transcribe a URL and wait for the result
77
+
78
+ The server fetches the link itself, so it must be a public http(s) address.
79
+ Private, loopback and cloud-metadata addresses are refused.
80
+
81
+ ```js
82
+ import { VocalStack } from '@vocalstack/js-sdk';
83
+ const vs = new VocalStack({ baseUrl: process.env.VOCALSTACK_BASE_URL, apiKey: process.env.VOCALSTACK_API_KEY });
84
+
85
+ const tx = await vs.transcriptions.create({
86
+ url: 'https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac',
87
+ name: 'JFK sample',
88
+ });
89
+ const recordingId = tx.job.recordingId;
90
+
91
+ // queued -> fetching -> transcribing -> done | failed
92
+ const job = await vs.jobs.wait(recordingId);
93
+ if (job.state !== 'done') throw new Error(`transcription ${job.state}: ${job.error}`);
94
+
95
+ const { sentences } = await vs.recordings.transcript(recordingId);
96
+ for (const s of sentences) console.log(s.speaker >= 0 ? `[speaker ${s.speaker}] ${s.text}` : s.text);
97
+ console.log('recordingId', recordingId);
98
+ ```
99
+
100
+ ## 2. Transcribe a local file
101
+
102
+ ```js
103
+ import { VocalStack } from '@vocalstack/js-sdk';
104
+ const vs = new VocalStack({ baseUrl: process.env.VOCALSTACK_BASE_URL, apiKey: process.env.VOCALSTACK_API_KEY });
105
+
106
+ const tx = await vs.transcriptions.create({
107
+ 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: 'off' (default) | 'auto' | 'on'
109
+ // Off unless asked for: settings.summary, settings.title, settings.summaryTranslations,
110
+ // settings.voice (spoken translations), and shareLink: true (a public share link).
111
+ });
112
+ const job = await vs.jobs.wait(tx.job.recordingId);
113
+ if (job.state !== 'done') throw new Error(`transcription ${job.state}: ${job.error}`);
114
+
115
+ const { sentences } = await vs.recordings.transcript(job.recordingId);
116
+ console.log(sentences.map((s) => s.text).join(' '));
117
+ ```
118
+
119
+ ## 3. Fetch a transcript or an export file
120
+
121
+ `recordings.transcript` returns segments (with word timings), sentences and
122
+ speakers. `recordings.export` returns the file as a `Response`: formats `json`,
123
+ `srt`, `xlsx`, `pdf`; add `{ lang: '<code>' }` for a translated file.
124
+
125
+ ```js
126
+ import { writeFile } from 'node:fs/promises';
127
+ import { VocalStack } from '@vocalstack/js-sdk';
128
+ const vs = new VocalStack({ baseUrl: process.env.VOCALSTACK_BASE_URL, apiKey: process.env.VOCALSTACK_API_KEY });
129
+ const recordingId = process.argv[2];
130
+
131
+ const snap = await vs.recordings.transcript(recordingId);
132
+ console.log(`${snap.segments.length} segments, ${snap.sentences.length} sentences, state ${snap.state}`);
133
+
134
+ const res = await vs.recordings.export(recordingId, 'srt');
135
+ await writeFile('transcript.srt', Buffer.from(await res.arrayBuffer()));
136
+ ```
137
+
138
+ ## 4. Live transcription from a microphone or a file
139
+
140
+ A live transcription takes raw PCM: 32-bit float little-endian, 16 kHz, mono.
141
+ Create it, start a recording, and open the publisher socket the grant names.
142
+ Here ffmpeg decodes a file at realtime speed (`-re`). For a microphone, swap
143
+ the input: `-f avfoundation -i :0` on macOS, `-f pulse -i default` on Linux.
144
+ (`@vocalstack/js-sdk/mic` is for browsers, and browsers must not hold API keys.)
145
+
146
+ ```js
147
+ import { spawn } from 'node:child_process';
148
+ import { VocalStack } from '@vocalstack/js-sdk';
149
+ const vs = new VocalStack({ baseUrl: process.env.VOCALSTACK_BASE_URL, apiKey: process.env.VOCALSTACK_API_KEY });
150
+
151
+ const tx = await vs.transcriptions.createLive({ name: 'Live demo', settings: { spokenLanguage: 'en' } });
152
+ const grant = await vs.transcriptions.startRecording(tx.id);
153
+ const stream = await vs.recordings.publish(grant, { readyDeadlineMs: 180_000 }); // a cold engine can take a while
154
+
155
+ const ff = spawn('ffmpeg', ['-loglevel', 'error', '-re', '-i', process.argv[2],
156
+ '-f', 'f32le', '-ac', '1', '-ar', '16000', '-']);
157
+ ff.stdout.on('data', (chunk) => stream.sendAudio(new Uint8Array(chunk)));
158
+ ff.on('close', () => stream.eof()); // eof() ends the recording
159
+
160
+ for await (const ev of stream) {
161
+ if (ev.type === 'transcript.committed') console.log(`[speaker ${ev.segment.speaker}] ${ev.segment.text}`);
162
+ // also: 'transcript.draft' (replaces the live tail), 'transcript.sentence', 'transcript.translated'
163
+ }
164
+
165
+ // To end a recording without eof(): vs.recordings.pause(id), then vs.recordings.stop(id).
166
+ const { sentences } = await vs.recordings.transcript(grant.recording.id);
167
+ console.log('final:', sentences.map((s) => s.text).join(' '));
168
+ ```
169
+
170
+ To transcribe without storing anything, use a realtime session instead:
171
+ `await vs.realtime.transcribeFile('talk.mp3')` returns `{ text, segments }`.
172
+
173
+ ## 5. Search transcripts
174
+
175
+ ```js
176
+ import { VocalStack } from '@vocalstack/js-sdk';
177
+ const vs = new VocalStack({ baseUrl: process.env.VOCALSTACK_BASE_URL, apiKey: process.env.VOCALSTACK_API_KEY });
178
+
179
+ const { hits } = await vs.search.query(process.argv[2] ?? 'country', { limit: 5 }); // q: 2+ characters
180
+ for (const h of hits) {
181
+ console.log(`${h.linkName} @ ${h.mediaStart.toFixed(1)}s [speaker ${h.speaker}]: ${h.text} (recording ${h.recordingId})`);
182
+ }
183
+ ```
184
+
185
+ ## 6. A batch of files
186
+
187
+ Many files at once cost less as a batch: one create, one minimum charge for
188
+ the whole batch. Its files get transcripts, exports and speaker labels (when
189
+ `settings.speakerLabels` asks), never a summary, title, share link or voices.
190
+
191
+ ```js
192
+ import { VocalStack } from '@vocalstack/js-sdk';
193
+ const vs = new VocalStack({ baseUrl: process.env.VOCALSTACK_BASE_URL, apiKey: process.env.VOCALSTACK_API_KEY });
194
+
195
+ const batch = await vs.batches.create({ name: 'Interviews', settings: { spokenLanguage: 'en' } });
196
+ for (const path of process.argv.slice(2)) await vs.batches.addFile(batch.id, { file: path }); // or addUrl(batch.id, url)
197
+ await vs.batches.start(batch.id);
198
+ const done = await vs.batches.wait(batch.id); // done (or stopped); check done.counts.failed
199
+ for (const tx of done.files ?? []) {
200
+ if (tx.job?.state !== 'done') continue;
201
+ const { sentences } = await vs.recordings.transcript(tx.job.recordingId);
202
+ console.log(tx.name, sentences.map((s) => s.text).join(' '));
203
+ }
204
+ // vs.batches.export(batch.id, 'srt') is every finished transcript in one zip.
205
+ ```
206
+
207
+ ## Errors
208
+
209
+ A failed call throws `VocalStackError` with `code`, `message`, `status`,
210
+ `retryable`, `details` (the body's `data.details`), `headers` and
211
+ `retryAfter` (seconds, from `Retry-After`). A `429`, and a `503 unavailable`
212
+ on a call that is safe to repeat, is retried for you after its `Retry-After`,
213
+ twice by default (`maxRetries` on the client).
214
+
215
+ | Status | `code` | What to do |
216
+ |---|---|---|
217
+ | 401 | `invalid_token`, `key_expired`, `key_revoked` | Check `VOCALSTACK_API_KEY`. Use the replacement key, or create a new one. |
218
+ | 403 | `forbidden` | Usually a missing scope: the message names it, and `err.headers.get('www-authenticate')` lists the scopes the route needs. A suspended key or account gets `suspended`. |
219
+ | 403 | `terms_acceptance_required` | The account has not accepted the current Terms of Service and Privacy Policy: someone signs in to the Dashboard and accepts them (an organisation's keys work once an owner or admin has). Not retryable until then. |
220
+ | 402 | `insufficient_credit` | The account's credit is out, or does not cover the transcription: every transcription is billed at least the plan's minimum (`rates.minimumSeconds`, 5 minutes). `details`: `balance`, `plan`, `topUpUrl`, and `required` when the cost was known up front. Add credit; `vs.billing.get()` shows the balance. |
221
+ | 409 | `payment_method_required` | Auto top-up (`vs.billing.setAutoTopUp`) and the portal (`vs.billing.portal()`) need a payment first (`vs.billing.createCheckout({ kind: 'topup', amount, acceptTerms: true })` opens one; a signed-in person's token, never an API key). |
222
+ | 402 | `quota_exceeded` | The plan's transcriptions-at-once limit is reached. `details`: `metric` (`concurrent_transcriptions`), `limit`, `used`, `plan`, `topUpUrl`. |
223
+ | 429 | `rate_limited` | Retried automatically; if it still fails, wait `err.retryAfter` seconds. Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
224
+ | 503 | `unavailable` | The service is waking (its database was asleep). Retried automatically after `Retry-After` (2 s) on calls that are safe to repeat: GET, HEAD, DELETE, or any call with `{ idempotencyKey }`. A create without a key is not retried: retry it yourself, with an `idempotencyKey` so it cannot run twice. A realtime socket refused this way throws `unavailable`; a bare 503 there is `capacity_exceeded`. |
225
+
226
+ ```js
227
+ import { VocalStack, VocalStackError } from '@vocalstack/js-sdk';
228
+ const vs = new VocalStack({ baseUrl: process.env.VOCALSTACK_BASE_URL, apiKey: process.env.VOCALSTACK_API_KEY });
229
+
230
+ try {
231
+ await vs.search.query('country');
232
+ } catch (err) {
233
+ if (!(err instanceof VocalStackError)) throw err;
234
+ if (err.code === 'insufficient_credit' || err.code === 'quota_exceeded') console.error(err.message, err.details);
235
+ else console.error(err.status, err.code, err.message);
236
+ }
237
+ ```
238
+
239
+ ## More
240
+
241
+ - The other groups: `vs.transcriptions` (`list`, `get`, `update`, `archive`,
242
+ `restore`, `purge`, `listShareLinks`, `createShareLink`), `vs.recordings`
243
+ (`get`, `summarize`, `summary`, `translation`, `audioUrl`, `subscribe` for a
244
+ live transcript feed), `vs.batches` (`create`, `addFile`, `addUrl`, `start`,
245
+ `wait`, `get`, `list`, `export`, `stop`, `delete`), `vs.corrections`, `vs.shareLinks`, `vs.voices`,
246
+ `vs.broadcasts`, `vs.me` (`get`, `usage`), `vs.languages`,
247
+ `vs.rtmp`, `vs.events`, `vs.meta`. Each method is named after its operation
248
+ in the OpenAPI document (`transcriptions.create` is `POST /v1/transcriptions`).
249
+ - `<base>/llms.txt` lists every operation in a form written for agents.
250
+ `<base>/llms-full.txt` adds parameters, auth mode, tier and scopes.
251
+ - `<base>/docs` is the interactive reference. `<base>/v1/openapi.json` is
252
+ the OpenAPI document; the SDK's types are generated from it.
@@ -1,2 +0,0 @@
1
- export declare const EXPECTED_VOCALSTACK_API_VERSION = "1.1.0";
2
- export declare const verifyApiVersion: (actualVersion: string | null, discontinued?: boolean) => void;
@@ -1,32 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.verifyApiVersion = exports.EXPECTED_VOCALSTACK_API_VERSION = void 0;
4
- exports.EXPECTED_VOCALSTACK_API_VERSION = '1.1.0';
5
- const verifyApiVersion = (actualVersion, discontinued = false) => {
6
- if (!actualVersion) {
7
- throw new Error('VocalStack API version is missing.');
8
- }
9
- if (discontinued) {
10
- throw Error(`The VocalStack API has undergone a new release (v.${actualVersion}) and no longer supports this SDK. Please update the SDK to the latest version by running 'npm update @vocalstack/js-sdk'.`);
11
- }
12
- const [major, minor, patch] = parseVersion(actualVersion);
13
- const [expectedMajor, expectedMinor, expectedPatch] = exports.EXPECTED_VOCALSTACK_API_VERSION.split('.').map((v) => parseInt(v, 10));
14
- if (major < expectedMajor ||
15
- (major === expectedMajor && minor < expectedMinor) ||
16
- (major === expectedMajor && minor === expectedMinor && patch < expectedPatch)) {
17
- throw new Error(`VocalStack API version ${actualVersion} is lower than that expected by this SDK. Expected version >= ${exports.EXPECTED_VOCALSTACK_API_VERSION}`);
18
- }
19
- if (major > expectedMajor || (major === expectedMajor && minor > expectedMinor)) {
20
- const type = major > expectedMajor ? 'major' : 'minor';
21
- console[type === 'major' ? 'error' : 'warn'](`The VocalStack API has undergone a new ${type} release (v.${actualVersion}). Your SDK version may not support all new API features. Consider updating by running 'npm update @vocalstack/js-sdk'.`);
22
- }
23
- };
24
- exports.verifyApiVersion = verifyApiVersion;
25
- function parseVersion(version) {
26
- const regex = /^\d+\.\d+\.\d+$/;
27
- if (!regex.test(version)) {
28
- throw new Error(`Invalid version format: ${version}. Expected format "major.minor.patch" with numeric segments.`);
29
- }
30
- const segments = version.split('.').map((v) => parseInt(v, 10));
31
- return [segments[0], segments[1], segments[2]];
32
- }
package/dist/RestApi.d.ts DELETED
@@ -1,21 +0,0 @@
1
- export type ApiResponse<T> = {
2
- status: 'waiting' | 'processing' | 'done' | 'error';
3
- data?: null | T;
4
- success: boolean;
5
- error?: string;
6
- };
7
- export type RequestOptions = RequestInit & {
8
- json?: Record<string, unknown>;
9
- };
10
- export declare class RestApi {
11
- private apiServer;
12
- private headers;
13
- constructor(apiServer: string, headers: Record<string, string>);
14
- get<T>(path: string, options?: RequestOptions): Promise<ApiResponse<T>>;
15
- post<T>(path: string, options?: RequestOptions): Promise<ApiResponse<T>>;
16
- put<T>(path: string, options?: RequestOptions): Promise<ApiResponse<T>>;
17
- patch<T>(path: string, options?: RequestOptions): Promise<ApiResponse<T>>;
18
- delete<T>(path: string, options?: RequestOptions): Promise<ApiResponse<T>>;
19
- private apiCall;
20
- private respond;
21
- }