@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/dist/resources.js CHANGED
@@ -3,6 +3,10 @@ import { listenerQuery, seg } from './http.js';
3
3
  import { RealtimeSession } from './realtime.js';
4
4
  import { Subscription } from './subscription.js';
5
5
  import { openWebSocket } from './ws.js';
6
+ function filterQuery(f) {
7
+ const at = (d) => (d instanceof Date ? d.toISOString() : d);
8
+ return { lifecycle: f.lifecycle, q: f.q, site: f.site, language: f.language, kind: f.kind, origin: f.origin, tag: f.tag, broadcast: f.broadcast, createdAfter: at(f.createdAfter), createdBefore: at(f.createdBefore), shared: f.shared };
9
+ }
6
10
  async function toBlob(file) {
7
11
  if (typeof file === 'string') {
8
12
  const fsSpecifier = 'node:' + 'fs';
@@ -14,8 +18,6 @@ async function toBlob(file) {
14
18
  }
15
19
  return { blob: new Blob([file]) };
16
20
  }
17
- /** Every item of a paged list: each page asked for with the previous
18
- * page's `nextCursor` until one comes back without it. */
19
21
  async function allPages(page) {
20
22
  const out = [];
21
23
  let cursor;
@@ -27,8 +29,6 @@ async function allPages(page) {
27
29
  cursor = r.nextCursor;
28
30
  }
29
31
  }
30
- /** What broadcasts and transcriptions have in common: both hold
31
- * recordings and share links. */
32
32
  class ParentResource {
33
33
  t;
34
34
  constructor(t) {
@@ -37,8 +37,6 @@ class ParentResource {
37
37
  p(id, rest = '') {
38
38
  return `${this.base}/${seg(id)}${rest}`;
39
39
  }
40
- /** Every one, newest first: a paged list is read to its last page
41
- * (`transcriptions.listPage` reads one). */
42
40
  list(lifecycle = 'active') {
43
41
  return allPages(async (cursor) => {
44
42
  const r = await this.t.json('GET', this.base, { query: { lifecycle, cursor } });
@@ -48,36 +46,36 @@ class ParentResource {
48
46
  get(id) {
49
47
  return this.t.json('GET', this.p(id));
50
48
  }
51
- /** Rename, change the default capture settings (a broadcast: also its access, password or link). */
52
49
  update(id, req) {
53
50
  return this.t.json('PATCH', this.p(id), { body: req });
54
51
  }
55
- /** Hidden, its links dead, its data kept until `restore` or `purge`. */
56
52
  archive(id) {
57
53
  return this.t.json('POST', this.p(id, '/archive'));
58
54
  }
59
55
  restore(id) {
60
56
  return this.t.json('POST', this.p(id, '/restore'));
61
57
  }
62
- /** Delete an archived one and everything in it, for good. */
63
58
  async purge(id) {
64
59
  await this.t.json('POST', this.p(id, '/purge'));
65
60
  }
66
- /** Every recording, newest first (every page of `listRecordingsPage`). */
61
+ archiveMany(ids) {
62
+ return this.t.json('POST', `${this.base}/archive`, { body: { ids } });
63
+ }
64
+ restoreMany(ids) {
65
+ return this.t.json('POST', `${this.base}/restore`, { body: { ids } });
66
+ }
67
+ purgeMany(ids) {
68
+ return this.t.json('POST', `${this.base}/purge`, { body: { ids } });
69
+ }
67
70
  listRecordings(id, lifecycle = 'active') {
68
71
  return allPages(async (cursor) => {
69
72
  const r = await this.listRecordingsPage(id, { lifecycle, cursor });
70
73
  return { items: r.recordings, nextCursor: r.nextCursor };
71
74
  });
72
75
  }
73
- /** One page of recordings, newest first: pass its `nextCursor` as `cursor` for the next. */
74
76
  listRecordingsPage(id, opts = {}) {
75
77
  return this.t.json('GET', this.p(id, '/recordings'), { query: { lifecycle: opts.lifecycle, cursor: opts.cursor, limit: opts.limit } });
76
78
  }
77
- /** Start a live recording. The grant's `streamUrl` is the publisher
78
- * socket (`vs.recordings.publish(grant)`). A 409 `conflict` with
79
- * `details.takeover` means another device is recording: retry with
80
- * `{ takeover: true }` to record here instead. */
81
79
  startRecording(id, req = {}, call = {}) {
82
80
  return this.t.json('POST', this.p(id, '/recordings'), { body: req, ...call });
83
81
  }
@@ -88,52 +86,43 @@ class ParentResource {
88
86
  createShareLink(id, req) {
89
87
  return this.t.json('POST', this.p(id, '/share-links'), { body: req });
90
88
  }
91
- /** The events socket a waiting screen needs: `recording.started` (it is
92
- * live), `recording.ended`, `job.progress`, `stream.ingest`.
93
- * Each names the transcription the recording is made in (`transcriptionId`). */
94
89
  async events(id, as = {}) {
95
90
  const token = as.asOwner === false ? undefined : await this.t.bearer();
96
91
  return openWebSocket(this.t.wsUrl(this.p(id, '/events'), listenerQuery(as)), token);
97
92
  }
98
93
  }
99
- /** `vs.broadcasts`: the links listeners open (design 77). */
100
94
  export class Broadcasts extends ParentResource {
101
95
  base = '/v1/broadcasts';
102
96
  key = 'broadcasts';
103
97
  create(req, call = {}) {
104
98
  return this.t.json('POST', this.base, { body: req, ...call });
105
99
  }
106
- /** Give a broadcast a stream it can record from (a YouTube live, an
107
- * internet radio address). Returns the broadcast. */
108
100
  addAudioSource(id, req) {
109
101
  return this.t.json('POST', this.p(id, '/audio-sources'), { body: req });
110
102
  }
111
103
  removeAudioSource(id, sourceId) {
112
104
  return this.t.json('DELETE', this.p(id, `/audio-sources/${seg(sourceId)}`));
113
105
  }
114
- /** Whether that source can be recorded right now; `fresh` skips the short cache. */
115
106
  checkAudioSource(id, sourceId, opts = {}) {
116
107
  return this.t.json('GET', this.p(id, `/audio-sources/${seg(sourceId)}/check`), { query: { fresh: opts.fresh || undefined } });
117
108
  }
118
109
  }
119
- /** `vs.transcriptions`: a file, a link, a microphone or a stream, transcribed and stored. */
120
110
  export class Transcriptions extends ParentResource {
121
111
  base = '/v1/transcriptions';
122
112
  key = 'transcriptions';
123
- /** One page of transcriptions, newest first: pass its `nextCursor` as
124
- * `cursor` for the next; the last page has none. */
113
+ list(opts = 'active') {
114
+ const f = typeof opts === 'string' ? { lifecycle: opts } : opts;
115
+ return allPages(async (cursor) => {
116
+ const r = await this.listPage({ ...f, cursor });
117
+ return { items: r.transcriptions, nextCursor: r.nextCursor };
118
+ });
119
+ }
125
120
  listPage(opts = {}) {
126
- return this.t.json('GET', this.base, { query: { lifecycle: opts.lifecycle, cursor: opts.cursor, limit: opts.limit } });
121
+ return this.t.json('GET', this.base, { query: { ...filterQuery(opts), cursor: opts.cursor, limit: opts.limit } });
127
122
  }
128
- /** A file or link recording in this transcription: no live phase, transcribed at full speed.
129
- * It has no job: open the grant's socket with `vs.recordings.publish(grant)` (with `url`
130
- * the server fetches the link) and read it until the session is done. */
131
123
  uploadRecording(id, req = {}, call = {}) {
132
124
  return this.t.json('POST', this.p(id, '/recordings/upload'), { body: req, ...call });
133
125
  }
134
- /** From a file (`file`) or a public link (`url`). The answer's
135
- * `job.recordingId` is the job: `vs.jobs.wait(recordingId)`,
136
- * then `vs.recordings.transcript(recordingId)`. */
137
126
  async create(req, call = {}) {
138
127
  const { file, fileName, ...options } = req;
139
128
  if (file === undefined)
@@ -144,20 +133,19 @@ export class Transcriptions extends ParentResource {
144
133
  fd.append('file', blob, fileName ?? name ?? 'upload');
145
134
  return this.t.json('POST', this.base, { body: fd, ...call });
146
135
  }
147
- /** `create({ url, ...opts })`. */
148
136
  createFromUrl(url, opts = {}, call = {}) {
149
137
  return this.create({ ...opts, url }, call);
150
138
  }
151
- /** A transcription captured as it is spoken: a microphone, or a stream
152
- * the server pulls. Created idle: `startRecording(id)` starts it. */
153
139
  createLive(req, call = {}) {
154
140
  return this.t.json('POST', `${this.base}/live`, { body: req, ...call });
155
141
  }
156
- /** Whether a link is live (a live transcription) or a file. */
142
+ async lookup(url) {
143
+ const r = await this.t.json('GET', `${this.base}/lookup`, { query: { url } });
144
+ return r.transcription ?? null;
145
+ }
157
146
  probe(url) {
158
147
  return this.t.json('GET', `${this.base}/probe`, { query: { url } });
159
148
  }
160
- /** Feed a broadcast from a live transcription. Returns the transcription. */
161
149
  attachBroadcast(id, broadcastId) {
162
150
  return this.t.json('PUT', this.p(id, `/broadcasts/${seg(broadcastId)}`));
163
151
  }
@@ -165,11 +153,6 @@ export class Transcriptions extends ParentResource {
165
153
  return this.t.json('DELETE', this.p(id, `/broadcasts/${seg(broadcastId)}`));
166
154
  }
167
155
  }
168
- /** `vs.batches`: several files transcribed together, for less than a
169
- * transcription each (one create, one minimum charge). Create a batch, add
170
- * each file or link with `addFile`, then `start` it; `get` shows its files
171
- * (each a transcription with its job) and their counts. A batch's files get
172
- * no summary, title, share link or spoken translations. */
173
156
  export class Batches {
174
157
  #t;
175
158
  constructor(t) {
@@ -178,31 +161,24 @@ export class Batches {
178
161
  #p(id, rest = '') {
179
162
  return `/v1/batches/${seg(id)}${rest}`;
180
163
  }
181
- /** An open batch, ready for files (`name` defaults to "Batch of <date>"). */
182
164
  create(req = {}, call = {}) {
183
165
  return this.#t.json('POST', '/v1/batches', { body: req, ...call });
184
166
  }
185
- /** Every batch, newest first (every page of `listPage`); each with its counts, not its files. */
186
167
  list() {
187
168
  return allPages(async (cursor) => {
188
169
  const r = await this.listPage({ cursor });
189
170
  return { items: r.batches, nextCursor: r.nextCursor };
190
171
  });
191
172
  }
192
- /** One page of batches, newest first: pass its `nextCursor` as `cursor` for the next. */
193
173
  listPage(opts = {}) {
194
174
  return this.#t.json('GET', '/v1/batches', { query: { cursor: opts.cursor, limit: opts.limit } });
195
175
  }
196
- /** A batch with its files (`files`, each a transcription with its job), in the order they were added. */
197
176
  get(id) {
198
177
  return this.#t.json('GET', this.#p(id));
199
178
  }
200
- /** Discard a batch that was never started, with its files (nothing was charged). */
201
179
  async delete(id) {
202
180
  await this.#t.json('DELETE', this.#p(id));
203
181
  }
204
- /** Add one file (`file`) or link (`url`) to an open batch: a transcription
205
- * of its own (`batchId`), waiting for the batch's `start`. */
206
182
  async addFile(id, req, call = {}) {
207
183
  const { file, fileName, ...options } = req;
208
184
  if (file === undefined)
@@ -213,36 +189,24 @@ export class Batches {
213
189
  fd.append('file', blob, fileName ?? name ?? 'upload');
214
190
  return this.#t.json('POST', this.#p(id, '/files'), { body: fd, ...call });
215
191
  }
216
- /** `addFile(id, { url, ...opts })`. */
217
192
  addUrl(id, url, opts = {}, call = {}) {
218
193
  return this.addFile(id, { ...opts, url }, call);
219
194
  }
220
- /** Seal the batch and queue its files, longest first; `wait` follows it to the end. */
221
195
  start(id, call = {}) {
222
196
  return this.#t.json('POST', this.#p(id, '/start'), call);
223
197
  }
224
- /** Every finished file's transcript in one `format` (as `recordings.export`
225
- * makes one), in one zip, as a `Response`: `.arrayBuffer()` it. Files
226
- * still running or failed are left out. */
227
198
  export(id, format) {
228
199
  return this.#t.raw('GET', this.#p(id, '/export'), { headers: { accept: '*/*' }, query: { format } });
229
200
  }
230
- /** Stop every file not finished yet: queued files never start, a running
231
- * one is cancelled and billed for what it transcribed. */
232
201
  stop(id) {
233
202
  return this.#t.json('POST', this.#p(id, '/stop'));
234
203
  }
235
- /** Poll a started batch until it is `done` (every file finished or
236
- * failed) or `stopped`, and return it with its files. It does not throw
237
- * on a failed file: check `counts.failed` and each file's `job.state`.
238
- * `onProgress` sees every read, the last one included. */
239
204
  async wait(id, opts = {}) {
240
205
  const interval = opts.intervalMs ?? 5000;
241
206
  const deadline = opts.timeoutMs ? Date.now() + opts.timeoutMs : Infinity;
242
207
  for (;;) {
243
208
  const batch = await this.get(id);
244
209
  opts.onProgress?.(batch);
245
- // Closed: every file over (a stopped batch's running file too).
246
210
  if ((batch.state === 'done' || batch.state === 'stopped') && batch.finishedAt)
247
211
  return batch;
248
212
  if (batch.state === 'open')
@@ -255,7 +219,6 @@ export class Batches {
255
219
  }
256
220
  }
257
221
  }
258
- /** `vs.recordings`: one take, live or uploaded. */
259
222
  export class Recordings {
260
223
  #t;
261
224
  constructor(t) {
@@ -273,19 +236,15 @@ export class Recordings {
273
236
  resume(id) {
274
237
  return this.#t.json('POST', this.#p(id, '/resume'));
275
238
  }
276
- /** Only from paused. */
277
239
  stop(id, reason) {
278
240
  return this.#t.json('POST', this.#p(id, '/stop'), { body: { reason } });
279
241
  }
280
242
  settings(id, patch) {
281
243
  return this.#t.json('PATCH', this.#p(id, '/settings'), { body: patch });
282
244
  }
283
- /** A new publisher grant for a recording that is already live. */
284
245
  publishToken(id, opts = {}) {
285
246
  return this.#t.json('POST', this.#p(id, '/publish-token'), { body: { takeover: opts.takeover ?? false } });
286
247
  }
287
- /** Open the publisher socket a grant names and wait until it is ready:
288
- * send raw PCM (f32le, 16 kHz, mono) with `sendAudio`, end with `eof()`. */
289
248
  publish(grant, opts = {}) {
290
249
  if (!grant.streamUrl)
291
250
  throw new VocalStackError('invalid_state', 'this grant has no streamUrl (an upload recording has no publisher socket)');
@@ -306,42 +265,38 @@ export class Recordings {
306
265
  async purge(id) {
307
266
  await this.#t.json('POST', this.#p(id, '/purge'));
308
267
  }
309
- /** The transcript: segments (with word timings), sentences and speakers,
310
- * up to `lastSeq`; `after` continues from a cursor, `lang` reads it translated. */
268
+ archiveMany(ids) {
269
+ return this.#t.json('POST', '/v1/recordings/archive', { body: { ids } });
270
+ }
271
+ restoreMany(ids) {
272
+ return this.#t.json('POST', '/v1/recordings/restore', { body: { ids } });
273
+ }
274
+ purgeMany(ids) {
275
+ return this.#t.json('POST', '/v1/recordings/purge', { body: { ids } });
276
+ }
311
277
  transcript(id, opts = {}) {
312
278
  return this.#t.json('GET', this.#p(id, '/transcript'), {
313
279
  query: { ...listenerQuery(opts), after: opts.after, lang: opts.lang, role: opts.asListener ? 'listener' : undefined },
314
280
  });
315
281
  }
316
- /** Translate the given sentences (at most 40 ids per call); the answer
317
- * carries only what this call translated. */
318
282
  translation(id, lang, sentences = [], as) {
319
283
  return this.#t.json('GET', this.#p(id, '/translation'), {
320
284
  query: { ...listenerQuery(as), lang, sentences: sentences.length ? sentences.join(',') : undefined },
321
285
  });
322
286
  }
323
- /** Queue the AI title and summary of a finished recording. */
324
287
  summarize(id, call = {}) {
325
288
  return this.#t.json('POST', this.#p(id, '/summarize'), call);
326
289
  }
327
- /** The title and summary as they stand, in `lang` when given. */
328
290
  summary(id, opts = {}) {
329
291
  return this.#t.json('GET', this.#p(id, '/summary'), { query: { ...listenerQuery(opts), lang: opts.lang } });
330
292
  }
331
- /** An export file (JSON, SRT, XLSX, PDF) as a `Response`: `.text()` or
332
- * `.arrayBuffer()` it. `lang` makes it read in that language. */
333
293
  export(id, format, opts = {}) {
334
294
  return this.#t.raw('GET', this.#p(id, '/export'), { headers: { accept: '*/*' }, query: { ...listenerQuery(opts), format, lang: opts.lang } });
335
295
  }
336
- /** The export as a plain link, for a browser download with a browser
337
- * access token (it rides in the query). Never use it with an API key:
338
- * the API refuses a key in a URL. */
339
296
  async exportUrl(id, format, opts = {}) {
340
297
  const token = opts.shareCode || opts.listenerToken ? undefined : await this.#t.bearer();
341
298
  return this.#t.url(this.#p(id, '/export'), { format, lang: opts.lang, shareCode: opts.shareCode, listenerToken: opts.listenerToken, access_token: token });
342
299
  }
343
- /** The signed media URL the server redirects to. `kind`: `delivery`
344
- * (default), `master`, or `source` (a transcription's original upload). */
345
300
  async audioUrl(id, opts = {}) {
346
301
  const res = await this.#t.raw('GET', this.#p(id, '/audio'), {
347
302
  query: { ...listenerQuery(opts), kind: opts.kind, lang: opts.lang, format: opts.format },
@@ -351,44 +306,55 @@ export class Recordings {
351
306
  throw new VocalStackError('not_found', 'no signed URL in the redirect');
352
307
  return location;
353
308
  }
354
- /** A stretch of the recording's audio, `startS` to `endS` seconds (at most 120 s). */
309
+ audioSrc(id, opts = {}) {
310
+ return this.#t.url(this.#p(id, '/audio'), { kind: opts.kind ?? 'delivery', lang: opts.lang, format: opts.format, voices: opts.voices, voice: opts.voice, ...mediaQuery(opts) });
311
+ }
312
+ clipSrc(id, startS, endS, opts = {}) {
313
+ return this.#t.url(this.#p(id, '/audio/clip'), { startS: startS.toFixed(2), endS: endS.toFixed(2), clean: opts.clean ? 'true' : undefined, ...mediaQuery(opts) });
314
+ }
315
+ voiceClipSrc(id, sentence, lang, opts = {}) {
316
+ return this.#t.url(this.#p(id, `/voice/${seg(sentence)}`), {
317
+ lang,
318
+ voices: opts.voices,
319
+ voice: opts.voice,
320
+ format: opts.format,
321
+ part: opts.part ? opts.part : undefined,
322
+ v: opts.v,
323
+ ...mediaQuery(opts),
324
+ });
325
+ }
355
326
  clip(id, startS, endS, as) {
356
327
  return this.#t.raw('GET', this.#p(id, '/audio/clip'), { headers: { accept: '*/*' }, query: { ...listenerQuery(as), startS, endS } });
357
328
  }
358
- /** The synthesized voice tracks of a recording (per language). */
359
329
  voice(id, lang, as) {
360
330
  return this.#t.json('GET', this.#p(id, '/voice'), { query: { ...listenerQuery(as), lang } });
361
331
  }
362
- /** Synthesize the voice track in these languages. */
363
- renderVoice(id, langs, call = {}) {
364
- return this.#t.json('POST', this.#p(id, '/voice'), { body: { langs }, ...call });
332
+ renderVoice(id, req, call = {}) {
333
+ const r = Array.isArray(req) ? { langs: req } : req;
334
+ return this.#t.json('POST', this.#p(id, '/voice'), {
335
+ query: listenerQuery(r),
336
+ body: { langs: r.langs, voices: r.voices, voice: r.voice, voiceBySpeaker: r.voiceBySpeaker, near: r.near, consent: r.consent },
337
+ ...call,
338
+ });
365
339
  }
366
- /** One sentence spoken in `lang`. */
367
340
  voiceClip(id, sentence, lang, opts = {}) {
368
- return this.#t.raw('GET', this.#p(id, `/voice/${seg(sentence)}`), { headers: { accept: '*/*' }, query: { ...listenerQuery(opts), lang, format: opts.format } });
341
+ return this.#t.raw('GET', this.#p(id, `/voice/${seg(sentence)}`), { headers: { accept: '*/*' }, query: { ...listenerQuery(opts), lang, format: opts.format, voices: opts.voices } });
369
342
  }
370
- /** A short-lived token for the recording's media URLs. */
371
343
  mediaToken(id) {
372
344
  return this.#t.json('GET', this.#p(id, '/media-token'));
373
345
  }
374
- /** Name a speaker (the diarizer's index); `null` clears the name. */
375
346
  async nameSpeaker(id, speaker, name) {
376
347
  await this.#t.json('PUT', this.#p(id, `/speakers/${seg(speaker)}`), { body: { name } });
377
348
  }
378
- /** The durable event log with wall times (owner only): every event
379
- * after `after` (a `seq`; the whole log without it), read `limit` a page. */
380
349
  events(id, opts = {}) {
381
350
  return allPages(async (cursor) => {
382
351
  const r = await this.eventsPage(id, cursor ? { cursor, limit: opts.limit } : { after: opts.after, limit: opts.limit });
383
352
  return { items: r.events, nextCursor: r.nextCursor };
384
353
  });
385
354
  }
386
- /** One page of the event log (500 by default, 1000 at most): from `after`
387
- * a `seq`, or the previous page's `nextCursor` as `cursor`. */
388
355
  eventsPage(id, opts = {}) {
389
356
  return this.#t.json('GET', this.#p(id, '/events'), { query: { cursor: opts.cursor, after: opts.after, limit: opts.limit } });
390
357
  }
391
- /** Attach to the live fan-out; resolves once the backfill snapshot is in place. */
392
358
  async subscribe(id, opts = {}) {
393
359
  const token = opts.asOwner === false ? undefined : await this.#t.bearer();
394
360
  const url = this.#t.wsUrl(this.#p(id, '/subscribe'), {
@@ -407,10 +373,19 @@ export class Recordings {
407
373
  }));
408
374
  }
409
375
  }
376
+ function mediaQuery(a) {
377
+ return { media: a.media, access_token: a.media ? undefined : a.token, listenerToken: a.listenerToken, shareCode: a.shareCode };
378
+ }
379
+ export function playableFormat() {
380
+ try {
381
+ const doc = globalThis.document;
382
+ return doc?.createElement('audio').canPlayType?.('audio/x-caf; codecs=opus') === 'probably' ? 'caf' : 'ogg';
383
+ }
384
+ catch {
385
+ return 'ogg';
386
+ }
387
+ }
410
388
  const TERMINAL = new Set(['done', 'failed', 'stopped', 'cropping']);
411
- /** `vs.jobs`: the transcription job of a file or link recording
412
- * (`/v1/recordings/{id}/job`). States: queued, fetching, transcribing,
413
- * done, failed, and cropping (waiting for `start`). */
414
389
  export class Jobs {
415
390
  #t;
416
391
  constructor(t) {
@@ -419,22 +394,15 @@ export class Jobs {
419
394
  get(recordingId) {
420
395
  return this.#t.json('GET', `/v1/recordings/${seg(recordingId)}/job`);
421
396
  }
422
- /** Release a job waiting in `cropping`: the window to transcribe, in seconds. */
423
397
  start(recordingId, window = {}) {
424
398
  return this.#t.json('POST', `/v1/recordings/${seg(recordingId)}/job/start`, { body: window });
425
399
  }
426
- /** Re-queue a failed job from its stored source. */
427
400
  retry(recordingId) {
428
401
  return this.#t.json('POST', `/v1/recordings/${seg(recordingId)}/job/retry`);
429
402
  }
430
- /** The file the server fetched for a link job waiting in `cropping`. */
431
403
  preview(recordingId) {
432
404
  return this.#t.raw('GET', `/v1/recordings/${seg(recordingId)}/job/preview`, { headers: { accept: '*/*' } });
433
405
  }
434
- /** Poll until the job is `done`, `failed` or `cropping`, and return it.
435
- * It does not throw on `failed`: check `job.state` (and `job.error`).
436
- * `onProgress` sees every job read, the last one included: its `state`
437
- * and `progress` (`pct`, `processedS`, the fetch phase). */
438
406
  async wait(recordingId, opts = {}) {
439
407
  const interval = opts.intervalMs ?? 2000;
440
408
  const deadline = opts.timeoutMs ? Date.now() + opts.timeoutMs : Infinity;
@@ -451,29 +419,24 @@ export class Jobs {
451
419
  }
452
420
  }
453
421
  }
454
- /** `vs.corrections`: edits to a finished transcript, each revertible. */
455
422
  export class Corrections {
456
423
  #t;
457
424
  constructor(t) {
458
425
  this.#t = t;
459
426
  }
460
- /** Every correction, newest first (every page of `listPage`). */
461
427
  list(recordingId) {
462
428
  return allPages(async (cursor) => {
463
429
  const r = await this.listPage(recordingId, { cursor });
464
430
  return { items: r.corrections, nextCursor: r.nextCursor };
465
431
  });
466
432
  }
467
- /** One page of corrections, newest first: pass its `nextCursor` as `cursor` for the next. */
468
433
  listPage(recordingId, opts = {}) {
469
434
  return this.#t.json('GET', `/v1/recordings/${seg(recordingId)}/corrections`, { query: { cursor: opts.cursor, limit: opts.limit } });
470
435
  }
471
- /** Apply a staged batch atomically; every target must be settled. */
472
436
  async apply(recordingId, batch) {
473
437
  const r = await this.#t.json('POST', `/v1/recordings/${seg(recordingId)}/corrections`, { body: batch });
474
438
  return r.corrections ?? [];
475
439
  }
476
- /** Read lines again with the language forced; lands as `line` corrections. */
477
440
  async retranscribe(recordingId, req) {
478
441
  const r = await this.#t.json('POST', `/v1/recordings/${seg(recordingId)}/retranscribe`, { body: req });
479
442
  return r.corrections ?? [];
@@ -482,13 +445,14 @@ export class Corrections {
482
445
  return this.#t.json('POST', `/v1/corrections/${seg(correctionId)}/revert`);
483
446
  }
484
447
  }
485
- /** `vs.shareLinks`: named public links to a transcription or broadcast
486
- * (create and list them on their owner). */
487
448
  export class ShareLinks {
488
449
  #t;
489
450
  constructor(t) {
490
451
  this.#t = t;
491
452
  }
453
+ open(code) {
454
+ return this.#t.json('GET', `/v1/shares/${seg(code)}`, { anonymous: true });
455
+ }
492
456
  update(id, req) {
493
457
  return this.#t.json('PATCH', `/v1/share-links/${seg(id)}`, { body: req });
494
458
  }
@@ -496,22 +460,55 @@ export class ShareLinks {
496
460
  return this.#t.json('DELETE', `/v1/share-links/${seg(id)}`);
497
461
  }
498
462
  }
499
- /** `vs.voices`: the account's cloned voices, and the catalogue a language offers. */
500
- export class Voices {
463
+ export class Tags {
501
464
  #t;
502
465
  constructor(t) {
503
466
  this.#t = t;
504
467
  }
505
468
  async list() {
506
- const r = await this.#t.json('GET', '/v1/voices');
469
+ const r = await this.#t.json('GET', '/v1/tags');
470
+ return r.tags ?? [];
471
+ }
472
+ create(req) {
473
+ return this.#t.json('POST', '/v1/tags', { body: req });
474
+ }
475
+ update(id, req) {
476
+ return this.#t.json('PATCH', `/v1/tags/${seg(id)}`, { body: req });
477
+ }
478
+ async delete(id) {
479
+ await this.#t.json('DELETE', `/v1/tags/${seg(id)}`);
480
+ }
481
+ tagTranscriptions(id, req) {
482
+ return this.#t.json('POST', `/v1/tags/${seg(id)}/transcriptions`, { body: req });
483
+ }
484
+ }
485
+ export class Voices {
486
+ #t;
487
+ constructor(t) {
488
+ this.#t = t;
489
+ }
490
+ async list(opts = {}) {
491
+ const r = await this.#t.json('GET', '/v1/voices', { query: { lifecycle: opts.lifecycle } });
507
492
  return r.voices ?? [];
508
493
  }
509
494
  get(id) {
510
495
  return this.#t.json('GET', `/v1/voices/${seg(id)}`);
511
496
  }
512
- /** Build a voice from a sample (`sample`) or audio the server fetches
513
- * (`sampleUrl`). Calling it asserts you have the speaker's permission,
514
- * unless `draft` (a draft is named and consented to later with `update`). */
497
+ archive(id) {
498
+ return this.#t.json('POST', `/v1/voices/${seg(id)}/archive`);
499
+ }
500
+ restore(id) {
501
+ return this.#t.json('POST', `/v1/voices/${seg(id)}/restore`);
502
+ }
503
+ archiveMany(ids) {
504
+ return this.#t.json('POST', '/v1/voices/archive', { body: { ids } });
505
+ }
506
+ restoreMany(ids) {
507
+ return this.#t.json('POST', '/v1/voices/restore', { body: { ids } });
508
+ }
509
+ deleteMany(ids) {
510
+ return this.#t.json('POST', '/v1/voices/delete', { body: { ids } });
511
+ }
515
512
  async create(req, call = {}) {
516
513
  const { sample, fileName, ...meta } = req;
517
514
  const form = new FormData();
@@ -522,34 +519,27 @@ export class Voices {
522
519
  }
523
520
  return this.#t.json('POST', '/v1/voices', { body: form, ...call });
524
521
  }
525
- /** Build a voice from one speaker of a recording (asserts permission). */
526
522
  createFromSpeaker(req, call = {}) {
527
523
  return this.#t.json('POST', '/v1/voices/from-speaker', { body: { consent: true, ...req }, ...call });
528
524
  }
529
- /** Rename, recolour, switch off; `{ name, consent: true, draft: false }` finishes a draft. */
525
+ setConsent(id, consent, opts = {}) {
526
+ return this.#t.json('POST', `/v1/voices/${seg(id)}/consent`, { body: { consent, ...opts } });
527
+ }
530
528
  update(id, patch) {
531
529
  return this.#t.json('PATCH', `/v1/voices/${seg(id)}`, { body: patch });
532
530
  }
533
531
  async delete(id) {
534
532
  await this.#t.json('DELETE', `/v1/voices/${seg(id)}`);
535
533
  }
536
- /** The voices that can speak `lang`, best first (with a token, also the
537
- * account's ready clones). `recording` also offers the cloned voices that
538
- * recording uses, to whoever may read it: its owner, or a listener with
539
- * `listenerToken` or `shareCode`. */
540
534
  available(lang, opts = {}) {
541
535
  return this.#t.json('GET', '/v1/voices/available', {
542
536
  query: { ...listenerQuery(opts), lang, recording: opts.recording },
543
537
  anonymous: opts.asOwner === false,
544
538
  });
545
539
  }
546
- /** A catalogue voice speaking `lang` (Ogg/Opus; `caf` for Apple). 202 while it is prepared. */
547
540
  preview(voice, lang, opts = {}) {
548
541
  return this.#t.raw('GET', '/v1/voices/preview', { headers: { accept: '*/*' }, query: { voice, lang, format: opts.format } });
549
542
  }
550
- /** A cloned voice speaking `lang`; 409 while it is not ready. With
551
- * `recording`, as it speaks in that recording, for whoever may read it
552
- * (its owner, or a listener with `listenerToken` or `shareCode`). */
553
543
  clonePreview(id, opts = {}) {
554
544
  return this.#t.raw('GET', `/v1/voices/${seg(id)}/preview`, {
555
545
  headers: { accept: '*/*' },
@@ -557,31 +547,38 @@ export class Voices {
557
547
  anonymous: opts.asOwner === false,
558
548
  });
559
549
  }
560
- /** The audio a voice was made from. */
561
550
  sample(id) {
562
551
  return this.#t.raw('GET', `/v1/voices/${seg(id)}/sample`, { headers: { accept: '*/*' } });
563
552
  }
564
553
  }
565
- /** `vs.search`: full-text search over the caller's transcripts. */
566
554
  export class Search {
567
555
  #t;
568
556
  constructor(t) {
569
557
  this.#t = t;
570
558
  }
571
- /** `q` is 2 or more characters. */
572
559
  query(q, opts = {}) {
573
560
  const query = {
561
+ ...filterQuery({ site: opts.site, language: opts.language, kind: opts.kind, origin: opts.origin, tag: opts.tag, createdAfter: opts.createdAfter, createdBefore: opts.createdBefore, shared: opts.shared }),
574
562
  q,
575
563
  broadcasts: opts.broadcasts?.join(','),
576
564
  listenerTokens: opts.listenerTokens?.join(','),
577
565
  recording: opts.recording,
566
+ transcription: opts.transcription,
578
567
  lang: opts.lang,
579
568
  limit: opts.limit,
580
569
  };
581
570
  return this.#t.json('GET', '/v1/search', { query });
582
571
  }
572
+ vector(q, opts = {}) {
573
+ const query = {
574
+ ...filterQuery({ site: opts.site, language: opts.language, kind: opts.kind, origin: opts.origin, tag: opts.tag, createdAfter: opts.createdAfter, createdBefore: opts.createdBefore, shared: opts.shared }),
575
+ q,
576
+ recording: opts.recording,
577
+ transcription: opts.transcription,
578
+ };
579
+ return this.#t.json('GET', '/v1/search/vector', { query });
580
+ }
583
581
  }
584
- /** `vs.me`: who the credential is. */
585
582
  export class MeApi {
586
583
  #t;
587
584
  constructor(t) {
@@ -591,18 +588,11 @@ export class MeApi {
591
588
  return this.#t.json('GET', '/v1/me');
592
589
  }
593
590
  }
594
- /** `vs.connect`: one-click Connect, on the app's server. */
595
591
  export class Connect {
596
592
  #t;
597
593
  constructor(t) {
598
594
  this.#t = t;
599
595
  }
600
- /** Redeem the `code` the dashboard sent to the app's `redirect_uri` for
601
- * an API key (OAuth 2.0 authorization code + PKCE). No credential is
602
- * sent, even when the client has one, and the call is never retried:
603
- * the code works once. A refusal is a `VocalStackError` whose `code` is
604
- * OAuth's `error` (`invalid_grant`: unknown, spent or expired code, or a
605
- * verifier or `redirect_uri` that does not match). */
606
596
  token(input, opts = {}) {
607
597
  const body = new URLSearchParams({
608
598
  grant_type: 'authorization_code',
@@ -613,165 +603,125 @@ export class Connect {
613
603
  return this.#t.json('POST', '/v1/connect/token', { body, anonymous: true, noRetry: true, oauthErrors: true, signal: opts.signal });
614
604
  }
615
605
  }
616
- /** `vs.billing`: the account's plan, credit balance and limits, the price
617
- * list, the ledger, the per-recording charges, the usage report, the
618
- * payments history, payment methods, invoice details and promo codes. */
619
606
  export class Billing {
620
607
  #t;
621
608
  constructor(t) {
622
609
  this.#t = t;
623
610
  }
624
- /** The account: plan, rates, balance, limits, subscription, top-up URL. */
625
611
  get() {
626
612
  return this.#t.json('GET', '/v1/billing');
627
613
  }
628
- /** The visible plans with their current rates and limits (no credential needed). */
629
614
  plans() {
630
615
  return this.#t.json('GET', '/v1/billing/plans');
631
616
  }
632
- /** The ledger, newest first. */
633
617
  ledger(opts = {}) {
634
618
  return this.#t.json('GET', '/v1/billing/ledger', { query: { cursor: opts.cursor, limit: opts.limit } });
635
619
  }
636
- /** The per-recording charges, newest first. */
637
620
  charges(opts = {}) {
638
621
  return this.#t.json('GET', '/v1/billing/charges', { query: { cursor: opts.cursor, limit: opts.limit } });
639
622
  }
640
- /** Open a checkout at the payment provider (a top-up or a subscription); send the customer to `url`.
641
- * A signed-in person only, never an API key. */
642
623
  createCheckout(req, opts = {}) {
643
624
  return this.#t.json('POST', '/v1/billing/checkouts', { body: req, ...opts });
644
625
  }
645
- /** A checkout's status (by its id or the provider's). */
646
626
  getCheckout(id) {
647
627
  return this.#t.json('GET', `/v1/billing/checkouts/${seg(id)}`);
648
628
  }
649
- /** Cancel the subscription at the end of its period. */
650
629
  cancelSubscription() {
651
630
  return this.#t.json('POST', '/v1/billing/subscription/cancel');
652
631
  }
653
- /** Take back a scheduled cancellation. */
654
632
  uncancelSubscription() {
655
633
  return this.#t.json('POST', '/v1/billing/subscription/uncancel');
656
634
  }
657
- /** Turn auto top-up on (threshold and amount from `plans().autoTopUp`) or off. */
658
635
  setAutoTopUp(req) {
659
636
  return this.#t.json('PUT', '/v1/billing/auto-top-up', { body: req });
660
637
  }
661
- /** A link to the provider's customer portal (payment methods, invoices, receipts). */
662
638
  portal() {
663
639
  return this.#t.json('GET', '/v1/billing/portal');
664
640
  }
665
- /** Charge the configured auto top-up amount now (under the day's cap). */
666
641
  retryAutoTopUp() {
667
642
  return this.#t.json('POST', '/v1/billing/auto-top-up/retry');
668
643
  }
669
- /** Turn the low-balance email on or off and set its line. */
670
644
  setLowBalanceAlert(req) {
671
645
  return this.#t.json('PUT', '/v1/billing/low-balance-alert', { body: req });
672
646
  }
673
- /** Renew `month`ly or `year`ly from the next period, at `subscription.switchPrices` (needs `acceptTerms` and `noNewWithdrawalPeriodShown`). */
674
647
  setInterval(req) {
675
648
  return this.#t.json('POST', '/v1/billing/subscription/interval', { body: req });
676
649
  }
677
- /** Keep the subscription by switching to yearly when monthly billing is withdrawn (`subscription.yearlyOptIn`). */
678
650
  optInYearly(req) {
679
651
  return this.#t.json('POST', '/v1/billing/subscription/yearly-opt-in', { body: req });
680
652
  }
681
- /** Take back a scheduled interval switch. */
682
653
  unsetInterval() {
683
654
  return this.#t.json('DELETE', '/v1/billing/subscription/interval');
684
655
  }
685
- /** What the account spent per day, source (API key or Dashboard) and kind. */
686
656
  usage(opts = {}) {
687
657
  return this.#t.json('GET', '/v1/billing/usage', { query: { startDate: opts.startDate, endDate: opts.endDate, tz: opts.tz } });
688
658
  }
689
- /** The usage report's charges one by one, newest first, with how many match and their total. */
690
659
  usageCharges(opts = {}) {
691
660
  const query = { startDate: opts.startDate, endDate: opts.endDate, tz: opts.tz, kind: opts.kind, source: opts.source, cursor: opts.cursor, limit: opts.limit };
692
661
  return this.#t.json('GET', '/v1/billing/usage/charges', { query });
693
662
  }
694
- /** The payments history, newest first. */
695
663
  payments(opts = {}) {
696
664
  return this.#t.json('GET', '/v1/billing/payments', { query: { cursor: opts.cursor, limit: opts.limit } });
697
665
  }
698
- /** A payment's invoice or receipt: `{url}`, or `{pending: true}` while the provider makes it. */
699
666
  paymentDocument(id) {
700
667
  return this.#t.json('GET', `/v1/billing/payments/${seg(id)}/document`);
701
668
  }
702
- /** The saved payment methods. */
703
669
  paymentMethods() {
704
670
  return this.#t.json('GET', '/v1/billing/payment-methods');
705
671
  }
706
- /** Remove a saved payment method. */
707
672
  async deletePaymentMethod(id) {
708
673
  await this.#t.json('DELETE', `/v1/billing/payment-methods/${seg(id)}`);
709
674
  }
710
- /** A link to the provider's portal to add a card or choose the default. */
711
675
  managePaymentMethods() {
712
676
  return this.#t.json('POST', '/v1/billing/payment-methods/manage');
713
677
  }
714
- /** The invoice details. */
715
678
  details() {
716
679
  return this.#t.json('GET', '/v1/billing/details');
717
680
  }
718
- /** Save the invoice details. */
719
681
  setDetails(req) {
720
682
  return this.#t.json('PUT', '/v1/billing/details', { body: req });
721
683
  }
722
- /** What a promo code gives for a top-up or a subscription. */
723
684
  checkPromoCode(req) {
724
685
  return this.#t.json('POST', '/v1/billing/promo-codes/check', { body: req });
725
686
  }
726
687
  }
727
- /** `vs.languages`. */
728
688
  export class Languages {
729
689
  #t;
730
690
  constructor(t) {
731
691
  this.#t = t;
732
692
  }
733
- /** Spoken and translation languages. */
734
693
  list() {
735
694
  return this.#t.json('GET', '/v1/languages');
736
695
  }
737
696
  }
738
- /** `vs.rtmp`. */
739
697
  export class Rtmp {
740
698
  #t;
741
699
  constructor(t) {
742
700
  this.#t = t;
743
701
  }
744
- /** The account's RTMP ingest: server URL (RTMPS where available), the stream key (in full only in the answer that made it, `maskedStreamKey` after), and whether an encoder is sending. */
745
702
  status() {
746
703
  return this.#t.json('GET', '/v1/rtmp');
747
704
  }
748
- /** A stream key of the app's own, beside the account's: `streamKey` is in full in this answer only. It works while the API key that made it does. */
749
705
  createKey(req) {
750
706
  return this.#t.json('POST', '/v1/rtmp/keys', { body: req });
751
707
  }
752
- /** The account's app stream keys, oldest first: masked, each with whether an encoder is sending on it (`live`, `audio`). */
753
708
  async listKeys() {
754
709
  const r = await this.#t.json('GET', '/v1/rtmp/keys');
755
710
  return r.keys ?? [];
756
711
  }
757
- /** Delete an app stream key: an encoder publishing on it is dropped within seconds. */
758
712
  async deleteKey(id) {
759
713
  await this.#t.json('DELETE', `/v1/rtmp/keys/${seg(id)}`);
760
714
  }
761
715
  }
762
- /** `vs.events`: the account socket. */
763
716
  export class Events {
764
717
  #t;
765
718
  constructor(t) {
766
719
  this.#t = t;
767
720
  }
768
- /** `events.ready`, then `lists.changed` whenever one of the owner's
769
- * lists of broadcasts or transcriptions is out of date: re-read them. */
770
721
  async account() {
771
722
  return openWebSocket(this.#t.wsUrl('/v1/events'), await this.#t.bearer());
772
723
  }
773
724
  }
774
- /** `vs.meta`. */
775
725
  export class Meta {
776
726
  #t;
777
727
  constructor(t) {
@@ -780,8 +730,10 @@ export class Meta {
780
730
  health() {
781
731
  return this.#t.json('GET', '/v1/health');
782
732
  }
783
- /** The OpenAPI document this SDK is generated from. */
784
733
  openapi() {
785
734
  return this.#t.json('GET', '/v1/openapi.json');
786
735
  }
736
+ clientPolicy(client) {
737
+ return this.#t.json('GET', `/v1/clients/${encodeURIComponent(client)}`);
738
+ }
787
739
  }