diff --git a/index.html b/index.html
index aed433b7..7c09fbf8 100644
--- a/index.html
+++ b/index.html
@@ -433,6 +433,13 @@
Saved Records
GPU
+
+
+
+
?
diff --git a/js/model.js b/js/model.js
index 07042c1e..648ae126 100644
--- a/js/model.js
+++ b/js/model.js
@@ -1,4 +1,5 @@
const tf = require('@tensorflow/tfjs-node');
+require('@tensorflow/tfjs-backend-webgpu');
const fs = require('fs');
const path = require('path');
let DEBUG = false;
@@ -213,9 +214,9 @@ class Model {
normalise(spec) {
return tf.tidy(() => {
- const spec_max = tf.max(spec, [1, 2]).reshape([-1, 1, 1, 1])
+ const spec_max = tf.max(spec, [1, 2], true)
if (this.version === 'v4'){
- const spec_min = tf.min(spec, [1, 2]).reshape([-1, 1, 1, 1])
+ const spec_min = tf.min(spec, [1, 2], true)
spec = tf.sub(spec, spec_min).div(tf.sub(spec_max, spec_min));
} else {
spec = spec.mul(255);
diff --git a/js/spectrogram.js b/js/spectrogram.js
new file mode 100644
index 00000000..19ab2934
--- /dev/null
+++ b/js/spectrogram.js
@@ -0,0 +1,113 @@
+const tf = require('@tensorflow/tfjs-node');
+const DEBUG = false;
+class PreprocessSpectrogramLayer extends tf.layers.Layer {
+ constructor(config) {
+ super(config);
+ this.imgHeight = config.imgHeight;
+ this.imgWidth = config.imgWidth;
+ this.version = config.version;
+ }
+
+ call(inputs) {
+ return tf.tidy(() => {
+ const spec_max = tf.max(inputs, [1, 2], true);
+ if (this.version === 'v4') {
+ const spec_min = tf.min(inputs, [1, 2], true);
+ const normalized = tf.div(tf.sub(inputs, spec_min), tf.sub(spec_max, spec_min));
+ return normalized;
+ } else {
+ const scaled = tf.mul(inputs, 255).div(spec_max);
+ return scaled;
+ }
+ });
+ }
+
+
+ build(inputShape) {
+ this.inputSpec = [{ shape: [null, inputShape[1], inputShape[2], inputShape[3]] }];
+ return this;
+ }
+
+ static get className() {
+ return 'PreprocessSpectrogramLayer';
+ }
+}
+let preprocessLayer
+
+onmessage = async (e) => {
+ const message = e.data.message;
+
+ if (message === 'load'){
+ const backend = e.data.backend;
+ tf.setBackend(backend).then(async () => {
+ if (backend === 'webgl') {
+ tf.env().set('WEBGL_FORCE_F16_TEXTURES', true);
+ tf.env().set('WEBGL_PACK', true);
+ tf.env().set('WEBGL_EXP_CONV', true);
+ tf.env().set('TOPK_K_CPU_HANDOFF_THRESHOLD', 128)
+ tf.env().set('TOPK_LAST_DIM_CPU_HANDOFF_SIZE_THRESHOLD', 0);
+ }
+ tf.enableProdMode();
+ if (DEBUG) {
+ console.log(tf.env());
+ console.log(tf.env().getFlags());
+ }
+ const config = e.data.config;
+ preprocessLayer = new PreprocessSpectrogramLayer(config);
+ console.log('Layer loaded')
+ })
+
+ } else {
+ let {audio, start,fileStart, file, snr, worker, threshold, confidence} = e.data.payload;
+ if (DEBUG) console.log('predictCunk begin', tf.memory().numTensors);
+ audio = tf.tensor1d(audio);
+
+ // check if we need to pad
+ const remainder = audio.shape % 72000;
+ let paddedBuffer;
+ if (remainder !== 0) {
+ // Pad to the nearest full sample
+ paddedBuffer = audio.pad([[0, 72000 - remainder]]);
+ audio.dispose();
+ if (DEBUG) console.log('Received final chunks')
+ }
+ const buffer = paddedBuffer || audio;
+ const numSamples = buffer.shape / 72000;
+ let bufferList = tf.split(buffer, numSamples);
+ buffer.dispose();
+ // Turn the audio into a spec tensor
+ // bufferList = tf.tidy(() => {
+ // return bufferList.map(x => {
+ // return this.version === 'v4' ? this.makeSpectrogram(x) : this.makeSpectrogram(this.normalise_audio(x));
+ // })
+ // });
+
+ const specBatch = makeSpectrogramBatch(bufferList);
+ //const specBatch = tf.stack(bufferList);
+ const batchKeys = [...Array(numSamples).keys()].map(i => start + 72000 * i);
+ postMessage({
+ message: 'specs',
+ specBatch: specBatch.arraySync(),
+ batchKeys: batchKeys,
+ threshold: threshold,
+ confidence: confidence,
+ file: file,
+ fileStart: fileStart,
+ worker: worker
+ })
+ specBatch.dispose()
+ }
+}
+
+function makeSpectrogramBatch(signalBatch) {
+ return tf.tidy(() => {
+ const specBatch = signalBatch.map(signal => {
+ // const sigMax = tf.max(signal);
+ // const sigMin = tf.min(signal);
+ // const range = sigMax.sub(sigMin);
+ // const normalizedSignal = signal.sub(sigMin).div(range).mul(2).sub(1);
+ return tf.abs(tf.signal.stft(signal, 512, 186));
+ });
+ return tf.stack(specBatch);
+ });
+}
\ No newline at end of file
diff --git a/js/ui.js b/js/ui.js
index 868fbf4c..dd1b27a0 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -1327,6 +1327,7 @@ window.onload = async () => {
warmup: true,
backend: 'tensorflow',
tensorflow: { threads: diagnostics['Cores'], batchSize: 32 },
+ webgpu: { threads: 2, batchSize: 32 },
webgl: { threads: 2, batchSize: 32 },
audio: { format: 'mp3', bitrate: 192, quality: 5, downmix: false, padding: false, fade: false },
limit: 500,
diff --git a/package.json b/package.json
index 9d05cc20..30a6c04a 100644
--- a/package.json
+++ b/package.json
@@ -175,7 +175,6 @@
"homepage": "https://github.com/mattk70/Chirpity-Electron#readme",
"devDependencies": {
"@playwright/test": "^1.39.0",
- "@tensorflow/tfjs-converter": "4.10.0",
"electron": "^25.3.0",
"electron-builder": "24.6.4",
"electron-playwright-helpers": "^1.6.0",
@@ -186,6 +185,7 @@
"dependencies": {
"@fast-csv/format": "^4.3.5",
"@popperjs/core": "^2.9.2",
+ "@tensorflow/tfjs-backend-webgpu": "^4.12.0",
"@tensorflow/tfjs-node": "^4.12.0",
"axios": "1.5.1",
"bootstrap": "5.2.2",