nuxt-module-essentia 1.0.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.
Files changed (36) hide show
  1. package/README.md +54 -0
  2. package/dist/module.d.ts +5 -0
  3. package/dist/module.js +31 -0
  4. package/dist/runtime/composables/useAudioAnalizer.d.ts +44 -0
  5. package/dist/runtime/index.d.ts +1 -0
  6. package/dist/runtime/utils/config.d.ts +1 -0
  7. package/package.json +70 -0
  8. package/src/runtime/composables/useAudioAnalizer.ts +379 -0
  9. package/src/runtime/essentia-wasm.web.js +40 -0
  10. package/src/runtime/essentia.js-core.js +1 -0
  11. package/src/runtime/index.ts +1 -0
  12. package/src/runtime/models/LICENSE +1367 -0
  13. package/src/runtime/models/danceability-msd-musicnn-1/tfjs/group1-shard1of1.bin +0 -0
  14. package/src/runtime/models/danceability-msd-musicnn-1/tfjs/model.json +1 -0
  15. package/src/runtime/models/emomusic-msd-musicnn-1/tfjs/group1-shard1of1.bin +0 -0
  16. package/src/runtime/models/emomusic-msd-musicnn-1/tfjs/model.json +1 -0
  17. package/src/runtime/models/mood_aggressive-msd-musicnn-1/tfjs/group1-shard1of1.bin +0 -0
  18. package/src/runtime/models/mood_aggressive-msd-musicnn-1/tfjs/model.json +1 -0
  19. package/src/runtime/models/mood_happy-msd-musicnn-1/tfjs/group1-shard1of1.bin +0 -0
  20. package/src/runtime/models/mood_happy-msd-musicnn-1/tfjs/model.json +1 -0
  21. package/src/runtime/models/mood_relaxed-msd-musicnn-1/tfjs/group1-shard1of1.bin +0 -0
  22. package/src/runtime/models/mood_relaxed-msd-musicnn-1/tfjs/model.json +1 -0
  23. package/src/runtime/models/mood_sad-msd-musicnn-1/tfjs/group1-shard1of1.bin +0 -0
  24. package/src/runtime/models/mood_sad-msd-musicnn-1/tfjs/model.json +1 -0
  25. package/src/runtime/models/msd-musicnn-1/group1-shard1of1.bin +0 -0
  26. package/src/runtime/models/msd-musicnn-1/model.json +1 -0
  27. package/src/runtime/utils/config.ts +14 -0
  28. package/src/runtime/workers/featureExtraction.js +89 -0
  29. package/src/runtime/workers/inference.js +135 -0
  30. package/src/runtime/workers/lib/essentia-wasm.module.js +33 -0
  31. package/src/runtime/workers/lib/essentia.js-model.umd.js +641 -0
  32. package/src/runtime/workers/lib/tf-backend-wasm-3.5.0.js +5115 -0
  33. package/src/runtime/workers/lib/tf.min.3.5.0.js +18 -0
  34. package/src/runtime/workers/tfjs-backend-wasm-simd.wasm +0 -0
  35. package/src/runtime/workers/tfjs-backend-wasm-threaded-simd.wasm +0 -0
  36. package/src/runtime/workers/tfjs-backend-wasm.wasm +0 -0
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # nuxt-module-essentia
2
+
3
+ Nuxt модуль для интеграции Essentia.js WASM библиотеки анализа аудио.
4
+ Работает на Nuxt 3 и 4
5
+
6
+ ## Установка
7
+
8
+ ```bash
9
+ npm install nuxt-module-essentia
10
+ ```
11
+
12
+ ### Подключение модуля
13
+
14
+ ```typescript
15
+ // nuxt.config.ts
16
+ export default defineNuxtConfig({
17
+ modules: ["nuxt-module-essentia"],
18
+
19
+ essentia: {
20
+ publicAssetsPath: "/essentia/", // опционально
21
+ },
22
+ });
23
+ ```
24
+
25
+ ### Использование композабла
26
+
27
+ ```typescript
28
+ // Автоматически доступен в компонентах
29
+ const {
30
+ getKeyMoodAndBpm,
31
+ keyBpmResults,
32
+ moodResults,
33
+ resetMoodResults,
34
+ essentia,
35
+ essentiaAnalysis,
36
+ featureExtractionWorker
37
+ } = useAudioAnalizer();
38
+ ```
39
+
40
+ ## Разработка
41
+
42
+ ```bash
43
+ # Сборка модуля
44
+ npm run build
45
+
46
+ # Режим разработки с watch
47
+ npm run dev
48
+ ```
49
+
50
+ ## Структура
51
+
52
+ - `src/module.ts` - основной файл модуля
53
+ - `src/runtime/` - runtime файлы (копируются в public)
54
+ - `src/runtime/composables/` - композаблы (автоимпорт)
@@ -0,0 +1,5 @@
1
+ export interface ModuleOptions {
2
+ publicAssetsPath: string;
3
+ }
4
+ declare const _default: import("@nuxt/schema").NuxtModule<ModuleOptions, ModuleOptions, false>;
5
+ export default _default;
package/dist/module.js ADDED
@@ -0,0 +1,31 @@
1
+ import { defineNuxtModule as a, createResolver as r, addTemplate as l, addImportsDir as c } from "@nuxt/kit";
2
+ const u = a({
3
+ meta: {
4
+ name: "nuxt-module-essentia",
5
+ configKey: "essentia",
6
+ compatibility: {
7
+ nuxt: "^3.0.0 || ^4.0.0"
8
+ }
9
+ },
10
+ defaults: {
11
+ publicAssetsPath: "/essentia/"
12
+ },
13
+ setup(e, s) {
14
+ var i;
15
+ const t = r(import.meta.url), o = t.resolve("../src/runtime"), n = t.resolve("../src/runtime/composables");
16
+ (i = s.options.nitro).publicAssets || (i.publicAssets = []), s.options.nitro.publicAssets.push({
17
+ dir: o,
18
+ baseURL: e.publicAssetsPath,
19
+ maxAge: 60 * 60 * 24 * 365
20
+ // 1 год
21
+ }), l({
22
+ filename: "essentia-config.mjs",
23
+ getContents: () => `export const essentiaConfig = ${JSON.stringify({
24
+ publicAssetsPath: e.publicAssetsPath
25
+ })}`
26
+ }), c(n);
27
+ }
28
+ });
29
+ export {
30
+ u as default
31
+ };
@@ -0,0 +1,44 @@
1
+ import { type Ref } from "vue";
2
+ export declare const useAudioAnalizer: () => {
3
+ getKeyMoodAndBpm: () => void;
4
+ keyBpmResults: Ref<{}, {}>;
5
+ moodResults: Ref<{}, {}>;
6
+ resetMoodResults: () => void;
7
+ essentia?: undefined;
8
+ essentiaAnalysis?: undefined;
9
+ featureExtractionWorker?: undefined;
10
+ } | {
11
+ getKeyMoodAndBpm: (file: File) => Promise<void>;
12
+ keyBpmResults: Ref<{
13
+ key: string;
14
+ bpm: number;
15
+ scale: string;
16
+ } | null, {
17
+ key: string;
18
+ bpm: number;
19
+ scale: string;
20
+ } | null>;
21
+ moodResults: Ref<{
22
+ color: string;
23
+ icon: string;
24
+ title: string;
25
+ key: string;
26
+ value: number;
27
+ }[], {
28
+ color: string;
29
+ icon: string;
30
+ title: string;
31
+ key: string;
32
+ value: number;
33
+ }[] | {
34
+ color: string;
35
+ icon: string;
36
+ title: string;
37
+ key: string;
38
+ value: number;
39
+ }[]>;
40
+ resetMoodResults: () => void;
41
+ essentia: any;
42
+ essentiaAnalysis: undefined;
43
+ featureExtractionWorker: any;
44
+ };
@@ -0,0 +1 @@
1
+ export { useAudioAnalizer } from "./composables/useAudioAnalizer";
@@ -0,0 +1 @@
1
+ export declare function getEssentiaConfig(): Promise<any>;
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "nuxt-module-essentia",
3
+ "version": "1.0.0",
4
+ "description": "Nuxt module for Essentia WASM integration",
5
+ "type": "module",
6
+ "main": "./dist/module.js",
7
+ "types": "./dist/module.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/module.js",
11
+ "types": "./dist/module.d.ts"
12
+ },
13
+ "./runtime": {
14
+ "import": "./src/runtime/index.ts",
15
+ "types": "./src/runtime/index.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src/runtime"
21
+ ],
22
+ "scripts": {
23
+ "build": "vite build && tsc --emitDeclarationOnly",
24
+ "dev": "vite build --watch",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "dependencies": {
28
+ "@nuxt/kit": "^3.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@nuxt/schema": "^3.0.0",
32
+ "nuxt": "^3.0.0",
33
+ "typescript": "^5.0.0",
34
+ "vite": "^5.0.0"
35
+ },
36
+ "peerDependencies": {
37
+ "nuxt": "^3.0.0"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/nikitakashin/nuxt-module-essentia.git"
42
+ },
43
+ "author": {
44
+ "name": "Kashin Nikita",
45
+ "url": "https://github.com/nikitakashin/",
46
+ "email": "dev@nkashin.ru"
47
+ },
48
+ "published": "11.2025",
49
+ "keywords": [
50
+ "nuxt",
51
+ "nuxt-module",
52
+ "essentia",
53
+ "essentia-wasm",
54
+ "audio-analysis",
55
+ "music-information-retrieval",
56
+ "wasm",
57
+ "web-audio",
58
+ "audio-processing",
59
+ "machine-learning",
60
+ "signal-processing",
61
+ "feature-extraction",
62
+ "nuxt3",
63
+ "nuxt4"
64
+ ],
65
+ "license": "ISC",
66
+ "bugs": {
67
+ "url": "https://github.com/nikitakashin/nuxt-module-essentia/issues"
68
+ },
69
+ "homepage": "https://github.com/nikitakashin/nuxt-module-essentia"
70
+ }
@@ -0,0 +1,379 @@
1
+ import { ref, type Ref } from "vue";
2
+
3
+ export const useAudioAnalizer = () => {
4
+ if (!import.meta.client) {
5
+ return {
6
+ getKeyMoodAndBpm: () => {},
7
+ keyBpmResults: ref({}),
8
+ moodResults: ref({}),
9
+ resetMoodResults: () => {},
10
+ };
11
+ }
12
+
13
+ const DEFAULT_MOOD_VALUE = [
14
+ {
15
+ color: "light-blue-lighten-2",
16
+ icon: "💃",
17
+ title: "Танцевальный",
18
+ key: "danceability",
19
+ value: 0,
20
+ },
21
+ {
22
+ color: "light-blue-lighten-1",
23
+ icon: "😊",
24
+ title: "Радостный",
25
+ key: "mood_happy",
26
+ value: 0,
27
+ },
28
+ {
29
+ color: "light-blue-darken-1",
30
+ icon: "😢",
31
+ title: "Грустный",
32
+ key: "mood_sad",
33
+ value: 0,
34
+ },
35
+ {
36
+ color: "light-blue-darken-2",
37
+ icon: "😌",
38
+ title: "Расслабляющий",
39
+ key: "mood_relaxed",
40
+ value: 0,
41
+ },
42
+ {
43
+ color: "light-blue-darken-3",
44
+ icon: "😤",
45
+ title: "Агрессивный",
46
+ key: "mood_aggressive",
47
+ value: 0,
48
+ },
49
+ ];
50
+
51
+ const keyBpmResults: Ref<{ key: string; bpm: number; scale: string } | null> = ref(null);
52
+
53
+ const moodResults = ref(DEFAULT_MOOD_VALUE);
54
+
55
+ let audioCtx: AudioContext;
56
+
57
+ if (import.meta.client) {
58
+ // @ts-ignore
59
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
60
+ }
61
+
62
+ const KEEP_PERCENTAGE = 0.15; // keep only 15% of audio file
63
+ let essentia: any = null;
64
+ // @ts-ignore
65
+ let essentiaAnalysis;
66
+ let featureExtractionWorker: any = null;
67
+ const modelNames = ["mood_happy", "mood_sad", "mood_relaxed", "mood_aggressive", "danceability"];
68
+
69
+ let basePath = "/essentia/";
70
+ let inferenceWorker: Worker;
71
+ let inferenceStartTime = 0;
72
+
73
+ if (import.meta.client) {
74
+ // Загружаем конфиг асинхронно
75
+ // @ts-ignore
76
+ import("#build/essentia-config.mjs")
77
+ .then((module: any) => {
78
+ basePath = module.essentiaConfig.publicAssetsPath;
79
+ })
80
+ .catch(() => {
81
+ basePath = "/essentia/"; // fallback
82
+ });
83
+
84
+ createInferenceWorker();
85
+ createFeatureExtractionWorker();
86
+ }
87
+ // @ts-ignore
88
+ if (import.meta.client) {
89
+ // Загружаем essentia через глобальный window объект
90
+ const script = document.createElement("script");
91
+ script.src = `${basePath}essentia-wasm.web.js`;
92
+ script.onload = () => {
93
+ // @ts-ignore
94
+ window.EssentiaWASM().then((wasmModule: any) => {
95
+ essentia = new wasmModule.EssentiaJS(false);
96
+ essentia.arrayToVector = wasmModule.arrayToVector;
97
+ });
98
+ };
99
+ document.head.appendChild(script);
100
+ }
101
+
102
+ function createInferenceWorker() {
103
+ inferenceWorker = new Worker(`${basePath}workers/inference.js`);
104
+ inferenceWorker.onmessage = function listenToWorker(msg) {
105
+ if (msg.data.predictions) {
106
+ const preds = msg.data.predictions;
107
+
108
+ moodResults.value.forEach((mood) => {
109
+ mood.value = Math.ceil(preds[mood.key] * 100);
110
+ });
111
+ }
112
+ };
113
+ }
114
+
115
+ function createFeatureExtractionWorker() {
116
+ featureExtractionWorker = new Worker(`${basePath}workers/featureExtraction.js`);
117
+ featureExtractionWorker.postMessage({
118
+ init: true,
119
+ });
120
+ featureExtractionWorker.onmessage =
121
+ // @ts-ignore
122
+ function listenToFeatureExtractionWorker(msg) {
123
+ // feed to models
124
+ if (msg.data.embeddings) {
125
+ inferenceStartTime = Date.now();
126
+ // send features off to each of the models
127
+ inferenceWorker.postMessage({
128
+ embeddings: msg.data.embeddings,
129
+ });
130
+ // msg.data.embeddings = null;
131
+ }
132
+ // free worker resource until next audio is uploaded
133
+ // featureExtractionWorker.terminate();
134
+ };
135
+ }
136
+
137
+ function monomix(buffer: AudioBuffer) {
138
+ // downmix to mono
139
+ let monoAudio;
140
+ if (buffer.numberOfChannels > 1) {
141
+ const leftCh = buffer.getChannelData(0);
142
+ const rightCh = buffer.getChannelData(1);
143
+ // @ts-ignore
144
+ monoAudio = leftCh.map((sample, i) => 0.5 * (sample + rightCh[i]));
145
+ } else {
146
+ monoAudio = buffer.getChannelData(0);
147
+ }
148
+
149
+ return monoAudio;
150
+ }
151
+
152
+ function shortenAudio(audioIn: Float32Array, keepRatio = 0.5, trim = false) {
153
+ /*
154
+ keepRatio applied after discarding start and end (if trim == true)
155
+ */
156
+ if (keepRatio < 0.15) {
157
+ keepRatio = 0.15; // must keep at least 15% of the file
158
+ } else if (keepRatio > 0.66) {
159
+ keepRatio = 0.66; // will keep at most 2/3 of the file
160
+ }
161
+
162
+ if (trim) {
163
+ const discardSamples = Math.floor(0.1 * audioIn.length); // discard 10% on beginning and end
164
+ audioIn = audioIn.subarray(discardSamples, audioIn.length - discardSamples); // create new view of buffer without beginning and end
165
+ }
166
+
167
+ const ratioSampleLength = Math.ceil(audioIn.length * keepRatio);
168
+ const patchSampleLength = 187 * 256; // cut into patchSize chunks so there's no weird jumps in audio
169
+ const numPatchesToKeep = Math.ceil(ratioSampleLength / patchSampleLength);
170
+
171
+ // space patchesToKeep evenly
172
+ const skipSize = Math.floor((audioIn.length - ratioSampleLength) / (numPatchesToKeep - 1));
173
+
174
+ let audioOut = [];
175
+ let startIndex = 0;
176
+ for (let i = 0; i < numPatchesToKeep; i++) {
177
+ let endIndex = startIndex + patchSampleLength;
178
+ let chunk = audioIn.slice(startIndex, endIndex);
179
+ audioOut.push(...chunk);
180
+ startIndex = endIndex + skipSize; // discard even space
181
+ }
182
+
183
+ return Float32Array.from(audioOut);
184
+ }
185
+
186
+ function downsampleArray(audioIn: Float32Array, sampleRateIn: number, sampleRateOut: number) {
187
+ if (sampleRateOut === sampleRateIn) {
188
+ return audioIn;
189
+ }
190
+ let sampleRateRatio = sampleRateIn / sampleRateOut;
191
+ let newLength = Math.round(audioIn.length / sampleRateRatio);
192
+ let result = new Float32Array(newLength);
193
+ let offsetResult = 0;
194
+ let offsetAudioIn = 0;
195
+
196
+ while (offsetResult < result.length) {
197
+ let nextOffsetAudioIn = Math.round((offsetResult + 1) * sampleRateRatio);
198
+ let accum = 0,
199
+ count = 0;
200
+ for (let i = offsetAudioIn; i < nextOffsetAudioIn && i < audioIn.length; i++) {
201
+ // @ts-ignore
202
+ accum += audioIn[i];
203
+ count++;
204
+ }
205
+ result[offsetResult] = accum / count;
206
+ offsetResult++;
207
+ offsetAudioIn = nextOffsetAudioIn;
208
+ }
209
+
210
+ return result;
211
+ }
212
+
213
+ function median(arr: number[]) {
214
+ if (arr.length === 0) return 0;
215
+ const sorted = [...arr].sort((a, b) => a - b);
216
+ const mid = Math.floor(sorted.length / 2);
217
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
218
+ }
219
+
220
+ function estimateTuningFrequency(vectorSignal: any, sampleRate = 16000) {
221
+ // Параметры для pitch-экстракции
222
+ const frameSize = 2048;
223
+ const hopSize = 512;
224
+ const minFreq = 80; // игнорируем басы и шум
225
+ const maxFreq = 1500; // выше — уже не вокал/мелодия
226
+ const confidenceThreshold = 0.7;
227
+ const silenceThreshold = 0.001; // чувствительность к тишине
228
+
229
+ // ✅ Теперь 7 аргументов!
230
+ const pitchResult = essentia.PitchYinFFT(
231
+ vectorSignal,
232
+ sampleRate,
233
+ frameSize,
234
+ hopSize,
235
+ minFreq,
236
+ maxFreq,
237
+ silenceThreshold
238
+ );
239
+
240
+ const centsDeviations = [];
241
+
242
+ for (let i = 0; i < pitchResult.pitch.length; i++) {
243
+ const freq = pitchResult.pitch[i];
244
+ const conf = pitchResult.pitchConfidence[i];
245
+
246
+ // Фильтруем ненадёжные и нерелевантные частоты
247
+ if (freq >= minFreq && freq <= maxFreq && conf >= confidenceThreshold) {
248
+ // MIDI note относительно A4=440 (MIDI 69)
249
+ const midiNote = 69 + 12 * Math.log2(freq / 440);
250
+ const roundedMidi = Math.round(midiNote);
251
+ const cents = (midiNote - roundedMidi) * 100; // отклонение в центах
252
+
253
+ // Ограничиваем выбросы (иногда pitch скачет)
254
+ if (Math.abs(cents) < 50) {
255
+ centsDeviations.push(cents);
256
+ }
257
+ }
258
+ }
259
+
260
+ // Если мало данных — возвращаем 440 по умолчанию
261
+ if (centsDeviations.length < 10) {
262
+ console.warn("Недостаточно надёжных данных для оценки строя. Используется A=440.");
263
+ return 440;
264
+ }
265
+
266
+ const medianCents = median(centsDeviations);
267
+ const estimatedA = 440 * Math.pow(2, medianCents / 1200);
268
+
269
+ // Ограничиваем разумные пределы (обычно A=432–445)
270
+ return Math.max(430, Math.min(450, estimatedA));
271
+ }
272
+
273
+ function preprocess(audioBuffer: AudioBuffer) {
274
+ if (audioBuffer instanceof AudioBuffer) {
275
+ const mono = monomix(audioBuffer);
276
+ // downmix to mono, and downsample to 16kHz sr for essentia tensorflow models
277
+ return downsampleArray(mono, audioBuffer.sampleRate, 16000);
278
+ } else {
279
+ throw new TypeError("Input to audio preprocessing is not of type AudioBuffer");
280
+ }
281
+ }
282
+
283
+ function computeKeyBPM(audioSignal: Float32Array) {
284
+ let vectorSignal = essentia.arrayToVector(audioSignal);
285
+
286
+ // const estimatedA = estimateTuningFrequency(vectorSignal, 16000);
287
+ // console.log("Оценка строя:", estimatedA.toFixed(2), "Гц"); // например: 439.12
288
+
289
+ const keyData = essentia.KeyExtractor(
290
+ vectorSignal,
291
+ true,
292
+ 4096,
293
+ 4096,
294
+ 12,
295
+ 3500,
296
+ 60,
297
+ 25,
298
+ 0.2,
299
+ "bgate",
300
+ 16000,
301
+ 0.0001,
302
+ 440,
303
+ "cosine",
304
+ "hann"
305
+ );
306
+ const bpm = essentia.PercivalBpmEstimator(
307
+ vectorSignal,
308
+ 1024,
309
+ 2048,
310
+ 128,
311
+ 128,
312
+ 210,
313
+ 50,
314
+ 16000
315
+ ).bpm;
316
+
317
+ return {
318
+ keyData: keyData,
319
+ bpm: bpm,
320
+ };
321
+ }
322
+
323
+ function processFile(arrayBuffer: ArrayBuffer) {
324
+ audioCtx.resume().then(() => {
325
+ audioCtx.decodeAudioData(arrayBuffer).then(async function handleDecodedAudio(audioBuffer) {
326
+ const prepocessedAudio = preprocess(audioBuffer);
327
+ await audioCtx.suspend();
328
+
329
+ if (essentia) {
330
+ essentiaAnalysis = computeKeyBPM(prepocessedAudio);
331
+
332
+ keyBpmResults.value = {
333
+ key: essentiaAnalysis.keyData.key,
334
+ scale: essentiaAnalysis.keyData.scale,
335
+ bpm: (essentiaAnalysis.bpm <= 69
336
+ ? essentiaAnalysis.bpm * 2
337
+ : essentiaAnalysis.bpm
338
+ ).toFixed(2),
339
+ };
340
+ }
341
+
342
+ // reduce amount of audio to analyse
343
+ let audioData = shortenAudio(prepocessedAudio, KEEP_PERCENTAGE, true); // <-- TRIMMED start/end
344
+
345
+ // send for feature extraction
346
+ featureExtractionWorker.postMessage(
347
+ {
348
+ audio: audioData.buffer,
349
+ },
350
+ [audioData.buffer]
351
+ );
352
+ // @ts-ignore
353
+ audioData = null;
354
+ });
355
+ });
356
+ }
357
+
358
+ const getKeyMoodAndBpm = async (file: File) => {
359
+ file.arrayBuffer().then((arrayBuffer: ArrayBuffer) => {
360
+ processFile(arrayBuffer);
361
+ });
362
+ };
363
+
364
+ const resetMoodResults = () => {
365
+ moodResults.value.forEach((moodResult) => {
366
+ moodResult.value = 0;
367
+ });
368
+ };
369
+
370
+ return {
371
+ getKeyMoodAndBpm,
372
+ keyBpmResults,
373
+ moodResults,
374
+ resetMoodResults,
375
+ essentia,
376
+ essentiaAnalysis,
377
+ featureExtractionWorker
378
+ };
379
+ };