ngx-transformers 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -2
- package/fesm2022/ngx-transformers-worker.mjs +307 -0
- package/fesm2022/ngx-transformers-worker.mjs.map +1 -0
- package/fesm2022/ngx-transformers.mjs +317 -37
- package/fesm2022/ngx-transformers.mjs.map +1 -1
- package/package.json +11 -3
- package/types/ngx-transformers-worker.d.ts +165 -0
- package/types/ngx-transformers.d.ts +174 -14
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders, signal, computed,
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, inject, DestroyRef, signal, computed, Injector, resource, untracked, effect, input, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { takeOnToken, createWorkerPipelineFactory } from 'ngx-transformers/worker';
|
|
3
4
|
|
|
4
5
|
const importTransformers = () => import('@huggingface/transformers');
|
|
6
|
+
/**
|
|
7
|
+
* Wraps a pipeline so the `onToken` run option (TextGenerator streaming)
|
|
8
|
+
* becomes a Transformers.js TextStreamer on the pipeline's tokenizer.
|
|
9
|
+
*/
|
|
10
|
+
function withTokenStreaming(pipe, module) {
|
|
11
|
+
const streaming = (async (input, ...args) => {
|
|
12
|
+
const { args: rest, onToken } = takeOnToken(args);
|
|
13
|
+
if (onToken && module.TextStreamer) {
|
|
14
|
+
const options = { ...rest[rest.length - 1] };
|
|
15
|
+
options['streamer'] = new module.TextStreamer(pipe.tokenizer, { skip_prompt: true, skip_special_tokens: true, callback_function: onToken });
|
|
16
|
+
rest[rest.length - 1] = options;
|
|
17
|
+
}
|
|
18
|
+
return pipe(input, ...rest);
|
|
19
|
+
});
|
|
20
|
+
streaming.dispose = () => pipe.dispose?.() ?? Promise.resolve();
|
|
21
|
+
return streaming;
|
|
22
|
+
}
|
|
5
23
|
/**
|
|
6
24
|
* The factory behind PIPELINE_FACTORY. It imports @huggingface/transformers
|
|
7
25
|
* lazily, on the first pipeline, so the library adds nothing to the initial
|
|
@@ -20,8 +38,9 @@ const importTransformers = () => import('@huggingface/transformers');
|
|
|
20
38
|
*/
|
|
21
39
|
function createDefaultPipelineFactory(load = importTransformers) {
|
|
22
40
|
return async (task, model, options) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
41
|
+
const module = await load();
|
|
42
|
+
const pipe = (await module.pipeline(task, model, options));
|
|
43
|
+
return withTokenStreaming(pipe, module);
|
|
25
44
|
};
|
|
26
45
|
}
|
|
27
46
|
const PIPELINE_FACTORY = new InjectionToken('ngx-transformers.pipeline-factory', {
|
|
@@ -44,6 +63,40 @@ const NGX_TRANSFORMERS_CONFIG = new InjectionToken('ngx-transformers.config', {
|
|
|
44
63
|
function provideTransformers(config) {
|
|
45
64
|
return makeEnvironmentProviders([{ provide: NGX_TRANSFORMERS_CONFIG, useValue: config }]);
|
|
46
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Runs every pipeline in a Web Worker so inference never blocks the UI.
|
|
68
|
+
* The worker file imports the worker entry point:
|
|
69
|
+
*
|
|
70
|
+
* ```ts
|
|
71
|
+
* // transformers.worker.ts
|
|
72
|
+
* /// <reference lib="webworker" />
|
|
73
|
+
* import { runTransformersWorker } from 'ngx-transformers/worker';
|
|
74
|
+
* runTransformersWorker();
|
|
75
|
+
*
|
|
76
|
+
* // app.config.ts
|
|
77
|
+
* provideTransformersWorker(
|
|
78
|
+
* () => new Worker(new URL('./transformers.worker', import.meta.url), { type: 'module' }),
|
|
79
|
+
* )
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* The worker is created on the first pipeline and terminated with the
|
|
83
|
+
* injector. Handles, signals and the wrappers work unchanged; results must
|
|
84
|
+
* survive structured cloning (plain objects, arrays, typed arrays and
|
|
85
|
+
* tensors do). A worker that fails to start rejects the calls waiting on
|
|
86
|
+
* it, like a failed load in-thread.
|
|
87
|
+
*/
|
|
88
|
+
function provideTransformersWorker(createWorker) {
|
|
89
|
+
return makeEnvironmentProviders([
|
|
90
|
+
{
|
|
91
|
+
provide: PIPELINE_FACTORY,
|
|
92
|
+
useFactory: () => {
|
|
93
|
+
const factory = createWorkerPipelineFactory(createWorker);
|
|
94
|
+
inject(DestroyRef).onDestroy(() => factory.terminate());
|
|
95
|
+
return factory;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
47
100
|
|
|
48
101
|
let webGpuProbe = null;
|
|
49
102
|
/**
|
|
@@ -76,7 +129,7 @@ async function probeWebGpu() {
|
|
|
76
129
|
}
|
|
77
130
|
}
|
|
78
131
|
|
|
79
|
-
const newSession = () => ({ pipe: null, loading: null, inFlight: 0 });
|
|
132
|
+
const newSession = () => ({ pipe: null, loading: null, inFlight: 0, files: new Map() });
|
|
80
133
|
/**
|
|
81
134
|
* A lazily-loaded Transformers.js pipeline wrapped in signals.
|
|
82
135
|
*
|
|
@@ -91,11 +144,15 @@ class PipelineHandle {
|
|
|
91
144
|
/** idle -> loading -> ready <-> busy; error only on load failure. */
|
|
92
145
|
status = signal('idle', /* @ts-ignore */
|
|
93
146
|
...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
94
|
-
/** Download progress
|
|
147
|
+
/** Download progress: the file reported last, plus the total over all files. */
|
|
95
148
|
progress = signal(null, /* @ts-ignore */
|
|
96
149
|
...(ngDevMode ? [{ debugName: "progress" }] : /* istanbul ignore next */ []));
|
|
150
|
+
/** The last load error; cleared when a load starts. */
|
|
97
151
|
error = signal(null, /* @ts-ignore */
|
|
98
152
|
...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
153
|
+
/** The error of the most recently started run, if it failed; cleared when a run starts. */
|
|
154
|
+
runError = signal(null, /* @ts-ignore */
|
|
155
|
+
...(ngDevMode ? [{ debugName: "runError" }] : /* istanbul ignore next */ []));
|
|
99
156
|
/** True once the model is usable (including while a run is in flight). */
|
|
100
157
|
ready = computed(() => this.status() === 'ready' || this.status() === 'busy', /* @ts-ignore */
|
|
101
158
|
...(ngDevMode ? [{ debugName: "ready" }] : /* istanbul ignore next */ []));
|
|
@@ -103,6 +160,8 @@ class PipelineHandle {
|
|
|
103
160
|
...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
104
161
|
session = newSession();
|
|
105
162
|
destroyed = false;
|
|
163
|
+
/** Counts runs so only the most recently started one writes runError. */
|
|
164
|
+
runSequence = 0;
|
|
106
165
|
constructor(request, factory, config) {
|
|
107
166
|
this.request = request;
|
|
108
167
|
this.factory = factory;
|
|
@@ -120,7 +179,12 @@ class PipelineHandle {
|
|
|
120
179
|
});
|
|
121
180
|
return session.loading;
|
|
122
181
|
}
|
|
123
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* Runs the pipeline, loading the model first if needed. Besides the
|
|
184
|
+
* pipeline's own options, `runOptions.signal` (an AbortSignal) makes the
|
|
185
|
+
* run reject before it starts when the signal has already fired, which
|
|
186
|
+
* spares superseded runs the inference (see inferenceResource()).
|
|
187
|
+
*/
|
|
124
188
|
run(input, runOptions) {
|
|
125
189
|
return this.runWith(input, runOptions);
|
|
126
190
|
}
|
|
@@ -130,16 +194,28 @@ class PipelineHandle {
|
|
|
130
194
|
* classification takes (text, candidateLabels, options).
|
|
131
195
|
*/
|
|
132
196
|
async runWith(input, ...extraArgs) {
|
|
197
|
+
const { args, signal } = takeSignal(extraArgs);
|
|
133
198
|
await this.load();
|
|
199
|
+
// The model cannot be interrupted, but a run nobody wants any more
|
|
200
|
+
// (a newer input superseded it while the model loaded) need not start.
|
|
201
|
+
if (signal?.aborted)
|
|
202
|
+
throw abortError();
|
|
134
203
|
const session = this.session;
|
|
135
204
|
const pipe = session.pipe;
|
|
136
205
|
if (!pipe) {
|
|
137
206
|
throw new Error('PipelineHandle: disposed before the model finished loading.');
|
|
138
207
|
}
|
|
208
|
+
const sequence = ++this.runSequence;
|
|
139
209
|
session.inFlight++;
|
|
140
210
|
this.status.set('busy');
|
|
211
|
+
this.runError.set(null);
|
|
141
212
|
try {
|
|
142
|
-
return (await pipe(input, ...
|
|
213
|
+
return (await pipe(input, ...args));
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
if (this.session === session && sequence === this.runSequence)
|
|
217
|
+
this.runError.set(err);
|
|
218
|
+
throw err;
|
|
143
219
|
}
|
|
144
220
|
finally {
|
|
145
221
|
// Back to ready once the last overlapping run finishes - a failed run
|
|
@@ -175,6 +251,9 @@ class PipelineHandle {
|
|
|
175
251
|
async doLoad(session) {
|
|
176
252
|
this.status.set('loading');
|
|
177
253
|
this.error.set(null);
|
|
254
|
+
// A retry after a failed attempt starts its bookkeeping from scratch.
|
|
255
|
+
session.files = new Map();
|
|
256
|
+
this.progress.set(null);
|
|
178
257
|
const options = {
|
|
179
258
|
...this.config.pipelineOptions,
|
|
180
259
|
...this.request.options,
|
|
@@ -204,6 +283,7 @@ class PipelineHandle {
|
|
|
204
283
|
// A cancelled load is not an error - dispose() already reset the handle.
|
|
205
284
|
if (this.session !== session)
|
|
206
285
|
return;
|
|
286
|
+
this.progress.set(null);
|
|
207
287
|
this.error.set(err);
|
|
208
288
|
this.status.set('error');
|
|
209
289
|
throw err;
|
|
@@ -219,20 +299,86 @@ class PipelineHandle {
|
|
|
219
299
|
return configured;
|
|
220
300
|
return this.config.autoDevice ? detectDevice() : configured;
|
|
221
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Transformers.js reports each file on its own (initiate, download,
|
|
304
|
+
* progress, done). The session records every file it hears about; the
|
|
305
|
+
* signal is published on progress and done events only, so a file that
|
|
306
|
+
* has not transferred a byte never replaces the one that is moving.
|
|
307
|
+
*/
|
|
222
308
|
onProgress(event, session) {
|
|
223
309
|
// Events from a load that dispose() cancelled must not resurrect the bar.
|
|
224
|
-
if (this.session !== session)
|
|
225
|
-
return;
|
|
226
|
-
if (event.status !== 'progress' || !event.file)
|
|
310
|
+
if (this.session !== session || !event.file)
|
|
227
311
|
return;
|
|
312
|
+
const file = session.files.get(event.file) ?? { loaded: 0, total: 0, done: false };
|
|
313
|
+
session.files.set(event.file, file);
|
|
314
|
+
switch (event.status) {
|
|
315
|
+
case 'progress':
|
|
316
|
+
file.loaded = event.loaded ?? file.loaded;
|
|
317
|
+
file.total = event.total ?? file.total;
|
|
318
|
+
break;
|
|
319
|
+
case 'done':
|
|
320
|
+
file.done = true;
|
|
321
|
+
if (file.total > 0)
|
|
322
|
+
file.loaded = file.total;
|
|
323
|
+
break;
|
|
324
|
+
default:
|
|
325
|
+
return; // initiate / download: recorded, not published
|
|
326
|
+
}
|
|
327
|
+
const percent = event.status === 'done' ? 100 : Math.round(event.progress ?? 0);
|
|
328
|
+
const overall = overallOf(session.files);
|
|
228
329
|
this.progress.set({
|
|
229
330
|
file: event.file,
|
|
230
|
-
progress:
|
|
231
|
-
loadedBytes:
|
|
232
|
-
totalBytes:
|
|
331
|
+
progress: percent,
|
|
332
|
+
loadedBytes: file.loaded,
|
|
333
|
+
totalBytes: file.total,
|
|
334
|
+
...(overall ? { overall } : {}),
|
|
233
335
|
});
|
|
234
336
|
}
|
|
235
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* The total over the files seen so far. Transformers.js fetches the small
|
|
340
|
+
* files (config, tokenizer) before it starts the weights, so a total
|
|
341
|
+
* computed before the weights are known would read 100% and then collapse;
|
|
342
|
+
* it is therefore withheld until a weights file has been seen.
|
|
343
|
+
*/
|
|
344
|
+
function overallOf(files) {
|
|
345
|
+
let weightsSeen = false;
|
|
346
|
+
let loadedBytes = 0;
|
|
347
|
+
let totalBytes = 0;
|
|
348
|
+
let filesDone = 0;
|
|
349
|
+
for (const [name, file] of files) {
|
|
350
|
+
if (name.includes('.onnx'))
|
|
351
|
+
weightsSeen = true;
|
|
352
|
+
if (file.total > 0) {
|
|
353
|
+
loadedBytes += Math.min(file.loaded, file.total);
|
|
354
|
+
totalBytes += file.total;
|
|
355
|
+
}
|
|
356
|
+
if (file.done)
|
|
357
|
+
filesDone++;
|
|
358
|
+
}
|
|
359
|
+
if (!weightsSeen)
|
|
360
|
+
return undefined;
|
|
361
|
+
return {
|
|
362
|
+
progress: totalBytes > 0 ? Math.round((loadedBytes / totalBytes) * 100) : 0,
|
|
363
|
+
loadedBytes,
|
|
364
|
+
totalBytes,
|
|
365
|
+
files: files.size,
|
|
366
|
+
filesDone,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/** Pulls `signal` out of a call's trailing options object. */
|
|
370
|
+
function takeSignal(args) {
|
|
371
|
+
const last = args[args.length - 1];
|
|
372
|
+
if (typeof last !== 'object' || last === null || Array.isArray(last))
|
|
373
|
+
return { args };
|
|
374
|
+
const { signal, ...rest } = last;
|
|
375
|
+
if (!(signal instanceof AbortSignal))
|
|
376
|
+
return { args };
|
|
377
|
+
return { args: [...args.slice(0, -1), rest], signal };
|
|
378
|
+
}
|
|
379
|
+
function abortError() {
|
|
380
|
+
return new DOMException('PipelineHandle: the run was aborted before it started.', 'AbortError');
|
|
381
|
+
}
|
|
236
382
|
function destroyedError() {
|
|
237
383
|
return new Error('PipelineHandle: destroyed with its component; create a new handle.');
|
|
238
384
|
}
|
|
@@ -255,8 +401,8 @@ const DEFAULT_TEXT_CLASSIFICATION_MODEL = 'Xenova/distilbert-base-uncased-finetu
|
|
|
255
401
|
*/
|
|
256
402
|
class TextClassifier extends PipelineHandle {
|
|
257
403
|
/** Classifies one text; resolves to labels sorted by score (top first). */
|
|
258
|
-
async classify(text, topK = 1) {
|
|
259
|
-
const out = await this.run(text, { top_k: topK });
|
|
404
|
+
async classify(text, topK = 1, options = {}) {
|
|
405
|
+
const out = await this.run(text, { top_k: topK, signal: options.signal });
|
|
260
406
|
// Single input: transformers.js returns a flat array of {label, score}.
|
|
261
407
|
const flat = (Array.isArray(out[0]) ? out[0] : out);
|
|
262
408
|
return [...flat].sort((a, b) => b.score - a.score);
|
|
@@ -293,23 +439,23 @@ function cosineSimilarity(a, b) {
|
|
|
293
439
|
*/
|
|
294
440
|
class TextEmbedder extends PipelineHandle {
|
|
295
441
|
/** Embeds one or many texts; always resolves to one vector per text. */
|
|
296
|
-
async embed(texts) {
|
|
442
|
+
async embed(texts, options = {}) {
|
|
297
443
|
const input = Array.isArray(texts) ? texts : [texts];
|
|
298
444
|
if (input.length === 0)
|
|
299
445
|
return [];
|
|
300
|
-
const out = await this.run(input, { pooling: 'mean', normalize: true });
|
|
446
|
+
const out = await this.run(input, { pooling: 'mean', normalize: true, signal: options.signal });
|
|
301
447
|
return toRows(out);
|
|
302
448
|
}
|
|
303
449
|
/** Cosine similarity of two texts in [-1, 1]. */
|
|
304
|
-
async similarity(a, b) {
|
|
305
|
-
const [va, vb] = await this.embed([a, b]);
|
|
450
|
+
async similarity(a, b, options = {}) {
|
|
451
|
+
const [va, vb] = await this.embed([a, b], options);
|
|
306
452
|
return cosineSimilarity(va, vb);
|
|
307
453
|
}
|
|
308
454
|
/** Ranks documents against a query, most similar first. */
|
|
309
|
-
async rank(query, documents) {
|
|
455
|
+
async rank(query, documents, options = {}) {
|
|
310
456
|
if (documents.length === 0)
|
|
311
457
|
return [];
|
|
312
|
-
const [queryVec, ...docVecs] = await this.embed([query, ...documents]);
|
|
458
|
+
const [queryVec, ...docVecs] = await this.embed([query, ...documents], options);
|
|
313
459
|
return docVecs
|
|
314
460
|
.map((vec, index) => ({
|
|
315
461
|
text: documents[index],
|
|
@@ -393,6 +539,8 @@ class SpeechRecognizer extends PipelineHandle {
|
|
|
393
539
|
runOptions['language'] = options.language;
|
|
394
540
|
if (options.task !== undefined)
|
|
395
541
|
runOptions['task'] = options.task;
|
|
542
|
+
if (options.signal !== undefined)
|
|
543
|
+
runOptions['signal'] = options.signal;
|
|
396
544
|
const raw = await this.run(input, runOptions);
|
|
397
545
|
const first = (Array.isArray(raw) ? raw[0] : raw) ?? {};
|
|
398
546
|
const chunks = first.chunks?.map((c) => ({
|
|
@@ -439,6 +587,8 @@ class ZeroShotClassifier extends PipelineHandle {
|
|
|
439
587
|
runOptions['multi_label'] = options.multiLabel;
|
|
440
588
|
if (options.hypothesisTemplate !== undefined)
|
|
441
589
|
runOptions['hypothesis_template'] = options.hypothesisTemplate;
|
|
590
|
+
if (options.signal !== undefined)
|
|
591
|
+
runOptions['signal'] = options.signal;
|
|
442
592
|
const raw = await this.runWith(text, [...labels], runOptions);
|
|
443
593
|
const first = Array.isArray(raw) ? raw[0] : raw;
|
|
444
594
|
const scoredLabels = first?.labels ?? [];
|
|
@@ -494,6 +644,8 @@ class Translator {
|
|
|
494
644
|
...(ngDevMode ? [{ debugName: "progress" }] : /* istanbul ignore next */ []));
|
|
495
645
|
error = computed(() => this.active()?.error() ?? null, /* @ts-ignore */
|
|
496
646
|
...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
647
|
+
runError = computed(() => this.active()?.runError() ?? null, /* @ts-ignore */
|
|
648
|
+
...(ngDevMode ? [{ debugName: "runError" }] : /* istanbul ignore next */ []));
|
|
497
649
|
ready = computed(() => this.active()?.ready() ?? false, /* @ts-ignore */
|
|
498
650
|
...(ngDevMode ? [{ debugName: "ready" }] : /* istanbul ignore next */ []));
|
|
499
651
|
busy = computed(() => this.active()?.busy() ?? false, /* @ts-ignore */
|
|
@@ -510,7 +662,7 @@ class Translator {
|
|
|
510
662
|
/** Translates one text; the pair defaults to the one given at creation. */
|
|
511
663
|
async translate(text, pair = {}) {
|
|
512
664
|
const handle = this.handleFor(pair);
|
|
513
|
-
const raw = await handle.run(text, this.languageOptions(pair));
|
|
665
|
+
const raw = await handle.run(text, { ...this.languageOptions(pair), signal: pair.signal });
|
|
514
666
|
const first = Array.isArray(raw) ? raw[0] : raw;
|
|
515
667
|
return (first?.translation_text ?? '').trim();
|
|
516
668
|
}
|
|
@@ -584,6 +736,120 @@ function createTranslator(options = {}) {
|
|
|
584
736
|
return translator;
|
|
585
737
|
}
|
|
586
738
|
|
|
739
|
+
// The smallest instruct model that holds a conversation; its repository
|
|
740
|
+
// ships the ONNX variants Transformers.js loads (~100 MB at q4).
|
|
741
|
+
const DEFAULT_TEXT_GENERATION_MODEL = 'HuggingFaceTB/SmolLM2-135M-Instruct';
|
|
742
|
+
/**
|
|
743
|
+
* Text generation with a small language model, streamed token by token.
|
|
744
|
+
* The default checkpoint is SmolLM2-135M-Instruct; swap `model` for any
|
|
745
|
+
* Transformers.js text-generation checkpoint (Qwen2.5, Llama, Phi...).
|
|
746
|
+
*/
|
|
747
|
+
class TextGenerator extends PipelineHandle {
|
|
748
|
+
/** The text generated so far by the latest generate() call; reset when a call starts. */
|
|
749
|
+
output = signal('', /* @ts-ignore */
|
|
750
|
+
...(ngDevMode ? [{ debugName: "output" }] : /* istanbul ignore next */ []));
|
|
751
|
+
/** Only the most recently started call may write output. */
|
|
752
|
+
generation = 0;
|
|
753
|
+
warnedNoStreaming = false;
|
|
754
|
+
/**
|
|
755
|
+
* Generates a reply to a prompt or a chat. Tokens stream into `output`
|
|
756
|
+
* (and `options.onToken`) while the model runs; resolves with the full,
|
|
757
|
+
* trimmed reply.
|
|
758
|
+
*/
|
|
759
|
+
async generate(prompt, options = {}) {
|
|
760
|
+
const runOptions = { max_new_tokens: options.maxNewTokens ?? 256 };
|
|
761
|
+
if (options.doSample !== undefined)
|
|
762
|
+
runOptions['do_sample'] = options.doSample;
|
|
763
|
+
if (options.temperature !== undefined)
|
|
764
|
+
runOptions['temperature'] = options.temperature;
|
|
765
|
+
if (options.topP !== undefined)
|
|
766
|
+
runOptions['top_p'] = options.topP;
|
|
767
|
+
if (options.topK !== undefined)
|
|
768
|
+
runOptions['top_k'] = options.topK;
|
|
769
|
+
if (options.repetitionPenalty !== undefined) {
|
|
770
|
+
runOptions['repetition_penalty'] = options.repetitionPenalty;
|
|
771
|
+
}
|
|
772
|
+
// A plain prompt would otherwise come back with the prompt in front.
|
|
773
|
+
if (typeof prompt === 'string')
|
|
774
|
+
runOptions['return_full_text'] = false;
|
|
775
|
+
if (options.signal !== undefined)
|
|
776
|
+
runOptions['signal'] = options.signal;
|
|
777
|
+
const generation = ++this.generation;
|
|
778
|
+
let streamed = 0;
|
|
779
|
+
this.output.set('');
|
|
780
|
+
runOptions['onToken'] = (text) => {
|
|
781
|
+
streamed++;
|
|
782
|
+
if (generation === this.generation)
|
|
783
|
+
this.output.update((current) => current + text);
|
|
784
|
+
options.onToken?.(text);
|
|
785
|
+
};
|
|
786
|
+
const raw = await this.run(prompt, runOptions);
|
|
787
|
+
const first = Array.isArray(raw) ? raw[0] : raw;
|
|
788
|
+
const generated = first?.generated_text;
|
|
789
|
+
const text = Array.isArray(generated) ? (generated.at(-1)?.content ?? '') : (generated ?? '');
|
|
790
|
+
const reply = text.trim();
|
|
791
|
+
if (generation === this.generation)
|
|
792
|
+
this.output.set(reply);
|
|
793
|
+
if (streamed === 0 && reply && !this.warnedNoStreaming) {
|
|
794
|
+
// Streaming relies on the pipeline factory turning the onToken run
|
|
795
|
+
// option into a TextStreamer, which the built-in factories do. A
|
|
796
|
+
// custom factory that returns a raw pipeline drops it silently.
|
|
797
|
+
this.warnedNoStreaming = true;
|
|
798
|
+
console.warn('ngx-transformers: the reply arrived without streamed tokens. A custom PIPELINE_FACTORY must wrap createDefaultPipelineFactory() (or handle the onToken run option) for TextGenerator.output to stream.');
|
|
799
|
+
}
|
|
800
|
+
return reply;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Creates a TextGenerator in an injection context; destroyed with the
|
|
805
|
+
* component. dtype defaults to 'q4', the size/quality sweet spot for small
|
|
806
|
+
* decoders on the WebAssembly runtime.
|
|
807
|
+
*/
|
|
808
|
+
function createTextGenerator(options = {}) {
|
|
809
|
+
const generator = new TextGenerator({ task: 'text-generation', model: DEFAULT_TEXT_GENERATION_MODEL, dtype: 'q4', ...options }, inject(PIPELINE_FACTORY), inject(NGX_TRANSFORMERS_CONFIG));
|
|
810
|
+
inject(DestroyRef, { optional: true })?.onDestroy(() => void generator.destroy());
|
|
811
|
+
return generator;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Runs inference whenever an input signal changes, as an Angular resource:
|
|
816
|
+
* `value()`, `isLoading()`, `error()` and `status()` are signals, and only
|
|
817
|
+
* the result for the latest input is kept.
|
|
818
|
+
*
|
|
819
|
+
* ```ts
|
|
820
|
+
* readonly text = signal('');
|
|
821
|
+
* readonly classifier = createTextClassifier();
|
|
822
|
+
* readonly sentiment = inferenceResource({
|
|
823
|
+
* input: () => this.text().trim() || undefined,
|
|
824
|
+
* run: (text, signal) => this.classifier.classify(text, 1, { signal }),
|
|
825
|
+
* debounceMs: 300,
|
|
826
|
+
* });
|
|
827
|
+
* // template: @if (sentiment.value(); as result) { {{ result[0].label }} }
|
|
828
|
+
* ```
|
|
829
|
+
*/
|
|
830
|
+
function inferenceResource(options) {
|
|
831
|
+
const injector = options.injector ?? inject(Injector);
|
|
832
|
+
const input = options.debounceMs
|
|
833
|
+
? debounced(options.input, options.debounceMs, injector)
|
|
834
|
+
: options.input;
|
|
835
|
+
return resource({
|
|
836
|
+
params: () => input(),
|
|
837
|
+
loader: ({ params, abortSignal }) => options.run(params, abortSignal),
|
|
838
|
+
injector,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
/** A signal that follows `source` once it has been stable for `ms`. */
|
|
842
|
+
function debounced(source, ms, injector) {
|
|
843
|
+
const out = signal(untracked(source), /* @ts-ignore */
|
|
844
|
+
...(ngDevMode ? [{ debugName: "out" }] : /* istanbul ignore next */ []));
|
|
845
|
+
effect((onCleanup) => {
|
|
846
|
+
const value = source();
|
|
847
|
+
const timer = setTimeout(() => out.set(value), ms);
|
|
848
|
+
onCleanup(() => clearTimeout(timer));
|
|
849
|
+
}, { injector });
|
|
850
|
+
return out.asReadonly();
|
|
851
|
+
}
|
|
852
|
+
|
|
587
853
|
/**
|
|
588
854
|
* Microphone capture with a signals API. start() asks for mic permission
|
|
589
855
|
* and records; stop() resolves with the encoded audio Blob - pass it to
|
|
@@ -685,7 +951,8 @@ function createMicRecorder(deps = {}) {
|
|
|
685
951
|
|
|
686
952
|
/**
|
|
687
953
|
* Drop-in status line for a PipelineHandle: shows model download progress
|
|
688
|
-
* while loading, then the ready/busy/error
|
|
954
|
+
* (over every file of the model) while loading, then the ready/busy/error
|
|
955
|
+
* state. Themeable via CSS custom
|
|
689
956
|
* properties (--nt-accent, --nt-ink, --nt-muted, --nt-track).
|
|
690
957
|
*
|
|
691
958
|
* ```html
|
|
@@ -700,6 +967,10 @@ class ModelProgressComponent {
|
|
|
700
967
|
/** Labels per status; override to localize. */
|
|
701
968
|
labels = input({}, /* @ts-ignore */
|
|
702
969
|
...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
|
|
970
|
+
/** The bar follows the whole download when the handle reports it, else the current file. */
|
|
971
|
+
percent(progress) {
|
|
972
|
+
return progress.overall?.progress ?? progress.progress;
|
|
973
|
+
}
|
|
703
974
|
label = computed(() => {
|
|
704
975
|
const defaults = {
|
|
705
976
|
idle: 'Model not loaded',
|
|
@@ -711,28 +982,31 @@ class ModelProgressComponent {
|
|
|
711
982
|
return this.labels()[this.status()] ?? defaults[this.status()];
|
|
712
983
|
}, /* @ts-ignore */
|
|
713
984
|
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
714
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.
|
|
715
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.
|
|
985
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ModelProgressComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
986
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ModelProgressComponent, isStandalone: true, selector: "ngx-model-progress", inputs: { status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: true, transformFunction: null }, progress: { classPropertyName: "progress", publicName: "progress", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
716
987
|
<div class="nt-row" [class.nt-error]="status() === 'error'" role="status">
|
|
717
988
|
<span class="nt-dot" [attr.data-status]="status()"></span>
|
|
718
989
|
<span class="nt-label">{{ label() }}</span>
|
|
719
990
|
@if (status() === 'loading' && progress(); as p) {
|
|
720
991
|
<span class="nt-file">{{ p.file }}</span>
|
|
992
|
+
@if (p.overall && p.overall.files > 1) {
|
|
993
|
+
<span class="nt-files">{{ p.overall.filesDone }}/{{ p.overall.files }} files</span>
|
|
994
|
+
}
|
|
721
995
|
<div
|
|
722
996
|
class="nt-track"
|
|
723
997
|
role="progressbar"
|
|
724
|
-
[attr.aria-valuenow]="p
|
|
998
|
+
[attr.aria-valuenow]="percent(p)"
|
|
725
999
|
aria-valuemin="0"
|
|
726
1000
|
aria-valuemax="100"
|
|
727
1001
|
>
|
|
728
|
-
<div class="nt-fill" [style.width.%]="p
|
|
1002
|
+
<div class="nt-fill" [style.width.%]="percent(p)"></div>
|
|
729
1003
|
</div>
|
|
730
|
-
<span class="nt-pct">{{ p
|
|
1004
|
+
<span class="nt-pct">{{ percent(p) }}%</span>
|
|
731
1005
|
}
|
|
732
1006
|
</div>
|
|
733
|
-
`, isInline: true, styles: [":host{--nt-accent: #f59e0b;--nt-ink: #1e293b;--nt-muted: #64748b;--nt-track: #e2e8f0;display:block;font-size:12.5px;color:var(--nt-ink)}.nt-row{display:flex;align-items:center;gap:8px}.nt-dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--nt-muted)}.nt-dot[data-status=loading]{background:var(--nt-accent);animation:nt-blink 1s ease-in-out infinite}.nt-dot[data-status=ready]{background:#22c55e}.nt-dot[data-status=busy]{background:#3b82f6;animation:nt-blink .7s ease-in-out infinite}.nt-dot[data-status=error]{background:#dc2626}.nt-label{font-weight:600}.nt-file{color:var(--nt-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}.nt-track{flex:1;min-width:60px;height:6px;border-radius:3px;background:var(--nt-track);overflow:hidden}.nt-fill{height:100%;background:var(--nt-accent);border-radius:3px;transition:width .12s linear}.nt-pct{font-variant-numeric:tabular-nums;color:var(--nt-muted)}.nt-error .nt-label{color:#dc2626}@keyframes nt-blink{0%,to{opacity:1}50%{opacity:.35}}@media(prefers-reduced-motion:reduce){.nt-dot{animation:none}.nt-fill{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1007
|
+
`, isInline: true, styles: [":host{--nt-accent: #f59e0b;--nt-ink: #1e293b;--nt-muted: #64748b;--nt-track: #e2e8f0;display:block;font-size:12.5px;color:var(--nt-ink)}.nt-row{display:flex;align-items:center;gap:8px}.nt-dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--nt-muted)}.nt-dot[data-status=loading]{background:var(--nt-accent);animation:nt-blink 1s ease-in-out infinite}.nt-dot[data-status=ready]{background:#22c55e}.nt-dot[data-status=busy]{background:#3b82f6;animation:nt-blink .7s ease-in-out infinite}.nt-dot[data-status=error]{background:#dc2626}.nt-label{font-weight:600}.nt-file{color:var(--nt-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}.nt-track{flex:1;min-width:60px;height:6px;border-radius:3px;background:var(--nt-track);overflow:hidden}.nt-fill{height:100%;background:var(--nt-accent);border-radius:3px;transition:width .12s linear}.nt-files{color:var(--nt-muted);white-space:nowrap}.nt-pct{font-variant-numeric:tabular-nums;color:var(--nt-muted)}.nt-error .nt-label{color:#dc2626}@keyframes nt-blink{0%,to{opacity:1}50%{opacity:.35}}@media(prefers-reduced-motion:reduce){.nt-dot{animation:none}.nt-fill{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
734
1008
|
}
|
|
735
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.
|
|
1009
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ModelProgressComponent, decorators: [{
|
|
736
1010
|
type: Component,
|
|
737
1011
|
args: [{ selector: 'ngx-model-progress', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
738
1012
|
<div class="nt-row" [class.nt-error]="status() === 'error'" role="status">
|
|
@@ -740,28 +1014,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
740
1014
|
<span class="nt-label">{{ label() }}</span>
|
|
741
1015
|
@if (status() === 'loading' && progress(); as p) {
|
|
742
1016
|
<span class="nt-file">{{ p.file }}</span>
|
|
1017
|
+
@if (p.overall && p.overall.files > 1) {
|
|
1018
|
+
<span class="nt-files">{{ p.overall.filesDone }}/{{ p.overall.files }} files</span>
|
|
1019
|
+
}
|
|
743
1020
|
<div
|
|
744
1021
|
class="nt-track"
|
|
745
1022
|
role="progressbar"
|
|
746
|
-
[attr.aria-valuenow]="p
|
|
1023
|
+
[attr.aria-valuenow]="percent(p)"
|
|
747
1024
|
aria-valuemin="0"
|
|
748
1025
|
aria-valuemax="100"
|
|
749
1026
|
>
|
|
750
|
-
<div class="nt-fill" [style.width.%]="p
|
|
1027
|
+
<div class="nt-fill" [style.width.%]="percent(p)"></div>
|
|
751
1028
|
</div>
|
|
752
|
-
<span class="nt-pct">{{ p
|
|
1029
|
+
<span class="nt-pct">{{ percent(p) }}%</span>
|
|
753
1030
|
}
|
|
754
1031
|
</div>
|
|
755
|
-
`, styles: [":host{--nt-accent: #f59e0b;--nt-ink: #1e293b;--nt-muted: #64748b;--nt-track: #e2e8f0;display:block;font-size:12.5px;color:var(--nt-ink)}.nt-row{display:flex;align-items:center;gap:8px}.nt-dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--nt-muted)}.nt-dot[data-status=loading]{background:var(--nt-accent);animation:nt-blink 1s ease-in-out infinite}.nt-dot[data-status=ready]{background:#22c55e}.nt-dot[data-status=busy]{background:#3b82f6;animation:nt-blink .7s ease-in-out infinite}.nt-dot[data-status=error]{background:#dc2626}.nt-label{font-weight:600}.nt-file{color:var(--nt-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}.nt-track{flex:1;min-width:60px;height:6px;border-radius:3px;background:var(--nt-track);overflow:hidden}.nt-fill{height:100%;background:var(--nt-accent);border-radius:3px;transition:width .12s linear}.nt-pct{font-variant-numeric:tabular-nums;color:var(--nt-muted)}.nt-error .nt-label{color:#dc2626}@keyframes nt-blink{0%,to{opacity:1}50%{opacity:.35}}@media(prefers-reduced-motion:reduce){.nt-dot{animation:none}.nt-fill{transition:none}}\n"] }]
|
|
1032
|
+
`, styles: [":host{--nt-accent: #f59e0b;--nt-ink: #1e293b;--nt-muted: #64748b;--nt-track: #e2e8f0;display:block;font-size:12.5px;color:var(--nt-ink)}.nt-row{display:flex;align-items:center;gap:8px}.nt-dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--nt-muted)}.nt-dot[data-status=loading]{background:var(--nt-accent);animation:nt-blink 1s ease-in-out infinite}.nt-dot[data-status=ready]{background:#22c55e}.nt-dot[data-status=busy]{background:#3b82f6;animation:nt-blink .7s ease-in-out infinite}.nt-dot[data-status=error]{background:#dc2626}.nt-label{font-weight:600}.nt-file{color:var(--nt-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}.nt-track{flex:1;min-width:60px;height:6px;border-radius:3px;background:var(--nt-track);overflow:hidden}.nt-fill{height:100%;background:var(--nt-accent);border-radius:3px;transition:width .12s linear}.nt-files{color:var(--nt-muted);white-space:nowrap}.nt-pct{font-variant-numeric:tabular-nums;color:var(--nt-muted)}.nt-error .nt-label{color:#dc2626}@keyframes nt-blink{0%,to{opacity:1}50%{opacity:.35}}@media(prefers-reduced-motion:reduce){.nt-dot{animation:none}.nt-fill{transition:none}}\n"] }]
|
|
756
1033
|
}], propDecorators: { status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: true }] }], progress: [{ type: i0.Input, args: [{ isSignal: true, alias: "progress", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }] } });
|
|
757
1034
|
|
|
758
|
-
|
|
759
|
-
* Public API
|
|
1035
|
+
/**
|
|
1036
|
+
* Public API surface of ngx-transformers: handles, wrappers, configuration
|
|
1037
|
+
* and the progress component.
|
|
1038
|
+
*
|
|
1039
|
+
* @module ngx-transformers
|
|
760
1040
|
*/
|
|
761
1041
|
|
|
762
1042
|
/**
|
|
763
1043
|
* Generated bundle index. Do not edit.
|
|
764
1044
|
*/
|
|
765
1045
|
|
|
766
|
-
export { DEFAULT_ASR_MODEL, DEFAULT_EMBEDDING_MODEL, DEFAULT_TEXT_CLASSIFICATION_MODEL, DEFAULT_ZERO_SHOT_MODEL, MicRecorder, ModelProgressComponent, NGX_TRANSFORMERS_CONFIG, PIPELINE_FACTORY, PipelineHandle, SpeechRecognizer, TextClassifier, TextEmbedder, Translator, WHISPER_SAMPLE_RATE, ZeroShotClassifier, cosineSimilarity, createDefaultPipelineFactory, createMicRecorder, createPipeline, createSpeechRecognizer, createTextClassifier, createTextEmbedder, createTranslator, createZeroShotClassifier, decodeAudio, defaultTranslationModel, detectDevice, hasWebGpu, provideTransformers, resetDeviceDetection, resolveTranslationModel };
|
|
1046
|
+
export { DEFAULT_ASR_MODEL, DEFAULT_EMBEDDING_MODEL, DEFAULT_TEXT_CLASSIFICATION_MODEL, DEFAULT_TEXT_GENERATION_MODEL, DEFAULT_ZERO_SHOT_MODEL, MicRecorder, ModelProgressComponent, NGX_TRANSFORMERS_CONFIG, PIPELINE_FACTORY, PipelineHandle, SpeechRecognizer, TextClassifier, TextEmbedder, TextGenerator, Translator, WHISPER_SAMPLE_RATE, ZeroShotClassifier, cosineSimilarity, createDefaultPipelineFactory, createMicRecorder, createPipeline, createSpeechRecognizer, createTextClassifier, createTextEmbedder, createTextGenerator, createTranslator, createZeroShotClassifier, decodeAudio, defaultTranslationModel, detectDevice, hasWebGpu, inferenceResource, provideTransformers, provideTransformersWorker, resetDeviceDetection, resolveTranslationModel };
|
|
767
1047
|
//# sourceMappingURL=ngx-transformers.mjs.map
|