@vocalstack/js-sdk 2.0.0-alpha.0 → 2.0.0-alpha.1

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