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.
@@ -1,7 +1,25 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, makeEnvironmentProviders, signal, computed, inject, DestroyRef, input, ChangeDetectionStrategy, Component } from '@angular/core';
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 { pipeline } = await load();
24
- return (await pipeline(task, model, options));
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 for the file currently transferring, else null. */
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
- /** Runs the pipeline, loading the model first if needed. */
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, ...extraArgs));
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: Math.round(event.progress ?? 0),
231
- loadedBytes: event.loaded ?? 0,
232
- totalBytes: event.total ?? 0,
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 state. Themeable via CSS custom
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.5", ngImport: i0, type: ModelProgressComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
715
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", 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: `
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.progress"
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.progress"></div>
1002
+ <div class="nt-fill" [style.width.%]="percent(p)"></div>
729
1003
  </div>
730
- <span class="nt-pct">{{ p.progress }}%</span>
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.5", ngImport: i0, type: ModelProgressComponent, decorators: [{
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.progress"
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.progress"></div>
1027
+ <div class="nt-fill" [style.width.%]="percent(p)"></div>
751
1028
  </div>
752
- <span class="nt-pct">{{ p.progress }}%</span>
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 Surface of ngx-transformers
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