add option to use Python or JS Tensorflow for ML predictions

This commit is contained in:
Alexandre Storelli 2019-03-21 19:56:23 +01:00
parent 3f2ead5701
commit 103966589e
10 changed files with 557 additions and 77 deletions

View File

@ -23,15 +23,26 @@ In `post-processing.js`, results are gathered for each audio segment and cleaned
A Readable interface, `Analyser`, is exposed to the end user. It streams objects containing the audio itself and all analysis results.
On a regular laptop CPU, computations run at 5-10X for files and at 10-20% usage for live stream.
On a regular laptop CPU and with the Python time-frequency analyser, computations run at 5-10X for files and at 10-20% usage for live stream.
## Getting started
### Installation
As prerequisites, you need:
As mandatory prerequisites, you need:
- Node.js and NPM. This project requires a Node >= v10.12.x. Tested with NPM v6.4.1. If you need to manage several node versions on your platform, you might want to use [NVM](https://github.com/creationix/nvm).
- FFmpeg (tested with v2.6.9). Installation instructions available [here](https://ffmpeg.org/download.html).
- FFmpeg (tested with v2.6.9). [Installation instructions available here](https://ffmpeg.org/download.html).
For best performance (~2x speedup) you should choose to do part of the computations with Python. Additional prerequisites are the following:
- Python (tested with v2.7.9).
- Keras (tested with v2.0.8). Keras installation instructions are available [here](https://keras.io/#installation).
- Tensorflow (tested with `tensorflow` v1.4.0 and `tensorflow-gpu` v1.3.0). Installation instructions are [here](https://www.tensorflow.org/install/).
```bash
pip install keras tensorflow
```
should be enough. If you do not have pip [follow these instructions to install it](https://pip.pypa.io/en/stable/installing/).
Then install this module:
@ -52,7 +63,7 @@ npm test
### Command-line demo
At startup and periodically during runtime, filter configuration files are automatically updated from [adblockradio.com/models/](https://adblockradio.com/models/):
- a compatible machine-learning model (`model.json` and `group1-shard1of1`), for the time-frequency analyser.
- a compatible machine-learning model (`model.keras` or `model.json` + `group1-shard1of1`), for the time-frequency analyser.
- a fingerprint database (`hotlist.sqlite`), for the fingerprint matcher.
#### Live stream analysis
@ -191,6 +202,7 @@ Property|Description|Default
Property|Description|Periodicity|Default
--------|-----------|-----------|-------
`enablePredictorMl`|perform machine learning inference|`predInterval`|`true`
`JSPredictorMl`|use tfjs instead of Python for ML inference (slower)|`false`
`enablePredictorHotlist`|compute audio fingerprints and search them in a DB|`predInterval`|`true`
`saveAudio*`|save stream audio data in segments on hard drive|`saveDuration`|`true`
`saveMetadata`|save a JSON with predictions|`saveDuration`|`true`

253
package-lock.json generated
View File

@ -557,6 +557,15 @@
"resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz",
"integrity": "sha1-DGwfq+KyPRcXPZpht7cJPrnhdp4="
},
"bl": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
"integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
"requires": {
"readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1"
}
},
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
@ -847,6 +856,11 @@
"supports-color": "^5.3.0"
}
},
"chownr": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g=="
},
"chromium-pickle-js": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz",
@ -1181,6 +1195,14 @@
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true
},
"decompress-response": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
"requires": {
"mimic-response": "^1.0.0"
}
},
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@ -1651,7 +1673,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
"integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
"dev": true,
"requires": {
"once": "^1.4.0"
}
@ -1727,6 +1748,11 @@
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true
},
"event-lite": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.2.tgz",
"integrity": "sha512-HnSYx1BsJ87/p6swwzv+2v6B4X+uxUteoDfRxsAb1S1BePzQqOLevVmkdA15GHJVd9A9Ok6wygUR18Hu0YeV9g=="
},
"events": {
"version": "1.1.1",
"resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz",
@ -1783,6 +1809,11 @@
}
}
},
"expand-template": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-1.1.1.tgz",
"integrity": "sha512-cebqLtV8KOZfw0UI8TEFWxtczxxC1jvyUvx6H4fyp1K1FN7A4Q+uggVUlOsI1K8AGU0rwOGqP8nCapdrw8CYQg=="
},
"expand-tilde": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
@ -2070,6 +2101,11 @@
"map-cache": "^0.2.2"
}
},
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
@ -2168,6 +2204,11 @@
"assert-plus": "^1.0.0"
}
},
"github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4="
},
"glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
@ -2387,8 +2428,7 @@
"ieee754": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz",
"integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=",
"dev": true
"integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q="
},
"ignore-walk": {
"version": "3.0.1",
@ -2438,6 +2478,11 @@
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
},
"int64-buffer": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz",
"integrity": "sha1-J3siiofZWtd30HwTgyAiQGpHNCM="
},
"invert-kv": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
@ -3082,6 +3127,11 @@
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
"dev": true
},
"mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@ -3219,6 +3269,17 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
},
"msgpack-lite": {
"version": "0.1.26",
"resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz",
"integrity": "sha1-3TxQsm8FnyXn7e42REGDWOKprYk=",
"requires": {
"event-lite": "^0.1.1",
"ieee754": "^1.1.8",
"int64-buffer": "^0.1.9",
"isarray": "^1.0.0"
}
},
"nan": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
@ -3243,6 +3304,11 @@
"to-regex": "^3.0.1"
}
},
"napi-build-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz",
"integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA=="
},
"needle": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz",
@ -3266,6 +3332,14 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node-abi": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.7.1.tgz",
"integrity": "sha512-OV8Bq1OrPh6z+Y4dqwo05HqrRL9YNF7QVMRfq1/pguwKLG+q9UB/Lk0x5qXjO23JjJg+/jqCHSTaG1P3tfKfuw==",
"requires": {
"semver": "^5.4.1"
}
},
"node-environment-flags": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.4.tgz",
@ -3352,6 +3426,11 @@
}
}
},
"noop-logger": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",
"integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI="
},
"nopt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
@ -3862,6 +3941,36 @@
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
"dev": true
},
"prebuild-install": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.2.1.tgz",
"integrity": "sha512-9DAccsInWHB48TBQi2eJkLPE049JuAI6FjIH0oIrij4bpDVEbX6JvlWRAcAAlUqBHhjgq0jNqA3m3bBXWm9v6w==",
"requires": {
"detect-libc": "^1.0.3",
"expand-template": "^1.0.2",
"github-from-package": "0.0.0",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"napi-build-utils": "^1.0.1",
"node-abi": "^2.2.0",
"noop-logger": "^0.1.1",
"npmlog": "^4.0.1",
"os-homedir": "^1.0.1",
"pump": "^2.0.1",
"rc": "^1.2.7",
"simple-get": "^2.7.0",
"tar-fs": "^1.13.0",
"tunnel-agent": "^0.6.0",
"which-pm-runs": "^1.0.0"
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
}
}
},
"prepend-http": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
@ -3936,6 +4045,15 @@
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
"integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ=="
},
"pump": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
@ -4320,6 +4438,21 @@
"resolved": "https://registry.npmjs.org/signal-windows/-/signal-windows-0.0.1.tgz",
"integrity": "sha1-1JTBv6T8Ycl0IpwwTfogGpIiqAI="
},
"simple-concat": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
},
"simple-get": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz",
"integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==",
"requires": {
"decompress-response": "^3.3.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"single-line-log": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz",
@ -4730,6 +4863,42 @@
}
}
},
"tar-fs": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz",
"integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==",
"requires": {
"chownr": "^1.0.1",
"mkdirp": "^0.5.1",
"pump": "^1.0.0",
"tar-stream": "^1.1.2"
},
"dependencies": {
"pump": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz",
"integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==",
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
}
}
},
"tar-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz",
"integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==",
"requires": {
"bl": "^1.0.0",
"buffer-alloc": "^1.2.0",
"end-of-stream": "^1.0.0",
"fs-constants": "^1.0.0",
"readable-stream": "^2.3.0",
"to-buffer": "^1.1.1",
"xtend": "^4.0.0"
}
},
"temp-file": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.3.2.tgz",
@ -4812,6 +4981,11 @@
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=",
"dev": true
},
"to-buffer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg=="
},
"to-object-path": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
@ -4908,6 +5082,11 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
"underscore": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.3.3.tgz",
"integrity": "sha1-R6xTaD2vgyv6lS4XdEF9pHgXrkI="
},
"union-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
@ -5187,6 +5366,11 @@
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true
},
"which-pm-runs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz",
"integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs="
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
@ -5344,6 +5528,11 @@
"integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=",
"dev": true
},
"xtend": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
@ -5446,6 +5635,64 @@
"requires": {
"fd-slicer": "~1.0.1"
}
},
"zeromq": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/zeromq/-/zeromq-5.1.0.tgz",
"integrity": "sha512-wBEwnWaC1BQgcVoC4ZcERuIf+F0LdW12rblcwFNQANUGjJqQB1Ty3D59+WhvBJU9EliQw9Qc6V914gYXMzO6jw==",
"requires": {
"nan": "^2.10.0",
"prebuild-install": "5.2.1"
}
},
"zerorpc": {
"version": "0.9.8",
"resolved": "https://registry.npmjs.org/zerorpc/-/zerorpc-0.9.8.tgz",
"integrity": "sha512-F3J8mp196uveb0icLL85GszQDHNGuOJT1BoUNvKzo8hetACiLKdXWLw0i/wTB/skg79DZBPA9h0MdmOt772KRQ==",
"requires": {
"msgpack-lite": "^0.1.26",
"underscore": "1.3.3",
"uuid": "^3.0.0",
"zeromq": "^4.6.0"
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
},
"prebuild-install": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.5.3.tgz",
"integrity": "sha512-/rI36cN2g7vDQnKWN8Uzupi++KjyqS9iS+/fpwG4Ea8d0Pip0PQ5bshUNzVwt+/D2MRfhVAplYMMvWLqWrCF/g==",
"requires": {
"detect-libc": "^1.0.3",
"expand-template": "^1.0.2",
"github-from-package": "0.0.0",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"node-abi": "^2.2.0",
"noop-logger": "^0.1.1",
"npmlog": "^4.0.1",
"os-homedir": "^1.0.1",
"pump": "^2.0.1",
"rc": "^1.1.6",
"simple-get": "^2.7.0",
"tar-fs": "^1.13.0",
"tunnel-agent": "^0.6.0",
"which-pm-runs": "^1.0.0"
}
},
"zeromq": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/zeromq/-/zeromq-4.6.0.tgz",
"integrity": "sha512-sU7pQqQj7f/C6orJZAXls+NEKaVMZZtnZqpMPTq5d5dP78CmdC0g15XIviFAN6poPuKl9qlGt74vipOUUuNeWg==",
"requires": {
"nan": "^2.6.2",
"prebuild-install": "^2.2.2"
}
}
}
}
}
}

