diff --git a/.eslintrc.js b/.eslintrc.js index 7ae4ccac1..57b37b8c0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,7 +13,9 @@ module.exports = { env: { browser: true, jasmine: true, + "jest/globals": true, }, + plugins: ["jest"], overrides: [ { files: ["examples/**"], diff --git a/package-lock.json b/package-lock.json index b1607fe59..a240a11b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ml5", - "version": "0.11.0", + "version": "0.11.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3126,6 +3126,133 @@ "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", "dev": true }, + "@typescript-eslint/scope-manager": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.10.2.tgz", + "integrity": "sha512-39Tm6f4RoZoVUWBYr3ekS75TYgpr5Y+X0xLZxXqcZNDWZdJdYbKd3q2IR4V9y5NxxiPu/jxJ8XP7EgHiEQtFnw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.10.2", + "@typescript-eslint/visitor-keys": "5.10.2" + } + }, + "@typescript-eslint/types": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.10.2.tgz", + "integrity": "sha512-Qfp0qk/5j2Rz3p3/WhWgu4S1JtMcPgFLnmAKAW061uXxKSa7VWKZsDXVaMXh2N60CX9h6YLaBoy9PJAfCOjk3w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.2.tgz", + "integrity": "sha512-WHHw6a9vvZls6JkTgGljwCsMkv8wu8XU8WaYKeYhxhWXH/atZeiMW6uDFPLZOvzNOGmuSMvHtZKd6AuC8PrwKQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.10.2", + "@typescript-eslint/visitor-keys": "5.10.2", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + } + } + }, + "@typescript-eslint/utils": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.10.2.tgz", + "integrity": "sha512-vuJaBeig1NnBRkf7q9tgMLREiYD7zsMrsN1DA3wcoMDvr3BTFiIpKjGiYZoKPllfEwN7spUjv7ZqD+JhbVjEPg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.10.2", + "@typescript-eslint/types": "5.10.2", + "@typescript-eslint/typescript-estree": "5.10.2", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.2.tgz", + "integrity": "sha512-zHIhYGGGrFJvvyfwHk5M08C5B5K4bewkm+rrvNTKk1/S15YHR+SA/QUF8ZWscXSfEaB8Nn2puZj+iHcoxVOD/Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.10.2", + "eslint-visitor-keys": "^3.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", + "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", + "dev": true + } + } + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -8896,6 +9023,15 @@ } } }, + "eslint-plugin-jest": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.0.0.tgz", + "integrity": "sha512-Fvs0YgJ/nw9FTrnqTuMGVrkozkd07jkQzWm0ajqyHlfcsdkxGfAuv30fgfWHOnHiCr9+1YQ365CcDX7vrNhqQg==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "^5.10.0" + } + }, "eslint-restricted-globals": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz", @@ -9025,6 +9161,23 @@ } } }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, "eslint-visitor-keys": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", @@ -22391,6 +22544,23 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index bb0851556..49ec6e938 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "eslint-loader": "2.0.0", "eslint-nibble": "^5.1.0", "eslint-plugin-import": "2.9.0", + "eslint-plugin-jest": "^26.0.0", "extract-text-webpack-plugin": "4.0.0-beta.0", "ghooks": "2.0.2", "html-webpack-plugin": "^3.0.7", @@ -118,4 +119,4 @@ "face-api.js": "~0.22.2", "onchange": "^6.1.0" } -} \ No newline at end of file +} diff --git a/src/BodyPix/__tests__/BodyPix.test.js b/src/BodyPix/__tests__/BodyPix.test.js index 6bb260026..66a5e817c 100644 --- a/src/BodyPix/__tests__/BodyPix.test.js +++ b/src/BodyPix/__tests__/BodyPix.test.js @@ -1,10 +1,10 @@ +/* eslint-disable no-use-before-define */ // Copyright (c) 2018 ml5 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT -import bodyPix from "../index"; -import * as tf from "@tensorflow/tfjs"; import * as tfBodyPix from "@tensorflow-models/body-pix"; +import bodyPix from "../index"; jest.mock("@tensorflow-models/body-pix"); @@ -14,6 +14,70 @@ const BODYPIX_DEFAULTS = { segmentationThreshold: 0.5, }; +const mockBodyPix = { + mobileNet: { + predict: jest.fn(), + convToOutput: jest.fn(), + dispose: jest.fn(), + }, + predictForSegmentation: jest.fn(), + predictForPartMap: jest.fn(), + estimatePersonSegmentationActivation: jest.fn(), + estimatePersonSegmentation: jest.fn(), + estimatePartSegmentationActivation: jest.fn(), + estimatePartSegmentation: jest.fn(), + dispose: jest.fn(), +}; + +describe("bodyPix", () => { + let bp; + + beforeAll(async () => { + tfBodyPix.load = jest.fn().mockResolvedValue(mockBodyPix); + bp = await bodyPix(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + }); + + it("Should create bodyPix with all the defaults", async () => { + expect(bp.config.multiplier).toBe(BODYPIX_DEFAULTS.multiplier); + expect(bp.config.outputStride).toBe(BODYPIX_DEFAULTS.outputStride); + expect(bp.config.segmentationThreshold).toBe(BODYPIX_DEFAULTS.segmentationThreshold); + }); + + it("segment calls segmentInternal and returns", async () => { + bp.segmentInternal = jest.fn().mockResolvedValue({ + segmentation: { + data: [], + width: 200, + height: 50, + }, + }); + const img = await getImageData(); + const results = await bp.segment(img); + expect(bp.segmentInternal).toHaveBeenCalledTimes(1); + expect(results.segmentation.width).toBe(200); + expect(results.segmentation.height).toBe(50); + }); + + it("segmentWithParts calls segmentWithPartsInternal and returns", async () => { + bp.segmentWithPartsInternal = jest.fn().mockResolvedValue({ + segmentation: { + data: [], + width: 200, + height: 50, + }, + }); + const img = await getImageData(); + const results = await bp.segmentWithParts(img); + expect(bp.segmentWithPartsInternal).toHaveBeenCalledTimes(1); + expect(results.segmentation.width).toBe(200); + expect(results.segmentation.height).toBe(50); + }); +}); + async function getImage() { const img = new Image(); img.crossOrigin = true; @@ -49,43 +113,3 @@ async function getImageData() { const img = new ImageData(arr, 200); return img; } - -describe("bodyPix", () => { - let bp; - - beforeAll(async () => { - tfBodyPix.load = jest.fn().mockResolvedValue({}); - bp = await bodyPix(); - }); - - it("Should create bodyPix with all the defaults", async () => { - console.log(bp); - expect(bp.config.multiplier).toBe(BODYPIX_DEFAULTS.multiplier); - expect(bp.config.outputStride).toBe(BODYPIX_DEFAULTS.outputStride); - expect(bp.config.segmentationThreshold).toBe(BODYPIX_DEFAULTS.segmentationThreshold); - }); - - // it("segment takes ImageData", async () => { - // const img = await getImageData(); - // const results = await bp.segment(img); - // // 200 * 50 == 10,000 * 4 == 40,000 the size of the array - // expect(results.segmentation.width).toBe(200); - // expect(results.segmentation.height).toBe(50); - // }); - - // describe("segmentation", () => { - // it("Should segment an image of a Harriet Tubman with a width and height of 128", async () => { - // const img = await getImage(); - // await bp.segment(img).then(results => { - // expect(results.segmentation.width).toBe(128); - // expect(results.segmentation.height).toBe(128); - - // expect(results.segmentation.width).toBe(128); - // expect(results.segmentation.height).toBe(128); - - // expect(results.segmentation.width).toBe(128); - // expect(results.segmentation.height).toBe(128); - // }); - // }); - // }); -}); diff --git a/src/BodyPix/index.js b/src/BodyPix/index.js index 1433485ba..5307cc3e2 100644 --- a/src/BodyPix/index.js +++ b/src/BodyPix/index.js @@ -11,6 +11,7 @@ * Ported and integrated from all the hard work by: https://github.com/tensorflow/tfjs-models/tree/master/body-pix */ +// @ts-check import * as tf from '@tensorflow/tfjs'; import * as bp from '@tensorflow-models/body-pix'; import callCallback from '../utils/callcallback'; @@ -29,8 +30,14 @@ class BodyPix { /** * Create BodyPix. * @param {HTMLVideoElement} video - An HTMLVideoElement. - * @param {object} options - An object with options. - * @param {function} callback - A callback to be called when the model is ready. + * @param {{ + * multiplier: Number; + * outputStride: Number; + * segmentationThreshold: Number; + * palette: Object; + * returnTensors: Boolean; + * }} options - An object with options. + * @param {Function} callback - A callback to be called when the model is ready. */ constructor(video, options, callback) { this.video = video; @@ -50,7 +57,7 @@ class BodyPix { /** * Load the model and set it to this.model - * @return {this} the BodyPix model. + * @return {Promise} the BodyPix model. */ async loadModel() { this.model = await bp.load(this.config.multiplier); @@ -60,7 +67,7 @@ class BodyPix { /** * Returns an rgb array - * @param {Object} a p5.Color obj + * @param {Object} p5ColorObj - a p5.Color obj * @return {Array} an [r,g,b] array */ /* eslint class-methods-use-this: "off" */ @@ -83,7 +90,7 @@ class BodyPix { /** * Returns a bodyPartsSpec object - * @param {Array} an array of [r,g,b] colors + * @param {Array} colorOptions - an array of [r,g,b] colors * @return {object} an object with the bodyParts by color and id */ /* eslint class-methods-use-this: "off" */ @@ -106,11 +113,11 @@ class BodyPix { /** * Segments the image with partSegmentation, return result object - * @param {HTMLImageElement | HTMLCanvasElement | object | function | number} imageToSegment - + * @param {HTMLImageElement | HTMLCanvasElement | object | function | number} imgToSegment - * takes any of the following params * @param {object} segmentationOptions - config params for the segmentation * includes outputStride, segmentationThreshold - * @return {Object} a result object with image, raw, bodyParts + * @return {Promise} a result object with image, raw, bodyParts */ async segmentWithPartsInternal(imgToSegment, segmentationOptions) { // estimatePartSegmentation @@ -219,7 +226,7 @@ class BodyPix { * @param {object} configOrCallback - config params for the segmentation * includes palette, outputStride, segmentationThreshold * @param {function} cb - a callback function that handles the results of the function. - * @return {function} a promise or the results of a given callback, cb. + * @return {Promise} a promise or the results of a given callback, cb. */ async segmentWithParts(optionsOrCallback, configOrCallback, cb) { let imgToSegment = this.video; @@ -267,11 +274,11 @@ class BodyPix { /** * Segments the image with personSegmentation, return result object - * @param {HTMLImageElement | HTMLCanvasElement | object | function | number} imageToSegment - + * @param {HTMLImageElement | HTMLCanvasElement | object | function | number} imgToSegment - * takes any of the following params * @param {object} segmentationOptions - config params for the segmentation * includes outputStride, segmentationThreshold - * @return {Object} a result object with maskBackground, maskPerson, raw + * @return {Promise} a result object with maskBackground, maskPerson, raw */ async segmentInternal(imgToSegment, segmentationOptions) { @@ -371,12 +378,12 @@ class BodyPix { /** * Segments the image with personSegmentation - * @param {HTMLImageElement | HTMLCanvasElement | object | function | number} optionsOrCallback - + * @param {HTMLVideoElement | HTMLImageElement | HTMLCanvasElement | object | function | number} optionsOrCallback - * takes any of the following params * @param {object} configOrCallback - config params for the segmentation * includes outputStride, segmentationThreshold * @param {function} cb - a callback function that handles the results of the function. - * @return {function} a promise or the results of a given callback, cb. + * @return {Promise} a promise or the results of a given callback, cb. */ async segment(optionsOrCallback, configOrCallback, cb) { let imgToSegment = this.video; @@ -423,6 +430,13 @@ class BodyPix { } +/** + * + * @param {Object | Function} videoOrOptionsOrCallback + * @param {Object | Function} optionsOrCallback + * @param {Function} cb + * @returns {Promise | Function} + */ const bodyPix = (videoOrOptionsOrCallback, optionsOrCallback, cb) => { let video; let options = {}; diff --git a/src/CharRNN/index_test.js b/src/CharRNN/__tests__/CharRNN.test.js similarity index 57% rename from src/CharRNN/index_test.js rename to src/CharRNN/__tests__/CharRNN.test.js index 915b1fccd..3165bcf82 100644 --- a/src/CharRNN/index_test.js +++ b/src/CharRNN/__tests__/CharRNN.test.js @@ -3,34 +3,37 @@ // This software is released under the MIT License. // https://opensource.org/licenses/MIT -const { charRNN } = ml5; +// TODO: Mock network requests -- local test run took roughly ~8 - ~12.2s!! -const RNN_MODEL_URL = 'https://raw.githubusercontent.com/ml5js/ml5-data-and-models/master/models/lstm/woolf'; +import charRNN from "../index"; + +const RNN_MODEL_URL = + "https://raw.githubusercontent.com/ml5js/ml5-data-and-models/master/models/lstm/woolf"; const RNN_MODEL_DEFAULTS = { cellsAmount: 2, - vocabSize: 223 + vocabSize: 223, }; const RNN_DEFAULTS = { - seed: 'a', + seed: "a", length: 20, temperature: 0.5, - stateful: false -} + stateful: false, +}; const RNN_OPTIONS = { - seed: 'the meaning of pizza is: ', + seed: "the meaning of pizza is: ", length: 10, - temperature: 0.7 -} + temperature: 0.7, +}; -describe('charRnn', () => { +describe("charRnn", () => { let rnn; beforeAll(async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000; // set extra long interval due to issues with CharRNN generation time - rnn = await charRNN(RNN_MODEL_URL, undefined); + jest.testTimeout = 20000; // set extra long interval due to issues with CharRNN generation time + rnn = await charRNN(RNN_MODEL_URL, undefined); }); // it('loads the model with all the defaults', async () => { @@ -38,21 +41,21 @@ describe('charRnn', () => { // expect(rnn.vocabSize).toBe(RNN_MODEL_DEFAULTS.vocabSize); // }); - describe('generate', () => { - it('instantiates an rnn with all the defaults', async () => { + describe("generate", () => { + it("instantiates an rnn with all the defaults", async () => { expect(rnn.ready).toBeTruthy(); expect(rnn.defaults.seed).toBe(RNN_DEFAULTS.seed); expect(rnn.defaults.length).toBe(RNN_DEFAULTS.length); expect(rnn.defaults.temperature).toBe(RNN_DEFAULTS.temperature); expect(rnn.defaults.stateful).toBe(RNN_DEFAULTS.stateful); }); - - it('Should generate content that follows default options if given an empty object', async() => { + + it("Should generate content that follows default options if given an empty object", async () => { const result = await rnn.generate({}); expect(result.sample.length).toBe(20); }); - it('generates content that follows the set options', async() => { + it("generates content that follows the set options", async () => { const result = await rnn.generate(RNN_OPTIONS); expect(result.sample.length).toBe(RNN_OPTIONS.length); });