@vocalstack/js-sdk 2.0.0-alpha.1 → 2.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/SKILL.md +10 -0
- package/dist/index.d.ts +2 -2
- package/dist/resources.d.ts +27 -5
- package/dist/resources.js +31 -4
- package/package.json +3 -7
- package/skills/vocalstack/SKILL.md +10 -0
package/README.md
CHANGED
|
@@ -31,8 +31,9 @@ npm install @vocalstack/js-sdk
|
|
|
31
31
|
|
|
32
32
|
- [Quickstart](https://github.com/vocalstack/js-sdk/blob/main/docs/quickstart.md): An API key, the SDK, and your first transcript in a dozen lines.
|
|
33
33
|
- [Transcribe a file](https://github.com/vocalstack/js-sdk/blob/main/docs/transcribe-a-file.md): Upload a file, wait for its job, read the transcript and export it. Asynchronous; nothing to keep open.
|
|
34
|
-
- [Transcribe a link](https://github.com/vocalstack/js-sdk/blob/main/docs/transcribe-a-link.md): The server fetches a public URL or a
|
|
34
|
+
- [Transcribe a link](https://github.com/vocalstack/js-sdk/blob/main/docs/transcribe-a-link.md): The server fetches a public URL, a YouTube video or a recording on a video or audio site, then transcribes it like a file. Asynchronous, with a fetch phase first.
|
|
35
35
|
- [Transcribe a batch of files](https://github.com/vocalstack/js-sdk/blob/main/docs/transcribe-a-batch.md): Send many files or links as one batch, start it once, and read every transcript when it is done. Cheaper than one transcription per file.
|
|
36
|
+
- [Search your transcriptions](https://github.com/vocalstack/js-sdk/blob/main/docs/search-transcriptions.md): Find transcriptions by words in their name or link, site, language, kind, tag and date, keep the shared ones, and play each with its share code.
|
|
36
37
|
- [Stream a microphone](https://github.com/vocalstack/js-sdk/blob/main/docs/stream-a-microphone.md): Live audio in, transcript events out, over one WebSocket. Drafts replace the live tail; commits append.
|
|
37
38
|
- [Stream a file at full speed](https://github.com/vocalstack/js-sdk/blob/main/docs/stream-a-file.md): Send a file's bytes through a realtime session and read the transcript as fast as the server makes it. Nothing is stored.
|
|
38
39
|
- [Go live](https://github.com/vocalstack/js-sdk/blob/main/docs/go-live.md): Publish live audio to a stored transcription, and put it on a broadcast link listeners open. Control is HTTP; audio is one WebSocket.
|
package/SKILL.md
CHANGED
|
@@ -179,6 +179,16 @@ for (const h of hits) {
|
|
|
179
179
|
}
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
+
By meaning, in any language, even with none of the words (vector search). Each
|
|
183
|
+
hit's `passage` is its whole paragraph: give the passages to your own model as
|
|
184
|
+
context to answer questions about what was said (RAG). Pay as you go counts 60
|
|
185
|
+
an hour (`allowance`); Premium and Enterprise have no limit.
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
const { hits: found } = await vs.search.vector('how much does housing cost');
|
|
189
|
+
for (const h of found) console.log(h.similarity.toFixed(2), h.passage.mediaStart, h.passage.text);
|
|
190
|
+
```
|
|
191
|
+
|
|
182
192
|
## 6. A batch of files
|
|
183
193
|
|
|
184
194
|
Many files at once cost less as a batch: one create, one minimum charge for
|
package/dist/index.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export { Subscription } from './subscription.js';
|
|
|
14
14
|
export type { SubscribeOptions, SubscriptionEvent } from './subscription.js';
|
|
15
15
|
export type { WsLike } from './ws.js';
|
|
16
16
|
export { Batches, Broadcasts, Connect, Corrections, Events, Jobs, Languages, MeApi, Meta, Recordings, Rtmp, Search, ShareLinks, Tags, Transcriptions, Voices, playableFormat, } from './resources.js';
|
|
17
|
-
export type { AudioKind, MediaAuth, MediaFormat, RenderVoice } from './resources.js';
|
|
18
|
-
export type { AddBatchFileInput, ConnectToken, ConnectTokenInput, CreateTranscriptionInput, LifecycleFilter, Me, PageOptions, RecordingPageOptions, SearchOptions, Upload, UsageChargesOptions, UsageOptions } from './resources.js';
|
|
17
|
+
export type { AudioKind, MediaAuth, MediaFormat, RenderVoice, VoicesChoice } from './resources.js';
|
|
18
|
+
export type { AddBatchFileInput, ConnectToken, ConnectTokenInput, CreateTranscriptionInput, LifecycleFilter, Me, PageOptions, RecordingPageOptions, SearchOptions, TranscriptionFilters, Upload, UsageChargesOptions, UsageOptions } from './resources.js';
|
|
19
19
|
export type * from '@vocalstack/contract';
|
|
20
20
|
export type VocalStackOptions = ClientOptions;
|
|
21
21
|
export declare class VocalStack {
|
package/dist/resources.d.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import type { AppStreamKey, AudioSourceCheck, Batch, BatchPage, AutoTopUpSettings, AutoTopUpView, BillingDetails, BillingView, CheckPromoCode, CheckoutView, CreateCheckout, LowBalanceAlertSettings, LowBalanceAlertView, PaymentDocument, PaymentMethodList, PaymentPage, PortalLink, PromoCodeView, SetInterval, SubscriptionView, YearlyOptIn, UsageChargePage, UsageReport, Broadcast, Correction, CorrectionBatch, CorrectionPage, CreateAppStreamKey, CreateAudioSource, CreateBatch, CreateLiveTranscription, CreateBroadcast, CreateShareLink, CreateTag, CreateTranscription, CreateVoiceFromSpeaker, CreatedAppStreamKey, ChargePage, ExportFormat, ClientApp, ClientPolicy, Health, LanguageLists, LedgerPage, LinkProbe, PatchVoice, PlanList, PublishGrant, Recording, RecordingEvent, RecordingEvents, RecordingPage, RetranscribeLine, RtmpStatus, SearchResults, SettingsPatch, ShareLink, SharedTranscript, StartRecording, StartJob, SummaryStatus, Tag, TagTranscriptions, Transcription, TranscriptionJob, TranscriptionPage, TranscriptSnapshot, UpdateBroadcast, UpdateTag, UpdateTranscription, UpdateShareLink, UploadRecording, Voice, VoiceCatalogue, VoiceTrack, ManyResult } from '@vocalstack/contract';
|
|
1
|
+
import type { AppStreamKey, AudioSourceCheck, Batch, BatchPage, AutoTopUpSettings, AutoTopUpView, BillingDetails, BillingView, CheckPromoCode, CheckoutView, CreateCheckout, LowBalanceAlertSettings, LowBalanceAlertView, PaymentDocument, PaymentMethodList, PaymentPage, PortalLink, PromoCodeView, SetInterval, SubscriptionView, YearlyOptIn, UsageChargePage, UsageReport, Broadcast, Correction, CorrectionBatch, CorrectionPage, CreateAppStreamKey, CreateAudioSource, CreateBatch, CreateLiveTranscription, CreateBroadcast, CreateShareLink, CreateTag, CreateTranscription, CreateVoiceFromSpeaker, CreatedAppStreamKey, ChargePage, ExportFormat, ClientApp, ClientPolicy, Health, LanguageLists, LedgerPage, LinkProbe, PatchVoice, PlanList, PublishGrant, Recording, RecordingEvent, RecordingEvents, RecordingPage, RetranscribeLine, RtmpStatus, SearchResults, VectorResults, SettingsPatch, ShareLink, SharedTranscript, StartRecording, StartJob, SummaryStatus, Tag, TagTranscriptions, Transcription, TranscriptionJob, TranscriptionPage, TranscriptSnapshot, UpdateBroadcast, UpdateTag, UpdateTranscription, UpdateShareLink, UploadRecording, Voice, VoiceCatalogue, VoiceTrack, ManyResult } from '@vocalstack/contract';
|
|
2
2
|
import { Transport, type CallOptions, type ReadAs } from './http.js';
|
|
3
3
|
import { RealtimeSession, type ConnectOptions } from './realtime.js';
|
|
4
4
|
import { Subscription, type SubscribeOptions } from './subscription.js';
|
|
5
5
|
import { type WsLike } from './ws.js';
|
|
6
6
|
export type Upload = Blob | Uint8Array | ArrayBuffer | string;
|
|
7
7
|
export type LifecycleFilter = 'active' | 'archived' | 'all';
|
|
8
|
+
export interface TranscriptionFilters {
|
|
9
|
+
lifecycle?: LifecycleFilter;
|
|
10
|
+
q?: string;
|
|
11
|
+
site?: string;
|
|
12
|
+
language?: string;
|
|
13
|
+
kind?: 'file' | 'link' | 'live';
|
|
14
|
+
origin?: Transcription['origin'];
|
|
15
|
+
tag?: string;
|
|
16
|
+
broadcast?: string;
|
|
17
|
+
createdAfter?: string | Date;
|
|
18
|
+
createdBefore?: string | Date;
|
|
19
|
+
shared?: boolean;
|
|
20
|
+
}
|
|
8
21
|
export interface RecordingPageOptions extends PageOptions {
|
|
9
22
|
lifecycle?: 'active' | 'archived';
|
|
10
23
|
}
|
|
@@ -49,13 +62,13 @@ export type CreateTranscriptionInput = CreateTranscription & {
|
|
|
49
62
|
export declare class Transcriptions extends ParentResource<Transcription, UpdateTranscription> {
|
|
50
63
|
protected readonly base: "/v1/transcriptions";
|
|
51
64
|
protected readonly key: "transcriptions";
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}): Promise<TranscriptionPage>;
|
|
65
|
+
list(opts?: LifecycleFilter | TranscriptionFilters): Promise<Transcription[]>;
|
|
66
|
+
listPage(opts?: PageOptions & TranscriptionFilters): Promise<TranscriptionPage>;
|
|
55
67
|
uploadRecording(id: string, req?: UploadRecording, call?: CallOptions): Promise<PublishGrant>;
|
|
56
68
|
create(req: CreateTranscriptionInput, call?: CallOptions): Promise<Transcription>;
|
|
57
69
|
createFromUrl(url: string, opts?: Omit<CreateTranscription, 'url'>, call?: CallOptions): Promise<Transcription>;
|
|
58
70
|
createLive(req: CreateLiveTranscription, call?: CallOptions): Promise<Transcription>;
|
|
71
|
+
lookup(url: string): Promise<Transcription | null>;
|
|
59
72
|
probe(url: string): Promise<LinkProbe>;
|
|
60
73
|
attachBroadcast(id: string, broadcastId: string): Promise<Transcription>;
|
|
61
74
|
detachBroadcast(id: string, broadcastId: string): Promise<Transcription>;
|
|
@@ -131,6 +144,7 @@ export declare class Recordings {
|
|
|
131
144
|
audioSrc(id: string, opts?: {
|
|
132
145
|
kind?: AudioKind;
|
|
133
146
|
lang?: string;
|
|
147
|
+
voices?: VoicesChoice;
|
|
134
148
|
voice?: string;
|
|
135
149
|
format?: MediaFormat;
|
|
136
150
|
} & MediaAuth): string;
|
|
@@ -138,6 +152,7 @@ export declare class Recordings {
|
|
|
138
152
|
clean?: boolean;
|
|
139
153
|
} & MediaAuth): string;
|
|
140
154
|
voiceClipSrc(id: string, sentence: number, lang: string, opts?: {
|
|
155
|
+
voices?: VoicesChoice;
|
|
141
156
|
voice?: string;
|
|
142
157
|
format?: 'ogg' | 'caf';
|
|
143
158
|
part?: number;
|
|
@@ -149,6 +164,7 @@ export declare class Recordings {
|
|
|
149
164
|
renderVoice(id: string, req: RenderVoice & ReadAs, call?: CallOptions): Promise<VoiceTrack[]>;
|
|
150
165
|
voiceClip(id: string, sentence: number, lang: string, opts?: {
|
|
151
166
|
format?: 'ogg' | 'caf';
|
|
167
|
+
voices?: VoicesChoice;
|
|
152
168
|
} & ReadAs): Promise<Response>;
|
|
153
169
|
mediaToken(id: string): Promise<{
|
|
154
170
|
token: string;
|
|
@@ -168,11 +184,14 @@ export declare class Recordings {
|
|
|
168
184
|
}
|
|
169
185
|
export type AudioKind = 'delivery' | 'voice' | 'background' | 'speech' | 'master' | 'source' | 'video';
|
|
170
186
|
export type MediaFormat = 'ogg' | 'caf' | 'mp3';
|
|
187
|
+
export type VoicesChoice = 'cloned' | 'synthetic';
|
|
171
188
|
export interface RenderVoice {
|
|
172
189
|
langs: string[];
|
|
190
|
+
voices?: VoicesChoice;
|
|
173
191
|
voice?: string;
|
|
174
192
|
voiceBySpeaker?: Record<string, string>;
|
|
175
193
|
near?: number;
|
|
194
|
+
consent?: boolean;
|
|
176
195
|
}
|
|
177
196
|
export interface MediaAuth extends ReadAs {
|
|
178
197
|
token?: string;
|
|
@@ -260,10 +279,11 @@ export declare class Voices {
|
|
|
260
279
|
} & ReadAs): Promise<Response>;
|
|
261
280
|
sample(id: string): Promise<Response>;
|
|
262
281
|
}
|
|
263
|
-
export interface SearchOptions {
|
|
282
|
+
export interface SearchOptions extends Omit<TranscriptionFilters, 'lifecycle' | 'q' | 'broadcast'> {
|
|
264
283
|
broadcasts?: string[];
|
|
265
284
|
listenerTokens?: string[];
|
|
266
285
|
recording?: string;
|
|
286
|
+
transcription?: string;
|
|
267
287
|
lang?: string;
|
|
268
288
|
limit?: number;
|
|
269
289
|
}
|
|
@@ -271,7 +291,9 @@ export declare class Search {
|
|
|
271
291
|
#private;
|
|
272
292
|
constructor(t: Transport);
|
|
273
293
|
query(q: string, opts?: SearchOptions): Promise<SearchResults>;
|
|
294
|
+
vector(q: string, opts?: VectorSearchOptions): Promise<VectorResults>;
|
|
274
295
|
}
|
|
296
|
+
export type VectorSearchOptions = Omit<SearchOptions, 'broadcasts' | 'listenerTokens' | 'lang' | 'limit'>;
|
|
275
297
|
export interface Me {
|
|
276
298
|
userId?: string | null;
|
|
277
299
|
apiKeyId?: string | null;
|
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';
|
|
@@ -106,8 +110,15 @@ export class Broadcasts extends ParentResource {
|
|
|
106
110
|
export class Transcriptions extends ParentResource {
|
|
107
111
|
base = '/v1/transcriptions';
|
|
108
112
|
key = 'transcriptions';
|
|
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
|
+
}
|
|
109
120
|
listPage(opts = {}) {
|
|
110
|
-
return this.t.json('GET', this.base, { query: {
|
|
121
|
+
return this.t.json('GET', this.base, { query: { ...filterQuery(opts), cursor: opts.cursor, limit: opts.limit } });
|
|
111
122
|
}
|
|
112
123
|
uploadRecording(id, req = {}, call = {}) {
|
|
113
124
|
return this.t.json('POST', this.p(id, '/recordings/upload'), { body: req, ...call });
|
|
@@ -128,6 +139,10 @@ export class Transcriptions extends ParentResource {
|
|
|
128
139
|
createLive(req, call = {}) {
|
|
129
140
|
return this.t.json('POST', `${this.base}/live`, { body: req, ...call });
|
|
130
141
|
}
|
|
142
|
+
async lookup(url) {
|
|
143
|
+
const r = await this.t.json('GET', `${this.base}/lookup`, { query: { url } });
|
|
144
|
+
return r.transcription ?? null;
|
|
145
|
+
}
|
|
131
146
|
probe(url) {
|
|
132
147
|
return this.t.json('GET', `${this.base}/probe`, { query: { url } });
|
|
133
148
|
}
|
|
@@ -292,7 +307,7 @@ export class Recordings {
|
|
|
292
307
|
return location;
|
|
293
308
|
}
|
|
294
309
|
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) });
|
|
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) });
|
|
296
311
|
}
|
|
297
312
|
clipSrc(id, startS, endS, opts = {}) {
|
|
298
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) });
|
|
@@ -300,6 +315,7 @@ export class Recordings {
|
|
|
300
315
|
voiceClipSrc(id, sentence, lang, opts = {}) {
|
|
301
316
|
return this.#t.url(this.#p(id, `/voice/${seg(sentence)}`), {
|
|
302
317
|
lang,
|
|
318
|
+
voices: opts.voices,
|
|
303
319
|
voice: opts.voice,
|
|
304
320
|
format: opts.format,
|
|
305
321
|
part: opts.part ? opts.part : undefined,
|
|
@@ -317,12 +333,12 @@ export class Recordings {
|
|
|
317
333
|
const r = Array.isArray(req) ? { langs: req } : req;
|
|
318
334
|
return this.#t.json('POST', this.#p(id, '/voice'), {
|
|
319
335
|
query: listenerQuery(r),
|
|
320
|
-
body: { langs: r.langs, voice: r.voice, voiceBySpeaker: r.voiceBySpeaker, near: r.near },
|
|
336
|
+
body: { langs: r.langs, voices: r.voices, voice: r.voice, voiceBySpeaker: r.voiceBySpeaker, near: r.near, consent: r.consent },
|
|
321
337
|
...call,
|
|
322
338
|
});
|
|
323
339
|
}
|
|
324
340
|
voiceClip(id, sentence, lang, opts = {}) {
|
|
325
|
-
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 } });
|
|
326
342
|
}
|
|
327
343
|
mediaToken(id) {
|
|
328
344
|
return this.#t.json('GET', this.#p(id, '/media-token'));
|
|
@@ -542,15 +558,26 @@ export class Search {
|
|
|
542
558
|
}
|
|
543
559
|
query(q, opts = {}) {
|
|
544
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 }),
|
|
545
562
|
q,
|
|
546
563
|
broadcasts: opts.broadcasts?.join(','),
|
|
547
564
|
listenerTokens: opts.listenerTokens?.join(','),
|
|
548
565
|
recording: opts.recording,
|
|
566
|
+
transcription: opts.transcription,
|
|
549
567
|
lang: opts.lang,
|
|
550
568
|
limit: opts.limit,
|
|
551
569
|
};
|
|
552
570
|
return this.#t.json('GET', '/v1/search', { query });
|
|
553
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
|
+
}
|
|
554
581
|
}
|
|
555
582
|
export class MeApi {
|
|
556
583
|
#t;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vocalstack/js-sdk",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.2",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,20 +25,16 @@
|
|
|
25
25
|
"skills"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"build": "
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
29
|
"test": "node --test test/*.test.mjs"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@vocalstack/contract": "1.0.0-alpha.
|
|
32
|
+
"@vocalstack/contract": "1.0.0-alpha.2",
|
|
33
33
|
"ws": "^8.18.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^22.0.0",
|
|
37
37
|
"@types/ws": "^8.5.0",
|
|
38
38
|
"typescript": "^5.6.0"
|
|
39
|
-
},
|
|
40
|
-
"repository": {
|
|
41
|
-
"type": "git",
|
|
42
|
-
"url": "git+https://github.com/vocalstack/js-sdk.git"
|
|
43
39
|
}
|
|
44
40
|
}
|
|
@@ -179,6 +179,16 @@ for (const h of hits) {
|
|
|
179
179
|
}
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
+
By meaning, in any language, even with none of the words (vector search). Each
|
|
183
|
+
hit's `passage` is its whole paragraph: give the passages to your own model as
|
|
184
|
+
context to answer questions about what was said (RAG). Pay as you go counts 60
|
|
185
|
+
an hour (`allowance`); Premium and Enterprise have no limit.
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
const { hits: found } = await vs.search.vector('how much does housing cost');
|
|
189
|
+
for (const h of found) console.log(h.similarity.toFixed(2), h.passage.mediaStart, h.passage.text);
|
|
190
|
+
```
|
|
191
|
+
|
|
182
192
|
## 6. A batch of files
|
|
183
193
|
|
|
184
194
|
Many files at once cost less as a batch: one create, one minimum charge for
|