View File

@ -4,7 +4,7 @@
"description": "Distinguish ads, talk and music in radios",
"main": "post-processing.js",
"scripts": {
"test": "mocha --delay test/file.js && mocha --delay test/online.js && mocha --delay test/offline.js"
"test": "mocha --delay test/file.js && mocha --delay test/file.js --mljs && mocha --delay test/online.js && mocha --delay test/online.js --mljs && mocha --delay test/offline.js && mocha --delay test/offline.js --mljs"
},
"keywords": [
"adblock",
@ -25,7 +25,9 @@
"sqlite3": "^4.0.6",
"stream-audio-fingerprint": "^1.0.4",
"stream-tireless-baler": "^1.0.15",
"tar": "^4.4.8"
"tar": "^4.4.8",
"zeromq": "^5.1.0",
"zerorpc": "^0.9.8"
},
"devDependencies": {
"aws-sdk": "^2.420.0",

View File

@ -341,7 +341,6 @@ class Analyser extends Readable {
}
const defaultModelPath = process.cwd() + '/model';
const defaultModelFile = this.country + '_' + this.name + '/model.json';
const defaultHotlistFile = this.country + '_' + this.name + '/hotlist.sqlite';
// default module options
@ -351,15 +350,18 @@ class Analyser extends Readable {
file: null, // analyse a file instead of a HTTP stream. will not download stream
records: null, // analyse a series of previous records (relative paths). will not download stream
modelPath: defaultModelPath, // directory where ML models and hotlist DBs are stored
modelFile: defaultModelFile, // path of the ML model relative to modelPath
hotlistFile: defaultHotlistFile, // path of the hotlist DB relative to modelPath
modelUpdates: true, // periodically fetch ML and hotlist models and refresh predictors
modelUpdateInterval: 60 // update model files every N minutes
modelUpdateInterval: 60, // update model files every N minutes
JSPredictorMl: false, // whether to use JS (+ native lib) instead of Python for ML. JS is simpler but slower.
}
// optional custom config
Object.assign(this.config, options.config);
const defaultModelFile = this.country + '_' + this.name + '/model.' + (this.config.JSPredictorMl ? 'json' : 'keras');
if (!this.config.modelFile) this.config.modelFile = defaultModelFile; // path of the ML model relative to modelPath
this.postProcessor = new PostProcessor({
country: this.country,
name: this.name,
@ -419,14 +421,19 @@ class Analyser extends Readable {
// download and/or update models at startup
// TODO only download model/hotlist if ML/hotlist is enabled
if (self.config.modelUpdates) {
await checkModelUpdates({
localPath: self.config.modelPath,
files: [
const files = self.config.JSPredictorMl ?
[
{ file: self.config.modelFile, tar: false },
{ file: self.config.modelFile.replace('model.json', 'group1-shard1of1'), tar: false },
{ file: self.config.hotlistFile, tar: true },
]
});
:
[
{ file: self.config.modelFile, tar: true },
{ file: self.config.hotlistFile, tar: true },
]
;
await checkModelUpdates({ localPath: self.config.modelPath, files });
} else {
log.info(self.country + '_' + self.name + ' model updates are disabled');
}
@ -484,14 +491,19 @@ class Analyser extends Readable {
self.modelUpdatesInterval = setInterval(function() {
if (self.config.modelUpdates) {
checkModelUpdates({
localPath: self.config.modelPath,
files: [
const files = self.config.JSPredictorMl ?
[
{ file: self.config.modelFile, tar: false, callback: self.predictor.refreshPredictorMl },
{ file: self.config.modelFile.replace('model.json', 'group1-shard1of1'), tar: false, callback: self.predictor.refreshPredictorMl },
{ file: self.config.hotlistFile, tar: true, callback: self.predictor.refreshPredictorHotlist },
]
});
:
[
{ file: self.config.modelFile, tar: true, callback: self.predictor.refreshPredictorMl },
{ file: self.config.hotlistFile, tar: true, callback: self.predictor.refreshPredictorHotlist },
]
;
checkModelUpdates({ localPath: self.config.modelPath, files });
}
checkMetadataUpdates(self.predictor.refreshMetadata);
}, self.config.modelUpdateInterval * 60000);

View File

@ -150,16 +150,14 @@ class PredictorFile {
});
// start analysis as soon as both ML and hotlist are ready
let hotlistReady = false;
if (this.config.enablePredictorHotlist) this.startPredictorHotlist(function() {
hotlistReady = true;
if ((self.config.enablePredictorMl && mlReady) || !self.config.enablePredictorMl) self.input.resume();
});
let mlReady = false;
if (this.config.enablePredictorMl) this.startPredictorMl(function() {
mlReady = true;
if ((self.config.enablePredictorHotlist && hotlistReady) || !self.config.enablePredictorHotlist) self.input.resume();
Promise.all([
this.config.enablePredictorHotlist && this.startPredictorHotlist(),
this.config.enablePredictorMl && this.startPredictorMl()
]).then(function() {
log.info("hotlist and/or ml loaded");
self.input.resume();
}).catch(function(err) {
log.error(self.country + "_" + self.name + " predictor err=" + err);
});
}
@ -214,31 +212,40 @@ class PredictorFile {
});
}
startPredictorHotlist(callback) {
async startPredictorHotlist() {
if (this.config.enablePredictorHotlist) {
this.hotlist = new Hotlist({
country: this.country,
name: this.name,
fileDB: this.hotlistFile,
callback: callback,
const self = this;
return new Promise(function(resolve, reject) {
self.hotlist = new Hotlist({
country: self.country,
name: self.name,
fileDB: self.hotlistFile,
callback: resolve,
});
});
} else {
this.hotlist = null;
}
}
startPredictorMl(callback) {
async startPredictorMl() {
if (this.config.enablePredictorMl) {
this.mlPredictor = new MlPredictor({
country: this.country,
name: this.name,
modelFile: this.modelFile,
});
const self = this;
return new Promise(function(resolve, reject) {
self.mlPredictor = new MlPredictor({
country: self.country,
name: self.name,
modelFile: self.modelFile,
JSPredictorMl: self.config.JSPredictorMl,
callback: resolve,
});
});
/*const self = this;
(async function() {
await self.mlPredictor.load(self.modelFile);
callback();
})();
})();*/
} else {
this.mlPredictor = null;
}
@ -246,8 +253,8 @@ class PredictorFile {
stopPredictors() {
log.info("close predictor");
if (this.hotlist) this.hotlist.end();
if (this.mlPredictor) this.mlPredictor.end();
if (this.hotlist) this.hotlist.destroy();
if (this.mlPredictor) this.mlPredictor.destroy();
}
}

View File

@ -10,6 +10,8 @@ const { log } = require("abr-log")("pred-ml");
const cp = require("child_process");
const assert = require("assert");
const fs = require("fs-extra");
function parse(msg) {
try {
return JSON.parse(msg);
@ -25,64 +27,245 @@ class MlPredictor extends Writable {
this.canonical = options.country + "_" + options.name;
this.verbose = options.verbose || false;
this.ready = false; // becomes true when ML model is loaded
this.modelFile = options.modelFile;
//this.ready2 = false; // becomes true when audio data is piped to this module. managed externally
//this.finalCallback = null;
this.modelFile = options.modelFile;
//this.readyToCallFinal = false;
this.dataWrittenSinceLastSeg = false;
this.JSPredictorMl = !!options.JSPredictorMl;
this.load = this.load.bind(this);
this.predict = this.predict.bind(this);
const self = this;
(async function() {
await self.load();
if (options.callback) options.callback();
})();
}
async load() {
const self = this;
return new Promise(function(resolve, reject) {
self.child = cp.fork(__dirname + '/ml-worker.js', {
env: {
canonical: self.canonical,
modelFile: self.modelFile,
if (this.JSPredictorMl) { // Javascript MFCC & Tensorflow: tfjs (pure JS) or node-tfjs (native lib and Node bindings)
log.info(this.canonical + " JS predictor");
await new Promise(function(resolve, reject) {
self.child = cp.fork(__dirname + '/ml-worker.js', {
env: {
canonical: self.canonical,
modelFile: self.modelFile,
}
});
self.child.once('message', function(msg) {
msg = parse(msg);
assert.equal(msg.type, 'loading');
if (msg.err) {
log.warn(self.canonical + ' could not load model: ' + JSON.stringify(msg));
return reject();
}
self.ready = msg.loaded;
log.info(self.canonical + ' loaded=' + self.ready);
resolve();
});
});
} else { // Python MFCC & Tensorflow
const isPKG = __dirname.indexOf("/snapshot/") === 0 || __dirname.indexOf("C:\\snapshot\\") === 0; // in a PKG environment (https://github.com/zeit/pkg)
const isElectron = !!(process && process.versions['electron']); // in a Electron environment (https://github.com/electron/electron/issues/2288)
log.info(this.canonical + " Python predictor. __dirname=" + __dirname + " env: PKG=" + isPKG + " Electron=" + isElectron);
if (isPKG) {
this.predictChild = cp.spawn(process.cwd() + "/dist/mlpredict/mlpredict",
[ this.canonical ], { stdio: ['pipe', 'pipe', 'pipe']});
} else if (isElectron) {
const paths = [
"",
"/Adblock Radio Buffer-linux-x64/resources/app"
];
for (let i=0; i<paths.length; i++) {
const path = process.cwd() + paths[i] + "/node_modules/adblockradio/predictor-ml/dist/mlpredict/mlpredict"
try {
await fs.access(path);
log.info("mlpredict found at " + path);
this.predictChild = cp.spawn(path, [ this.canonical ], { stdio: ['pipe', 'pipe', 'pipe']});
break;
} catch (e) {
// pass
}
if (i === paths.length - 1) {
const msg = "Could not locate mlpredict. cwd=" + process.cwd() + " paths=" + JSON.stringify(paths);
log.error(msg);
throw new Error(msg);
}
}
} else {
this.predictChild = cp.spawn('python', [
'-u',
__dirname + '/mlpredict.py',
this.canonical,
], { stdio: ['pipe', 'pipe', 'pipe'] });
}
const zerorpc = require("zerorpc");
// increase default timeouts, otherwise this would fail at model loading on some CPU-bound devices.
// https://github.com/0rpc/zerorpc-node#clients
this.client = new zerorpc.Client({ timeout: 120, heartbeatInterval: 60000 });
this.client.connect("ipc:///tmp/" + this.canonical);
this.client.on("error", function(error) {
log.error(self.canonical + " RPC client error:" + error);
});
this.predictChild.stdout.on('data', function(msg) { // received messages from python worker
const msgS = msg.toString().split("\n");
// sometimes, several lines arrive at once. separate them.
for (let i=0; i<msgS.length; i++) {
if (msgS[i].length > 0) log.debug(msgS[i]);
}
});
self.child.once('message', function(msg) {
msg = parse(msg);
assert.equal(msg.type, 'loading');
if (msg.err) {
log.warn(self.canonical + ' could not load model: ' + JSON.stringify(msg));
return reject();
}
self.ready = msg.loaded;
log.info(self.canonical + ' loaded=' + self.ready);
resolve();
this.predictChild.stderr.on("data", function(msg) {
if (msg.includes("Using TensorFlow backend.")) return;
log.error(self.canonical + " mlpredict child stderr data: " + msg);
});
});
this.predictChild.stdin.on("error", function(err) {
log.warn(self.canonical + " mlpredict child stdin error: " + err);
});
this.predictChild.stdout.on("error", function(err) {
log.warn(self.canonical + " mlpredict child stdout error: " + err);
});
this.predictChild.stderr.on("error", function(err) {
log.warn(self.canonical + " mlpredict child stderr error: " + err);
});
this.predictChild.stdout.on("end", function() {
//log.debug("cp stdout end");
//self.readyToCallFinal = true;
//if (self.finalCallback) self.finalCallback();
});
await new Promise(function(resolve, reject) {
self.client.invoke("load", self.modelFile, function(error, res, more) {
if (error) {
if (error === "model not found") {
log.error(self.canonical + " Keras ML file " + self.modelFile + " not found. Cannot tag audio");
} else {
log.error(error);
// TODO has occasionally thrown:
// "Initializer for variable lstm_1_2/kernel/ is from inside a control-flow construct,
// such as a loop or conditional. When creating a variable inside a loop or conditional,
// use a lambda as the initializer."
//
// but cannot reproduce :/
}
return reject();
}
log.info(self.canonical + " predictor process is ready to crunch audio");
self.ready = true;
return resolve();
});
});
}
}
_write(buf, enc, next) {
if (this.child && this.ready) {
if (this.JSPredictorMl && this.child && this.ready) {
this.child.send(JSON.stringify({
type: 'write',
buf: buf,
}));
} else if (!this.JSPredictorMl && this.client && this.predictChild && this.ready) {
this.dataWrittenSinceLastSeg = true;
const self = this;
this.client.invoke("write", buf, function(err, res, more) {
if (err) {
log.error(self.canonical + " _write client returned error=" + err);
}
});
}
next();
}
predict(callback) {
if (this.child && this.ready) {
const self = this;
if (this.JSPredictorMl && this.child && this.ready) {
this.child.send(JSON.stringify({
type: 'predict',
}));
const self = this;
this.child.once('message', function(msg) {
msg = parse(msg);
assert.equal(msg.type, 'predict');
if (msg.err) log.warn(self.canonical + ' skipped prediction: ' + JSON.stringify(msg));
callback(null, msg.outData);
});
} else if (!this.JSPredictorMl && this.client && this.predictChild && this.ready) {
if (!this.dataWrittenSinceLastSeg) {
//if (this.ready2) log.warn(this.canonical + " skip predict as no data is available for analysis");
return callback();
}
this.dataWrittenSinceLastSeg = false;
this.client.invoke("predict", function(err, res, more) {
if (err) {
log.error(self.canonical + " predict() returned error=" + err);
return callback(err);
}
try {
var results = JSON.parse(res);
//log.debug("results=" + JSON.stringify(results))
//log.debug("perf: nwin=" + results.nwin + " pre=" + results.timings.pre + " tf=" + results.timings.tf + " post=" + results.timings.post + " total=" + results.timings.total);
} catch(e) {
log.error(self.canonical + " could not parse json results: " + e + " original data=|" + res + "|");
return callback(err);
}
let outData = {
type: results.type,
confidence: results.confidence,
softmaxraw: results.softmax.concat([0]), // the last class is about jingles. ML does not detect them.
//date: new Date(stream.lastData.getTime() + Math.round(stream.tBuffer*1000)),
gain: results.rms,
lenPcm: results.lenpcm
}
callback(null, outData);
});
} else {
callback(null);
}
}
_final() {
if (this.child) {
this.child.kill();
log.info(this.canonical + " killed child process.");
if (this.JSPredictorMl) {
if (this.child) {
this.child.kill();
log.info(this.canonical + " killed child process.");
}
} else {
const self = this;
this.client.invoke("exit", function(err, res, more) {
if (err) {
log.error(self.canonical + "_final: exit() returned error=" + err);
}
});
this.client.close();
// if not enough, kill it directly!
this.predictChild.stdin.end();
this.predictChild.kill();
//if (this.readyToCallFinal) return next();
//this.readyToCallFinal = next;
}
}
}

View File

@ -302,17 +302,24 @@ class Predictor {
}
async refreshPredictorMl() {
log.info(this.canonical + " refresh ML predictor");
log.info(this.canonical + " refresh ML predictor (" + (this.config.JSPredictorMl ? "JS" : "Python") + " child process)");
if (this.mlPredictor) {
this.decoder.stdout.unpipe(this.mlPredictor);
this.mlPredictor.destroy();
}
if (this.config.enablePredictorMl && !this.mlPredictor) {
this.mlPredictor = new MlPredictor({
country: this.country,
name: this.name,
modelFile: this.modelFile,
JSPredictorMl: this.config.JSPredictorMl,
});
this.decoder.stdout.pipe(this.mlPredictor);
} else {
this.mlPredictor = null;
}
if (this.config.enablePredictorMl) {
await this.mlPredictor.load();
//if (this.config.enablePredictorMl) {
// await this.mlPredictor.load();
// we pipe decoder into mlPredictor later, once mlPredictor is ready to process data. the flag for this is mlPredictor.ready2
/*const self = this;
this.mlPredictor.ready2 = false;
@ -335,13 +342,13 @@ class Predictor {
}
}, self.config.waitAfterMlModelLoad); // to not overwhelm the CPU in CPU-bound systems
*/
} else {
/*} else {
if (this.mlPredictor) {
if (this.mlPredictor.ready2) this.decoder.stdout.unpipe(this.mlPredictor);
this.mlPredictor.destroy();
this.mlPredictor = null;
}
}
}*/
}
refreshMetadata() {

View File

@ -9,6 +9,8 @@ const TEST_ML = true;
const TEST_HOTLIST = true;
const PRED_INTERVAL = 1; // in seconds
const MLJS = process.argv.includes('--mljs');
if (cluster.isMaster) {
const TIMEOUT = 30000; // this must be at least the length of the audio tested
@ -77,7 +79,7 @@ if (cluster.isMaster) {
run();
});*/
describe('File analysis', function() {
describe('File analysis (' + (MLJS ? 'JS' : 'Python') + ' child process)', function() {
it("should have emitted data", function() {
assert(gotData);
@ -175,6 +177,7 @@ if (cluster.isMaster) {
enablePredictorMl: TEST_ML,
saveMetadata: true,
verbose: false,
JSPredictorMl: MLJS
}
});

View File

@ -10,6 +10,8 @@ const PRED_INTERVAL = 1; // in seconds
const COUNTRY = "France";
const NAME = "RTL";
const MLJS = process.argv.includes('--mljs');
if (cluster.isMaster) {
// first, download some chunks of audio
@ -205,8 +207,8 @@ if (cluster.isMaster) {
}
}
const test = function(origNPredictions) {
describe('Offline analysis', function() {
const test = function() {
describe('Offline analysis (' + (MLJS ? 'JS' : 'Python') + ' child process)', function() {
it("should have emitted an end event", function() {
assert(oaFinished);
@ -284,6 +286,7 @@ if (cluster.isMaster) {
saveMetadata: true,
fetchMetadata: true,
verbose: true,
JSPredictorMl: MLJS,
}
});
@ -321,6 +324,7 @@ if (cluster.isMaster) {
enablePredictorHotlist: true,
enablePredictorMl: true,
modelUpdates: false,
JSPredictorMl: MLJS,
}
});

View File

@ -10,6 +10,8 @@ const TEST_ML = true;
const TEST_HOTLIST = true;
const PRED_INTERVAL = 1; // in seconds
const MLJS = process.argv.includes('--mljs');
if (cluster.isMaster) {
const CLOSE_DELAY = 15000;
@ -89,7 +91,7 @@ if (cluster.isMaster) {
})();
});
describe('Live stream analysis', function() {
describe('Live stream analysis (' + (MLJS ? 'JS' : 'Python') + ' child process)', function() {
it("should have emitted data", function() {
assert(gotData);
@ -192,6 +194,7 @@ if (cluster.isMaster) {
saveMetadata: true,
fetchMetadata: true,
verbose: false,
JSPredictorMl: MLJS,
}
});