├── logo.png ├── .babelrc ├── tests ├── assets │ ├── tiny-image.png │ └── test-data.js ├── unit │ ├── helpers.js │ ├── invalid-response-unit-tests.js │ ├── model-versions-unit-tests.js │ ├── search-concepts-unit-tests.js │ ├── search-models-unit-tests.js │ ├── concepts-unit-tests.js │ ├── search-inputs-unit-tests.js │ ├── input-unit-tests.js │ └── models-unit-tests.js └── integration │ ├── api-key-int-tests.js │ ├── helpers.js │ ├── concepts-int-tests.js │ ├── workflows-int-tests.js │ ├── delete-int-tests.js │ ├── inputs-search-int-tests.js │ └── models-int-tests.js ├── examples ├── package.json └── index.js ├── .npmignore ├── .gitignore ├── src ├── solutions │ ├── Solutions.js │ └── Moderation.js ├── Concept.js ├── ModelVersion.js ├── Region.js ├── Regions.js ├── helpers.js ├── index.js ├── constants.js ├── Workflows.js ├── Workflow.js ├── Input.js ├── App.js ├── Concepts.js ├── utils.js ├── Model.js ├── Inputs.js └── Models.js ├── LICENSE ├── .eslintrc ├── CONTRIBUTING.md ├── package.json ├── CHANGELOG.md ├── .travis.yml ├── README.md ├── scripts └── app_and_key_for_tests.py └── gulpfile.js /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clarifai/clarifai-javascript/HEAD/logo.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "sourceMaps": false 4 | } 5 | -------------------------------------------------------------------------------- /tests/assets/tiny-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clarifai/clarifai-javascript/HEAD/tests/assets/tiny-image.png -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clarifai-examples", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "license": "Apache-2.0" 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | sdk 2 | .DS_Store 3 | aws.json 4 | npm-debug.log 5 | .awspublish* 6 | out/* 7 | docs 8 | node_modules/* 9 | examples/node_modules 10 | .babelrc 11 | -------------------------------------------------------------------------------- /tests/unit/helpers.js: -------------------------------------------------------------------------------- 1 | const BASE_URL = 'https://api.clarifai.com'; 2 | const SAMPLE_API_KEY = 'some-sample-API-key'; 3 | 4 | module.exports = {BASE_URL, SAMPLE_API_KEY}; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | node_modules/* 3 | examples/node_modules 4 | .DS_Store 5 | aws.json 6 | npm-debug.log 7 | .awspublish* 8 | out/* 9 | docs 10 | .idea 11 | sdk 12 | dist 13 | package-lock.json 14 | -------------------------------------------------------------------------------- /src/solutions/Solutions.js: -------------------------------------------------------------------------------- 1 | let Moderation = require('./Moderation'); 2 | 3 | class Solutions { 4 | 5 | constructor(_config) { 6 | this.moderation = new Moderation(_config); 7 | } 8 | } 9 | 10 | module.exports = Solutions; 11 | -------------------------------------------------------------------------------- /src/Concept.js: -------------------------------------------------------------------------------- 1 | /** 2 | * class representing a concept and its info 3 | * @class 4 | */ 5 | class Concept { 6 | constructor(_config, data) { 7 | this.id = data.id; 8 | this.name = data.name; 9 | this.createdAt = data.created_at || data.createdAt; 10 | this.appId = data.app_id || data.appId; 11 | this.value = data.value || null; 12 | this._config = _config; 13 | this.rawData = data; 14 | } 15 | } 16 | ; 17 | 18 | module.exports = Concept; 19 | -------------------------------------------------------------------------------- /src/ModelVersion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * class representing a version of a model 3 | * @class 4 | */ 5 | class ModelVersion { 6 | constructor(_config, data) { 7 | this.id = data.id; 8 | this.created_at = this.createdAt = data.created_at || data.createdAt; 9 | this.status = data.status; 10 | this.active_concept_count = data.active_concept_count; 11 | this.metrics = data.metrics; 12 | this._config = _config; 13 | this.rawData = data; 14 | } 15 | } 16 | ; 17 | 18 | module.exports = ModelVersion; 19 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | const Clarifai = process.env.TRAVIS ? require('clarifai') : require('../src'); 2 | 3 | const clarifai = new Clarifai.App({ 4 | apiKey: process.env.CLARIFAI_API_KEY 5 | }); 6 | 7 | function log(d) { 8 | try { 9 | console.log(JSON.stringify(d, null, 2)); 10 | } catch (e) { 11 | console.log(d); 12 | } 13 | } 14 | 15 | // Prediction on general model using video API 16 | clarifai.models.predict(Clarifai.GENERAL_MODEL, 'https://samples.clarifai.com/3o6gb3kkXfLvdKEZs4.gif', {video: true}) 17 | .then(log) 18 | .catch(log); 19 | -------------------------------------------------------------------------------- /src/Region.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Region / bounding box. Region points are percentages from the edge. 3 | * E.g. top of 0.2 means the cropped input will start 20% down from the original edge. 4 | * @class 5 | */ 6 | class Region { 7 | constructor(_config, data) { 8 | this.id = data.id; 9 | this.top = data.region_info.bounding_box.top_row; 10 | this.left = data.region_info.bounding_box.left_col; 11 | this.bottom = data.region_info.bounding_box.bottom_row; 12 | this.right = data.region_info.bounding_box.right_col; 13 | } 14 | } 15 | 16 | module.exports = Region; 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Clarifai, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/Regions.js: -------------------------------------------------------------------------------- 1 | let Region = require('./Region'); 2 | 3 | /** 4 | * A collection of regions. 5 | * @class 6 | */ 7 | class Regions { 8 | constructor(_config, rawData = []) { 9 | this._config = _config; 10 | this.rawData = rawData; 11 | rawData.forEach((regionData, index) => { 12 | this[index] = new Region(this._config, regionData); 13 | }); 14 | this.length = rawData.length; 15 | } 16 | 17 | [Symbol.iterator]() { 18 | let index = -1; 19 | return { 20 | next: () => ({ value: this[++index], done: index >= this.length }) 21 | }; 22 | }; 23 | } 24 | 25 | module.exports = Regions; 26 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "spaced-comment": [2, "always"], 5 | "semi": [2, "always"], 6 | "curly": [2, "all"], 7 | "no-else-return": 2, 8 | "no-unreachable": 2, 9 | "no-return-assign": 2, 10 | "indent": [2, 2], 11 | "no-unused-vars": [2, {"vars": "all", "args": "none"}], 12 | "key-spacing": [2, {"afterColon": true}], 13 | "quotes": [2, "single"], 14 | "camelcase": [2, {"properties": "never"}], 15 | "new-cap": 2, 16 | "no-const-assign": 2, 17 | "eqeqeq": 2, 18 | "no-multi-str": 2, 19 | "react/display-name": 0, 20 | "react/prop-types": 0, 21 | "react/no-did-mount-set-state": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | const SUCCESS_CODES = [200, 201]; 2 | 3 | module.exports = { 4 | isSuccess: (response) => { 5 | return SUCCESS_CODES.indexOf(response.status) > -1; 6 | }, 7 | deleteEmpty: (obj, strict = false) => { 8 | Object.keys(obj).forEach((key) => { 9 | if (obj[key] === null || 10 | obj[key] === undefined || 11 | strict === true && ( 12 | obj[key] === '' || 13 | obj[key].length === 0 || 14 | Object.keys(obj[key]).length === 0)) { 15 | delete obj[key]; 16 | } 17 | }); 18 | }, 19 | clone: (obj) => { 20 | let keys = Object.keys(obj); 21 | let copy = {}; 22 | keys.forEach((k) => { 23 | copy[k] = obj[k]; 24 | }); 25 | return copy; 26 | }, 27 | checkType: (regex, val) => { 28 | if ((regex instanceof RegExp) === false) { 29 | regex = new RegExp(regex); 30 | } 31 | return regex.test(Object.prototype.toString.call(val)); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /tests/assets/test-data.js: -------------------------------------------------------------------------------- 1 | const TINY_IMAGE_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg=='; 2 | 3 | const sampleImages = [ 4 | 'https://s3.amazonaws.com/samples.clarifai.com/metro-north.jpg', 5 | 'https://s3.amazonaws.com/samples.clarifai.com/wedding.jpg', 6 | 'https://s3.amazonaws.com/samples.clarifai.com/cookies.jpeg', 7 | 'https://s3.amazonaws.com/samples.clarifai.com/beer.jpeg', 8 | 'https://s3.amazonaws.com/samples.clarifai.com/dog.tiff', 9 | 'https://s3.amazonaws.com/samples.clarifai.com/red-car-1.png', 10 | 'https://s3.amazonaws.com/samples.clarifai.com/red-car-2.jpeg', 11 | 'https://s3.amazonaws.com/samples.clarifai.com/red-truck.png', 12 | 'https://s3.amazonaws.com/samples.clarifai.com/black-car.jpg' 13 | ]; 14 | 15 | const sampleVideos = [ 16 | 'https://samples.clarifai.com/3o6gb3kkXfLvdKEZs4.gif', 17 | 'https://samples.clarifai.com/beer.mp4' 18 | ]; 19 | 20 | module.exports = {sampleImages, sampleVideos, TINY_IMAGE_BASE64}; 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Building 2 | 3 | 1. Clone this repo 4 | 2. `npm i` 5 | 3. `npm run build` 6 | 7 | This will create three folders: 8 | 9 | - `dist`: compiled source files, suitable for the server 10 | - `sdk`: bundled and minified versions of the library, suitable for browser environments 11 | - `docs` documentation generated from source code 12 | 13 | ## Development 14 | 15 | #### Helpful development tasks 16 | 17 | * `npm run watch` - this will do an initial build, then build on any changes to *src* 18 | * `npm run unittest` - run tests in the */tests/unit* folder 19 | * `npm run test` - run tests in */tests/integration* AND */tests/unit* folders 20 | * tests require the following environment variables to be set: 21 | * `CLARIFAI_API_KEY` 22 | * `npm run clean` - empty and remove the folders created on build 23 | 24 | #### Command line optional params 25 | 26 | * `--stage` - if set will build with the env vars found in `gulpfile.js`. Possible values are: `dev` 27 | (default), `test`, `staging`, `prod` 28 | 29 | #### JSDocs 30 | 31 | To compile docs, run `npm run jsdocs` 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | let App = require('./App'); 2 | let {version} = require('./../package.json'); 3 | 4 | module.exports = global.Clarifai = { 5 | version, 6 | App, 7 | GENERAL_MODEL: 'aaa03c23b3724a16a56b629203edc62c', 8 | FOOD_MODEL: 'bd367be194cf45149e75f01d59f77ba7', 9 | TRAVEL_MODEL: 'eee28c313d69466f836ab83287a54ed9', 10 | NSFW_MODEL: 'e9576d86d2004ed1a38ba0cf39ecb4b1', 11 | WEDDINGS_MODEL: 'c386b7a870114f4a87477c0824499348', 12 | WEDDING_MODEL: 'c386b7a870114f4a87477c0824499348', 13 | COLOR_MODEL: 'eeed0b6733a644cea07cf4c60f87ebb7', 14 | CLUSTER_MODEL: 'cccbe437d6e54e2bb911c6aa292fb072', 15 | FACE_DETECT_MODEL: '53e1df302c079b3db8a0a36033ed2d15', 16 | LOGO_MODEL: 'c443119bf2ed4da98487520d01a0b1e3', 17 | DEMOGRAPHICS_MODEL: 'c0c0ac362b03416da06ab3fa36fb58e3', 18 | GENERAL_EMBED_MODEL: 'bbb5f41425b8468d9b7a554ff10f8581', 19 | FACE_EMBED_MODEL: 'e15d0f873e66047e579f90cf82c9882z', 20 | APPAREL_MODEL: 'e0be3b9d6a454f0493ac3a30784001ff', 21 | MODERATION_MODEL: 'd16f390eb32cad478c7ae150069bd2c6', 22 | TEXTURES_AND_PATTERNS: 'fbefb47f9fdb410e8ce14f24f54b47ff', 23 | LANDSCAPE_QUALITY: 'bec14810deb94c40a05f1f0eb3c91403', 24 | PORTRAIT_QUALITY: 'de9bd05cfdbf4534af151beb2a5d0953', 25 | CELEBRITY_MODEL: 'e466caa0619f444ab97497640cefc4dc' 26 | }; 27 | -------------------------------------------------------------------------------- /tests/integration/api-key-int-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {errorHandler} = require('./helpers'); 3 | const {sampleImages} = require('../assets/test-data'); 4 | 5 | describe('Integration Tests - API key', () => { 6 | it('can initialize an app with an api key', done => { 7 | expect(process.env.CLARIFAI_API_KEY).toBeDefined(); 8 | const anApp = new Clarifai.App({apiKey: process.env.CLARIFAI_API_KEY}); 9 | expect(anApp._config.apiKey).toEqual(process.env.CLARIFAI_API_KEY); 10 | done(); 11 | }); 12 | 13 | it('can make calls with an api key', done => { 14 | const anApp = new Clarifai.App({apiKey: process.env.CLARIFAI_API_KEY}); 15 | anApp.models.predict(Clarifai.GENERAL_MODEL, [ 16 | { 17 | 'url': sampleImages[0] 18 | }, 19 | { 20 | 'url': sampleImages[1] 21 | } 22 | ]) 23 | .then(response => { 24 | expect(response.outputs).toBeDefined(); 25 | const outputs = response.outputs; 26 | expect(outputs.length).toBe(2); 27 | const output = outputs[0]; 28 | expect(output.id).toBeDefined(); 29 | expect(output.status).toBeDefined(); 30 | expect(output.input).toBeDefined(); 31 | expect(output.model).toBeDefined(); 32 | expect(output.created_at).toBeDefined(); 33 | expect(output.data).toBeDefined(); 34 | done(); 35 | }) 36 | .catch(errorHandler.bind(done)); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/solutions/Moderation.js: -------------------------------------------------------------------------------- 1 | let axios = require('axios'); 2 | let {wrapToken} = require('../utils'); 3 | let {isSuccess, clone} = require('../helpers'); 4 | 5 | let BASE_URL = 'https://api.clarifai-moderation.com'; 6 | 7 | class Moderation { 8 | 9 | constructor(_config) { 10 | this._config = _config; 11 | 12 | } 13 | 14 | predict(modelID, imageURL) { 15 | return wrapToken(this._config, (headers) => { 16 | let url = `${BASE_URL}/v2/models/${modelID}/outputs`; 17 | let params = { 18 | inputs: [ 19 | { 20 | data: { 21 | image: { 22 | url: imageURL 23 | } 24 | } 25 | } 26 | ] 27 | }; 28 | 29 | return new Promise((resolve, reject) => { 30 | return axios.post(url, params, {headers}).then((response) => { 31 | if (isSuccess(response)) { 32 | let data = clone(response.data); 33 | resolve(data); 34 | } else { 35 | reject(response); 36 | } 37 | }, reject); 38 | }); 39 | }); 40 | } 41 | 42 | getModerationStatus(imageID) { 43 | return wrapToken(this._config, (headers) => { 44 | let url = `${BASE_URL}/v2/inputs/${imageID}/outputs`; 45 | return new Promise((resolve, reject) => { 46 | return axios.get(url, {headers}).then((response) => { 47 | let data = clone(response.data); 48 | resolve(data); 49 | }, reject); 50 | 51 | }); 52 | }); 53 | } 54 | } 55 | 56 | module.exports = Moderation; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clarifai", 3 | "version": "2.9.1", 4 | "description": "Official Clarifai Javascript SDK", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/Clarifai/clarifai-javascript", 7 | "author": "Clarifai Inc.", 8 | "license": "Apache-2.0", 9 | "scripts": { 10 | "jsdoc": "jsdoc src/* -t node_modules/minami -d docs/$npm_package_version && jsdoc src/* -t node_modules/minami -d docs/latest", 11 | "test": "gulp test", 12 | "unittest": "gulp unittest", 13 | "watch": "gulp watch", 14 | "build": "npm run clean && gulp build && npm run jsdoc", 15 | "release": "release-it", 16 | "clean": "gulp cleanbuild" 17 | }, 18 | "dependencies": { 19 | "axios": ">=0.13.0 <2", 20 | "promise": "^7.1.1", 21 | "valid-url": "^1.0.9" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.8.3", 25 | "@babel/preset-env": "^7.8.3", 26 | "@babel/register": "^7.8.3", 27 | "acorn": "^7.1.0", 28 | "axios-mock-adapter": "^1.16.0", 29 | "babel-eslint": "^10.0.1", 30 | "eslint": "^4.12.1", 31 | "babelify": "^10.0.0", 32 | "del": "^2.0.2", 33 | "envify": "^3.4.0", 34 | "gulp": "^4.0.2", 35 | "gulp-babel": "^8.0.0", 36 | "gulp-browserify": "^0.5.1", 37 | "gulp-eslint": "^5.0.0", 38 | "gulp-insert": "^0.5.0", 39 | "gulp-jasmine": "^4.0.0", 40 | "gulp-notify": "^3.2.0", 41 | "gulp-rename": "^1.4.0", 42 | "gulp-replace-task": "^0.11.0", 43 | "gulp-uglify": "^3.0.2", 44 | "gulp-util": "^3.0.8", 45 | "jsdoc": "^3.4.1", 46 | "minami": "^1.1.1", 47 | "release-it": "^2.9.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/integration/helpers.js: -------------------------------------------------------------------------------- 1 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 360000; // 6 minutes 2 | 3 | function errorHandler(err) { 4 | console.log("Received an error response from the API:"); 5 | if (err.response) { 6 | console.log(err.response.status + " " + err.response.statusText); 7 | 8 | try { 9 | console.log(JSON.stringify(err.response.data, null, 2)); 10 | } catch (e) { 11 | console.log(err.response.data); 12 | } 13 | 14 | console.log("where the request was: " + err.response.config.method.toUpperCase() + " " + err.response.config.url); 15 | if (err.response.config.method.toUpperCase() !== "GET") { 16 | try { 17 | console.log(JSON.stringify(JSON.parse(err.response.config.data), null, 2)); 18 | } catch (e) { 19 | console.log(err.response.config.data); 20 | } 21 | } 22 | } else { 23 | console.log(err); 24 | } 25 | 26 | console.log("Full response object:"); 27 | console.log(JSON.stringify(err, null, 4)); 28 | 29 | this(err); 30 | } 31 | 32 | function pollStatus(fn) { 33 | var getStatus = setInterval(() => { 34 | fn(getStatus); 35 | }, 1000); 36 | } 37 | 38 | function waitForInputsUpload(app) { 39 | return new Promise((resolve, reject) => { 40 | app.inputs.getStatus() 41 | .then(response => { 42 | if (response.counts.errors !== 0) { 43 | throw new Error('Error processing inputs', response); 44 | } else if (response.counts.to_process === 0 && response.counts.processing === 0) { 45 | resolve(); 46 | } else { 47 | setTimeout( 48 | () => { 49 | waitForInputsUpload(app) 50 | .then(resolve) 51 | .catch(reject); 52 | }, 53 | 1000 54 | ); 55 | } 56 | }) 57 | .catch(reject); 58 | }); 59 | } 60 | 61 | module.exports = { 62 | errorHandler, 63 | pollStatus, 64 | waitForInputsUpload 65 | }; 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## [[2.9.1]](https://github.com/Clarifai/clarifai-javascript/releases/tag/2.9.1) - [npm](https://www.npmjs.com/package/clarifai/v/2.9.1) - 2019-08-14 4 | 5 | ### Added 6 | - Added the hard-coded CELEBRITY model ID 7 | - Added the config parameter to workflow predict 8 | 9 | ### Changed 10 | - Made model version ID be applied in getOutputInfo 11 | - Relaxed the required axios dependency version range 12 | - Deprecated client ID/secret 13 | 14 | ## [[2.9.0]](https://github.com/Clarifai/clarifai-javascript/releases/tag/2.9.0) - [npm](https://www.npmjs.com/package/clarifai/v/2.9.0) - 2018-10-18 15 | 16 | ### Added 17 | - The moderation solution 18 | 19 | ## [[2.8.1]](https://github.com/Clarifai/clarifai-javascript/releases/tag/2.8.1) - [npm](https://www.npmjs.com/package/clarifai/v/2.8.1) - 2018-08-17 20 | 21 | ### Fixed 22 | - Pass the `options` parameter in `models.getVersions()` 23 | 24 | ## [[2.8.0]](https://github.com/Clarifai/clarifai-javascript/releases/tag/2.8.0) - [npm](https://www.npmjs.com/package/clarifai/v/2.8.0) - 2018-06-25 25 | 26 | ### Added 27 | - Ability to list workflows using the new `Workflows` object 28 | 29 | ### Fixed 30 | - Moved `create` and `delete` to `Workflows`, made them deprecated in `Workflow` 31 | 32 | ## [[2.7.1]](https://github.com/Clarifai/clarifai-javascript/releases/tag/2.7.1) - [npm](https://www.npmjs.com/package/clarifai/v/2.7.1) - 2018-04-23 33 | 34 | ### Fixed 35 | - Added missing input region serialization 36 | 37 | ## [[2.7.0]](https://github.com/Clarifai/clarifai-javascript/releases/tag/2.7.0) - [npm](https://www.npmjs.com/package/clarifai/v/2.7.0) - 2018-04-09 38 | 39 | ### Added 40 | - Support for custom face recognition https://github.com/Clarifai/clarifai-javascript/commit/ddee3667df0dfc648568bfc0c71d55600e223f1a 41 | 42 | ## [[2.6.0]](https://github.com/Clarifai/clarifai-javascript/releases/tag/2.6.0) - [npm](https://www.npmjs.com/package/clarifai/v/2.6.0) - 2018-01-17 43 | 44 | ### Added 45 | - Concept renaming https://github.com/Clarifai/clarifai-javascript/commit/5c966203ec9177d7f7c43b162da1910084f751ca 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '8' 5 | - '10' 6 | - 'node' 7 | 8 | branches: 9 | only: 10 | - master 11 | - /^\d+\.\d+\.\d+$/ 12 | 13 | cache: 14 | directories: 15 | - node_modules 16 | 17 | before_script: 18 | - npm list --depth=0 # Prints the installed dependency versions. 19 | - export PYTHONPATH=. 20 | - export CLARIFAI_APP_ID="$(python scripts/app_and_key_for_tests.py --create-app javascript-travis)" 21 | - export CLARIFAI_API_KEY="$(python scripts/app_and_key_for_tests.py --create-key ${CLARIFAI_APP_ID})" 22 | 23 | script: 24 | - npm run test 25 | - npm run build 26 | 27 | after_script: 28 | - export PYTHONPATH=. 29 | - python scripts/app_and_key_for_tests.py --delete-app $CLARIFAI_APP_ID 30 | 31 | deploy: 32 | - provider: s3 33 | access_key_id: ${AWS_ACCESS_KEY_ID} 34 | secret_access_key: ${AWS_SECRET_ACCESS_KEY} 35 | bucket: ${AWS_BUCKET} 36 | skip_cleanup: true 37 | acl: public_read 38 | upload-dir: js 39 | cache_control: "max-age=21600, no-transform, public" 40 | local_dir: sdk 41 | on: 42 | node_js: 10 43 | tags: true 44 | repo: Clarifai/clarifai-javascript 45 | - provider: s3 46 | access_key_id: ${AWS_ACCESS_KEY_ID} 47 | secret_access_key: ${AWS_SECRET_ACCESS_KEY} 48 | bucket: ${AWS_BUCKET} 49 | skip_cleanup: true 50 | acl: public_read 51 | upload-dir: js 52 | cache_control: "max-age=21600, no-transform, public" 53 | local_dir: docs 54 | on: 55 | node_js: 10 56 | tags: true 57 | repo: Clarifai/clarifai-javascript 58 | - provider: npm 59 | email: eng+npm@clarifai.com 60 | api_key: ${NPM_TOKEN} 61 | skip_cleanup: true 62 | on: 63 | node_js: 10 64 | tags: true 65 | repo: Clarifai/clarifai-javascript 66 | - provider: releases 67 | skip_cleanup: true 68 | api_key: ${GITHUB_TOKEN} 69 | file_glob: true 70 | file: sdk/* 71 | on: 72 | node_js: 10 73 | tags: true 74 | repo: Clarifai/clarifai-javascript 75 | 76 | after_deploy: cd examples && npm i clarifai@latest && CLARIFAI_API_KEY=${API_KEY} node index && cd .. 77 | -------------------------------------------------------------------------------- /tests/unit/invalid-response-unit-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {BASE_URL, SAMPLE_API_KEY} = require('./helpers'); 3 | const axios = require('axios'); 4 | const MockAdapter = require('axios-mock-adapter'); 5 | 6 | let app; 7 | 8 | let mock; 9 | 10 | describe('Unit Tests - Invalid Response', () => { 11 | beforeAll(() => { 12 | app = new Clarifai.App({ 13 | apiKey: SAMPLE_API_KEY, 14 | apiEndpoint: BASE_URL 15 | }); 16 | }); 17 | 18 | beforeEach(() => { 19 | mock = new MockAdapter(axios); 20 | }); 21 | 22 | it('Handles invalid JSON', done => { 23 | mock.onGet(BASE_URL + '/v2/inputs/%40modelID').reply(200, ` 24 | { 25 | "status": { 26 | "code": 10000, 27 | "description": "Ok" 28 | }, 29 | "model": { 30 | `); 31 | app.models.get('@modelID') 32 | .catch(response => { 33 | done(); 34 | }); 35 | }); 36 | 37 | it('Handles predict with invalid URL', done => { 38 | mock.onPost(BASE_URL + '/v2/models/%40modelID/outputs').reply(400, ` 39 | { 40 | "status": { 41 | "code": 10020, 42 | "description": "Failure" 43 | }, 44 | "outputs": [ 45 | { 46 | "id": "@outputID", 47 | "status": { 48 | "code": 30002, 49 | "description": "Download failed; check URL", 50 | "details": "404 Client Error: Not Found for url: @invalidURL" 51 | }, 52 | "created_at": "2019-01-20T19:39:15.460417224Z", 53 | "model": { 54 | "id": "@modelID", 55 | "name": "color", 56 | "created_at": "2016-05-11T18:05:45.924367Z", 57 | "app_id": "main", 58 | "output_info": { 59 | "message": "Show output_info with: GET /models/{model_id}/output_info", 60 | "type": "color", 61 | "type_ext": "color" 62 | }, 63 | "model_version": { 64 | "id": "@modelVersionID", 65 | "created_at": "2016-07-13T01:19:12.147644Z", 66 | "status": { 67 | "code": 21100, 68 | "description": "Model trained successfully" 69 | }, 70 | "train_stats": {} 71 | }, 72 | "display_name": "Color" 73 | }, 74 | "input": { 75 | "id": "@inputID", 76 | "data": { 77 | "image": { 78 | "url": "@invalidURL" 79 | } 80 | } 81 | }, 82 | "data": {} 83 | } 84 | ] 85 | } 86 | `); 87 | 88 | app.models.predict('@modelID', {url: '@invalidURL'}) 89 | .then(response => { 90 | done.fail('Did not throw'); 91 | }) 92 | .catch(response => { 93 | done(); 94 | }); 95 | }); 96 | 97 | // TODO(Rok) MEDIUM: Add testing mixed success. 98 | }); 99 | -------------------------------------------------------------------------------- /tests/integration/concepts-int-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {errorHandler} = require('./helpers'); 3 | const langConceptId = '的な' + Date.now(); 4 | const beerId = 'beer' + Date.now(); 5 | const ferrariId = 'ferrari' + Date.now(); 6 | 7 | let app; 8 | 9 | describe('Integration Tests - Concepts', () => { 10 | const conceptsIds = [ 11 | 'porsche' + Date.now(), 12 | 'rolls royce' + Date.now(), 13 | 'lamborghini' + Date.now(), 14 | langConceptId, 15 | beerId, 16 | ferrariId 17 | ]; 18 | 19 | beforeAll(() => { 20 | app = new Clarifai.App({ 21 | apiKey: process.env.CLARIFAI_API_KEY, 22 | apiEndpoint: process.env.API_ENDPOINT 23 | }); 24 | }); 25 | 26 | it('creates concepts given a list of strings', done => { 27 | app.concepts.create(conceptsIds) 28 | .then(concepts => { 29 | expect(concepts).toBeDefined(); 30 | expect(concepts.length).toBe(conceptsIds.length); 31 | expect(concepts[0].id).toBe(conceptsIds[0]); 32 | expect(concepts[1].id).toBe(conceptsIds[1]); 33 | expect(concepts[2].id).toBe(conceptsIds[2]); 34 | done(); 35 | }) 36 | .catch(errorHandler.bind(done)); 37 | }); 38 | 39 | it('gets concept with id in a different language', done => { 40 | app.concepts.get(langConceptId) 41 | .then(concept => { 42 | expect(concept.id).toBe(langConceptId); 43 | expect(concept.name).toBe(langConceptId); 44 | done(); 45 | }) 46 | .catch(errorHandler.bind(done)); 47 | }); 48 | 49 | it('search concepts', done => { 50 | app.concepts.search('lab*') 51 | .then(concepts => { 52 | expect(concepts.length).toBe(6); 53 | expect(concepts[0].name).toBe('label'); 54 | done(); 55 | }) 56 | .catch(errorHandler.bind(done)); 57 | }); 58 | 59 | it('search concepts in a different language', done => { 60 | app.concepts.search('狗*', 'zh') 61 | .then(concepts => { 62 | expect(concepts.length).toBe(3); 63 | return app.models.delete(); 64 | }) 65 | .then(response => { 66 | expect(response.status).toBeDefined(); 67 | done(); 68 | }) 69 | .catch(errorHandler.bind(done)); 70 | }); 71 | 72 | it('updates a concept name', done => { 73 | const originalName = conceptsIds[0]; 74 | const newName = `${originalName}-newName`; 75 | 76 | app.concepts.update({ id: originalName, name: newName }) 77 | .then(concepts => { 78 | expect(concepts[0].id).toBe(originalName); 79 | expect(concepts[0].name).toBe(newName); 80 | done(); 81 | }) 82 | .catch(errorHandler.bind(done)); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | const MAX_BATCH_SIZE = 128; 2 | const GEO_LIMIT_TYPES = ['withinMiles', 'withinKilometers', 'withinRadians', 'withinDegrees']; 3 | const SYNC_TIMEOUT = 360000; // 6 minutes 4 | const MODEL_QUEUED_FOR_TRAINING = '21103'; 5 | const MODEL_TRAINING = '21101'; 6 | const POLLTIME = 2000; 7 | 8 | module.exports = { 9 | API: { 10 | TOKEN_PATH: '/token', 11 | MODELS_PATH: '/models', 12 | MODEL_PATH: '/models/$0', 13 | MODEL_VERSIONS_PATH: '/models/$0/versions', 14 | MODEL_VERSION_PATH: '/models/$0/versions/$1', 15 | MODEL_PATCH_PATH: '/models/$0/output_info/data/concepts', 16 | MODEL_OUTPUT_PATH: '/models/$0/output_info', 17 | MODEL_VERSION_OUTPUT_PATH: '/models/$0/versions/$1/output_info', 18 | MODEL_SEARCH_PATH: '/models/searches', 19 | PREDICT_PATH: '/models/$0/outputs', 20 | VERSION_PREDICT_PATH: '/models/$0/versions/$1/outputs', 21 | CONCEPTS_PATH: '/concepts', 22 | CONCEPT_PATH: '/concepts/$0', 23 | CONCEPT_SEARCH_PATH: '/concepts/searches', 24 | MODEL_INPUTS_PATH: '/models/$0/inputs', 25 | MODEL_VERSION_INPUTS_PATH: '/models/$0/versions/$1/inputs', 26 | MODEL_VERSION_METRICS_PATH: '/models/$0/versions/$1/metrics', 27 | INPUTS_PATH: '/inputs', 28 | INPUT_PATH: '/inputs/$0', 29 | INPUTS_STATUS_PATH: '/inputs/status', 30 | SEARCH_PATH: '/searches', 31 | WORKFLOWS_PATH: '/workflows', 32 | WORKFLOW_PATH: '/workflows/$0', 33 | WORKFLOW_RESULTS_PATH: '/workflows/$0/results' 34 | }, 35 | ERRORS: { 36 | paramsRequired: (param) => { 37 | let paramList = Array.isArray(param) ? param : [param]; 38 | return new Error(`The following ${paramList.length > 1 ? 'params are' : 'param is'} required: ${paramList.join(', ')}`); 39 | }, 40 | MAX_INPUTS: new Error(`Number of inputs passed exceeded max of ${MAX_BATCH_SIZE}`), 41 | INVALID_GEOLIMIT_TYPE: new Error(`Incorrect geo_limit type. Value must be any of the following: ${GEO_LIMIT_TYPES.join(', ')}`), 42 | INVALID_DELETE_ARGS: new Error(`Wrong arguments passed. You can only delete all models (provide no arguments), delete select models (provide list of ids), 43 | delete a single model (providing a single id) or delete a model version (provide a single id and version id)`) 44 | }, 45 | STATUS: { 46 | MODEL_QUEUED_FOR_TRAINING, 47 | MODEL_TRAINING 48 | }, 49 | // var replacement must be given in order 50 | replaceVars: (path, vars = []) => { 51 | let newPath = path; 52 | vars.forEach((val, index) => { 53 | if (index === 0) { 54 | val = encodeURIComponent(val); 55 | } 56 | newPath = newPath.replace(new RegExp(`\\$${index}`, 'g'), val); 57 | }); 58 | return newPath; 59 | }, 60 | getBasePath: (apiEndpoint = 'https://api.clarifai.com', userId, appId) => { 61 | if(!userId || !appId) { 62 | return `${apiEndpoint}/v2`; 63 | } 64 | return `${apiEndpoint}/v2/users/${userId}/apps/${appId}`; 65 | }, 66 | GEO_LIMIT_TYPES, 67 | MAX_BATCH_SIZE, 68 | SYNC_TIMEOUT, 69 | POLLTIME 70 | }; 71 | -------------------------------------------------------------------------------- /src/Workflows.js: -------------------------------------------------------------------------------- 1 | let axios = require('axios'); 2 | let Workflow = require('./Workflow'); 3 | let {API, replaceVars} = require('./constants'); 4 | let {WORKFLOWS_PATH, WORKFLOW_PATH,} = API; 5 | let {wrapToken,} = require('./utils'); 6 | let {isSuccess,} = require('./helpers'); 7 | 8 | /** 9 | * class representing a collection of workflows 10 | * @class 11 | */ 12 | class Workflows { 13 | constructor(_config, rawData = []) { 14 | this._config = _config; 15 | this.rawData = rawData; 16 | rawData.forEach((workflowData, index) => { 17 | this[index] = new Workflow(this._config, workflowData); 18 | }); 19 | this.length = rawData.length; 20 | } 21 | 22 | /** 23 | * Get all workflows in app 24 | * @param {Object} options Object with keys explained below: (optional) 25 | * @param {Number} options.page The page number (optional, default: 1) 26 | * @param {Number} options.perPage Number of images to return per page (optional, default: 20) 27 | * @return {Promise(Workflows, error)} A Promise that is fulfilled with an instance of Workflows or rejected with an error 28 | */ 29 | list(options = {page: 1, perPage: 20}) { 30 | let url = `${this._config.basePath}${WORKFLOWS_PATH}`; 31 | return wrapToken(this._config, (headers) => { 32 | return new Promise((resolve, reject) => { 33 | axios.get(url, { 34 | headers, 35 | params: { 36 | page: options.page, 37 | per_page: options.perPage, 38 | } 39 | }).then((response) => { 40 | if (isSuccess(response)) { 41 | resolve(new Workflows(this._config, response.data.workflows)); 42 | } else { 43 | reject(response); 44 | } 45 | }, reject); 46 | }); 47 | }); 48 | } 49 | 50 | create(workflowId, config) { 51 | const url = `${this._config.basePath}${WORKFLOWS_PATH}`; 52 | const modelId = config.modelId; 53 | const modelVersionId = config.modelVersionId; 54 | const body = { 55 | workflows: [{ 56 | id: workflowId, 57 | nodes: [{ 58 | id: 'concepts', 59 | model: { 60 | id: modelId, 61 | model_version: { 62 | id: modelVersionId 63 | } 64 | } 65 | }] 66 | }] 67 | }; 68 | 69 | return wrapToken(this._config, (headers) => { 70 | return new Promise((resolve, reject) => { 71 | axios.post(url, body, { 72 | headers 73 | }).then(response => { 74 | const workflowId = response.data.workflows[0].id; 75 | resolve(workflowId); 76 | }, reject); 77 | }); 78 | }); 79 | } 80 | 81 | delete(workflowId) { 82 | const url = `${this._config.basePath}${replaceVars(WORKFLOW_PATH, [workflowId])}`; 83 | return wrapToken(this._config, (headers) => { 84 | return new Promise((resolve, reject) => { 85 | axios.delete(url, { 86 | headers 87 | }).then(response => { 88 | const data = response.data; 89 | resolve(data); 90 | }, reject); 91 | }); 92 | }); 93 | } 94 | } 95 | ; 96 | 97 | module.exports = Workflows; 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://github.com/user-attachments/assets/f440f534-7ff1-4189-8d07-38210810534f) 2 | 3 | # Deprecated 4 | For node.js users, this API Client is no longer supported. 5 | Please use https://github.com/Clarifai/clarifai-nodejs-grpc instead which is more feature rich, faster and supported. We will be introducing a new web grpc client shortly as well to fully deprecate this javascript client. Stay tuned. 6 | 7 | 8 | # Clarifai API JavaScript Client 9 | 10 | This is the official JavaScript client for interacting with our powerful recognition 11 | [API](https://developer.clarifai.com). The Clarifai API offers image and video recognition as a service. Whether you 12 | have one image or billions, you are only steps away from using artificial intelligence to recognize your visual content. 13 | 14 | * Try the Clarifai demo at: https://clarifai.com/demo 15 | * Sign up for a free account at: https://clarifai.com/developer/account/signup/ 16 | * Read the developer guide at: https://clarifai.com/developer/guide/ 17 | 18 | 19 | [![Build Status](https://travis-ci.org/Clarifai/clarifai-javascript.svg?branch=master)](https://travis-ci.org/Clarifai/clarifai-javascript) 20 | [![npm version](https://badge.fury.io/js/clarifai.svg)](https://badge.fury.io/js/clarifai) 21 | 22 | ## Installation 23 | Install the API client: 24 | ``` 25 | npm install clarifai 26 | ``` 27 | 28 | ## Basic Use 29 | 30 | Firstly, generate your Clarifai API key [on the API keys page](https://clarifai.com/developer/account/keys). The client 31 | uses it for authentication. 32 | 33 | Then, use the code below to create a `Clarifai.App` instance using which you interact with the client. 34 | 35 | ```js 36 | const Clarifai = require('clarifai'); 37 | 38 | const app = new Clarifai.App({ 39 | apiKey: 'YOUR_API_KEY' 40 | }); 41 | ``` 42 | 43 | *This will work in node.js and browsers via [Browserify](http://browserify.org/).* 44 | 45 | You can also use the SDK by adding this script to your HTML: 46 | 47 | ```html 48 | 49 | ``` 50 | 51 | ## Documentation 52 | 53 | Dive right into code examples to get up and running as quickly as possible with our [Quick Start](https://developer.clarifai.com/quick-start/). 54 | 55 | Learn the basics — predicting the contents of an image, searching across a collection and creating your own models with our [Guide](https://developer.clarifai.com/guide/). 56 | 57 | Check out the [JSDoc](https://sdk.clarifai.com/js/latest/index.html) for a deeper reference. 58 | 59 | Looking for a different client? We have many languages available with lots of documentation [Technical Reference](https://clarifai.com/developer/reference) 60 | 61 | ## React Native 62 | 63 | You'll most likely encounter the error `process.nextTick is not a function` while using this library with React Native. 64 | 65 | To solve this, add `process.nextTick = setImmediate;` as close to the top of your entrypoint as you can. See [#20](https://github.com/Clarifai/clarifai-javascript/issues/20) for more info. 66 | 67 | 68 | ## License 69 | 70 | This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. 71 | -------------------------------------------------------------------------------- /tests/unit/model-versions-unit-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {BASE_URL, SAMPLE_API_KEY} = require('./helpers'); 3 | const axios = require('axios'); 4 | const MockAdapter = require('axios-mock-adapter'); 5 | const {errorHandler} = require('../integration/helpers'); 6 | 7 | let app; 8 | 9 | let mock; 10 | 11 | describe('Unit Tests - Model Versions', () => { 12 | beforeAll(() => { 13 | app = new Clarifai.App({ 14 | apiKey: SAMPLE_API_KEY, 15 | apiEndpoint: BASE_URL 16 | }); 17 | }); 18 | 19 | beforeEach(() => { 20 | mock = new MockAdapter(axios); 21 | }); 22 | 23 | it('Gets model version', done => { 24 | mock.onGet(BASE_URL + '/v2/models/%40modelID/versions/@modelVersionID').reply(200, JSON.parse(` 25 | { 26 | "status": { 27 | "code": 10000, 28 | "description": "Ok" 29 | }, 30 | "model_version": { 31 | "id": "@modelVersionID", 32 | "created_at": "2017-10-31T16:30:31.226185Z", 33 | "status": { 34 | "code": 21100, 35 | "description": "Model trained successfully" 36 | }, 37 | "active_concept_count": 5, 38 | "train_stats": {} 39 | } 40 | } 41 | `)); 42 | 43 | 44 | app.models.getVersion('@modelID', '@modelVersionID') 45 | .then(response => { 46 | expect(mock.history.get.length).toBe(1); 47 | 48 | expect(response.status.code).toEqual(10000); 49 | expect(response.model_version.id).toEqual('@modelVersionID'); 50 | done(); 51 | }) 52 | .catch(errorHandler.bind(done)); 53 | }); 54 | 55 | it('Gets model versions', done => { 56 | mock.onGet(BASE_URL + '/v2/models/%40modelID/versions').reply(200, JSON.parse(` 57 | { 58 | "status": { 59 | "code": 10000, 60 | "description": "Ok" 61 | }, 62 | "model_versions": [ 63 | { 64 | "id": "@modelVersionID1", 65 | "created_at": "2017-10-31T16:30:31.226185Z", 66 | "status": { 67 | "code": 21100, 68 | "description": "Model trained successfully" 69 | }, 70 | "active_concept_count": 5, 71 | "train_stats": {} 72 | }, 73 | { 74 | "id": "@modelVersionID2", 75 | "created_at": "2017-05-16T19:20:38.733764Z", 76 | "status": { 77 | "code": 21100, 78 | "description": "Model trained successfully" 79 | }, 80 | "active_concept_count": 5, 81 | "train_stats": {} 82 | } 83 | ] 84 | } 85 | `)); 86 | 87 | app.models.getVersions('@modelID') 88 | .then(response => { 89 | expect(mock.history.get.length).toBe(1); 90 | 91 | expect(response.status.code).toEqual(10000); 92 | expect(response.model_versions[0].id).toEqual('@modelVersionID1'); 93 | expect(response.model_versions[1].id).toEqual('@modelVersionID2'); 94 | done(); 95 | }) 96 | .catch(errorHandler.bind(done)); 97 | }); 98 | 99 | it('Delete model version', done => { 100 | mock.onDelete(BASE_URL + '/v2/models/%40modelID/versions/@versionID').reply(200, JSON.parse(` 101 | { 102 | "status": { 103 | "code": 10000, 104 | "description": "Ok" 105 | } 106 | } 107 | `)); 108 | 109 | app.models.delete('@modelID', '@versionID').then(response => { 110 | expect(mock.history.delete.length).toBe(1); 111 | 112 | expect(response.status.code).toEqual(10000); 113 | 114 | done(); 115 | }).catch(errorHandler.bind(done)); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /tests/unit/search-concepts-unit-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {BASE_URL, SAMPLE_API_KEY} = require('./helpers'); 3 | const axios = require('axios'); 4 | const MockAdapter = require('axios-mock-adapter'); 5 | const {errorHandler} = require('../integration/helpers'); 6 | 7 | let app; 8 | 9 | let mock; 10 | 11 | describe('Unit Tests - Concept Search', () => { 12 | beforeAll(() => { 13 | app = new Clarifai.App({ 14 | apiKey: SAMPLE_API_KEY, 15 | apiEndpoint: BASE_URL 16 | }); 17 | }); 18 | 19 | beforeEach(() => { 20 | mock = new MockAdapter(axios); 21 | }); 22 | 23 | it('Searches concepts', done => { 24 | mock.onPost(BASE_URL + '/v2/concepts/searches').reply(200, JSON.parse(` 25 | { 26 | "status": { 27 | "code": 10000, 28 | "description": "Ok" 29 | }, 30 | "concepts": [ 31 | { 32 | "id": "@conceptID1", 33 | "name": "concealer", 34 | "value": 1, 35 | "created_at": "2016-03-17T11:43:01.223962Z", 36 | "language": "en", 37 | "app_id": "main", 38 | "definition": "concealer" 39 | }, 40 | { 41 | "id": "@conceptID2", 42 | "name": "concentrate", 43 | "value": 1, 44 | "created_at": "2016-03-17T11:43:01.223962Z", 45 | "language": "en", 46 | "app_id": "main", 47 | "definition": "direct one's attention on something" 48 | } 49 | ] 50 | } 51 | `)); 52 | 53 | 54 | app.concepts.search('conc*') 55 | .then(concepts => { 56 | expect(mock.history.post.length).toBe(1); 57 | expect(JSON.parse(mock.history.post[0].data)).toEqual(JSON.parse(` 58 | { 59 | "concept_query": { 60 | "name": "conc*", 61 | "language": null 62 | } 63 | } 64 | `)); 65 | 66 | expect(concepts[0].id).toEqual('@conceptID1'); 67 | expect(concepts[0].name).toEqual('concealer'); 68 | 69 | expect(concepts[1].id).toEqual('@conceptID2'); 70 | expect(concepts[1].name).toEqual('concentrate'); 71 | 72 | done(); 73 | }) 74 | .catch(errorHandler.bind(done)); 75 | }); 76 | 77 | it('Searches concepts with language', done => { 78 | mock.onPost(BASE_URL + '/v2/concepts/searches').reply(200, JSON.parse(` 79 | { 80 | "status": { 81 | "code": 10000, 82 | "description": "Ok" 83 | }, 84 | "concepts": [ 85 | { 86 | "id": "@conceptID1", 87 | "name": "狗", 88 | "value": 1, 89 | "created_at": "2016-03-17T11:43:01.223962Z", 90 | "language": "zh", 91 | "app_id": "main" 92 | }, 93 | { 94 | "id": "@conceptID2", 95 | "name": "狗仔队", 96 | "value": 1, 97 | "created_at": "2016-03-17T11:43:01.223962Z", 98 | "language": "zh", 99 | "app_id": "main" 100 | }, 101 | { 102 | "id": "@conceptID3", 103 | "name": "狗窝", 104 | "value": 1, 105 | "created_at": "2016-03-17T11:43:01.223962Z", 106 | "language": "zh", 107 | "app_id": "main" 108 | } 109 | ] 110 | } 111 | `)); 112 | 113 | 114 | app.concepts.search('狗*', 'zh') 115 | .then(concepts => { 116 | expect(mock.history.post.length).toBe(1); 117 | expect(JSON.parse(mock.history.post[0].data)).toEqual(JSON.parse(` 118 | { 119 | "concept_query": { 120 | "name": "狗*", 121 | "language": "zh" 122 | } 123 | } 124 | `)); 125 | 126 | expect(concepts[0].id).toEqual('@conceptID1'); 127 | expect(concepts[0].name).toEqual('狗'); 128 | 129 | expect(concepts[1].id).toEqual('@conceptID2'); 130 | expect(concepts[1].name).toEqual('狗仔队'); 131 | 132 | expect(concepts[2].id).toEqual('@conceptID3'); 133 | expect(concepts[2].name).toEqual('狗窝'); 134 | 135 | done(); 136 | }) 137 | .catch(errorHandler.bind(done)); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /src/Workflow.js: -------------------------------------------------------------------------------- 1 | let axios = require('axios'); 2 | let {formatObjectForSnakeCase} = require('./utils'); 3 | let {API, replaceVars} = require('./constants'); 4 | let {WORKFLOWS_PATH, WORKFLOW_PATH, WORKFLOW_RESULTS_PATH} = API; 5 | let {wrapToken, formatInput} = require('./utils'); 6 | let {checkType} = require('./helpers'); 7 | 8 | /** 9 | * class representing a workflow 10 | * @class 11 | */ 12 | class Workflow { 13 | constructor(_config, rawData=[]) { 14 | this._config = _config; 15 | this.rawData = rawData; 16 | this.id = rawData.id; 17 | this.createdAt = rawData.created_at || rawData.createdAt; 18 | this.appId = rawData.app_id || rawData.appId; 19 | } 20 | 21 | /** 22 | * @deprecated 23 | */ 24 | create(workflowId, config) { 25 | const url = `${this._config.basePath}${WORKFLOWS_PATH}`; 26 | const modelId = config.modelId; 27 | const modelVersionId = config.modelVersionId; 28 | const body = { 29 | workflows: [{ 30 | id: workflowId, 31 | nodes: [{ 32 | id: 'concepts', 33 | model: { 34 | id: modelId, 35 | model_version: { 36 | id: modelVersionId 37 | } 38 | } 39 | }] 40 | }] 41 | }; 42 | 43 | return wrapToken(this._config, (headers) => { 44 | return new Promise((resolve, reject) => { 45 | axios.post(url, body, { 46 | headers 47 | }).then(response => { 48 | const workflowId = response.data.workflows[0].id; 49 | resolve(workflowId); 50 | }, reject); 51 | }); 52 | }); 53 | } 54 | 55 | /** 56 | * @deprecated 57 | */ 58 | delete(workflowId, config) { 59 | const url = `${this._config.basePath}${replaceVars(WORKFLOW_PATH, [workflowId])}`; 60 | return wrapToken(this._config, (headers) => { 61 | return new Promise((resolve, reject) => { 62 | axios.delete(url, { 63 | headers 64 | }).then(response => { 65 | const data = response.data; 66 | resolve(data); 67 | }, reject); 68 | }); 69 | }); 70 | } 71 | 72 | /** 73 | * Returns workflow output according to inputs 74 | * @param {string} workflowId Workflow id 75 | * @param {object[]|object|string} inputs An array of objects/object/string pointing to an image resource. A string can either be a url or base64 image bytes. Object keys explained below: 76 | * @param {object} inputs[].image Object with keys explained below: 77 | * @param {string} inputs[].image.(url|base64) Can be a publicly accessibly url or base64 string representing image bytes (required) 78 | * @param {object} config An object with keys explained below. 79 | * @param {float} config.minValue The minimum confidence threshold that a result must meet. From 0.0 to 1.0 80 | * @param {number} config.maxConcepts The maximum number of concepts to return 81 | */ 82 | predict(workflowId, inputs, config = {}) { 83 | const url = `${this._config.basePath}${replaceVars(WORKFLOW_RESULTS_PATH, [workflowId])}`; 84 | if (checkType(/(Object|String)/, inputs)) { 85 | inputs = [inputs]; 86 | } 87 | return wrapToken(this._config, (headers) => { 88 | const params = { 89 | inputs: inputs.map(formatInput) 90 | }; 91 | if (config && Object.getOwnPropertyNames(config).length > 0) { 92 | params['output_config'] = formatObjectForSnakeCase(config); 93 | } 94 | return new Promise((resolve, reject) => { 95 | axios.post(url, params, { 96 | headers 97 | }).then((response) => { 98 | const data = response.data; 99 | resolve(data); 100 | }, reject); 101 | }); 102 | }); 103 | } 104 | } 105 | 106 | module.exports = Workflow; 107 | -------------------------------------------------------------------------------- /tests/unit/search-models-unit-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {BASE_URL, SAMPLE_API_KEY} = require('./helpers'); 3 | const axios = require('axios'); 4 | const MockAdapter = require('axios-mock-adapter'); 5 | const {errorHandler} = require('../integration/helpers'); 6 | 7 | let app; 8 | 9 | let mock; 10 | 11 | describe('Unit Tests - Model Search', () => { 12 | beforeAll(() => { 13 | app = new Clarifai.App({ 14 | apiKey: SAMPLE_API_KEY, 15 | apiEndpoint: BASE_URL 16 | }); 17 | }); 18 | 19 | beforeEach(() => { 20 | mock = new MockAdapter(axios); 21 | }); 22 | 23 | it('Search models by name', done => { 24 | mock.onPost(BASE_URL + '/v2/models/searches').reply(200, JSON.parse(` 25 | { 26 | "status": { 27 | "code": 10000, 28 | "description": "Ok", 29 | "req_id": "08e649a6116f4f56992e1676b25dcde6" 30 | }, 31 | "models": [ 32 | { 33 | "id": "@modelID", 34 | "name": "moderation", 35 | "created_at": "2017-05-12T21:28:00.471607Z", 36 | "app_id": "main", 37 | "output_info": { 38 | "message": "Show output_info with: GET /models/{model_id}/output_info", 39 | "type": "concept", 40 | "type_ext": "concept" 41 | }, 42 | "model_version": { 43 | "id": "@modelVersionID", 44 | "created_at": "2017-10-26T20:29:09.263232Z", 45 | "status": { 46 | "code": 21100, 47 | "description": "Model is trained and ready" 48 | }, 49 | "active_concept_count": 5, 50 | "worker_id": "8b7c05a25ce04d0490367390665f1526" 51 | }, 52 | "display_name": "Moderation" 53 | } 54 | ] 55 | } 56 | `)); 57 | 58 | 59 | app.models.search("moderation*") 60 | .then(models => { 61 | expect(mock.history.post.length).toBe(1); 62 | expect(JSON.parse(mock.history.post[0].data)).toEqual(JSON.parse(` 63 | { 64 | "model_query": { 65 | "name": "moderation*", 66 | "type": null 67 | } 68 | } 69 | `)); 70 | 71 | expect(models[0].id).toEqual('@modelID'); 72 | expect(models[0].modelVersion.id).toEqual('@modelVersionID'); 73 | 74 | done(); 75 | }) 76 | .catch(errorHandler.bind(done)); 77 | }); 78 | 79 | it('Search models by name and type', done => { 80 | mock.onPost(BASE_URL + '/v2/models/searches').reply(200, JSON.parse(` 81 | { 82 | "status": { 83 | "code": 10000, 84 | "description": "Ok", 85 | "req_id": "300ceac8704748a592da50d77ad44253" 86 | }, 87 | 88 | "models": [{ 89 | "id": "@modelID", 90 | "name": "color", 91 | "created_at": "2017-03-06T22:57:00.660603Z", 92 | "app_id": "main", 93 | "output_info": { 94 | "message": "Show output_info with: GET /models/{model_id}/output_info", 95 | "type": "blur", 96 | "type_ext": "color" 97 | }, 98 | "model_version": { 99 | "id": "@modelVersionID", 100 | "created_at": "2017-03-06T22:57:00.684652Z", 101 | "status": { 102 | "code": 21100, 103 | "description": "Model trained successfully" 104 | } 105 | }, 106 | "display_name": "Color" 107 | }] 108 | } 109 | `)); 110 | 111 | 112 | app.models.search("*", "color") 113 | .then(models => { 114 | expect(mock.history.post.length).toBe(1); 115 | expect(JSON.parse(mock.history.post[0].data)).toEqual(JSON.parse(` 116 | { 117 | "model_query": { 118 | "name": "*", 119 | "type": "color" 120 | } 121 | } 122 | `)); 123 | 124 | expect(models[0].id).toEqual('@modelID'); 125 | expect(models[0].modelVersion.id).toEqual('@modelVersionID'); 126 | 127 | done(); 128 | }) 129 | .catch(errorHandler.bind(done)); 130 | }); 131 | 132 | }); 133 | -------------------------------------------------------------------------------- /src/Input.js: -------------------------------------------------------------------------------- 1 | let axios = require('axios'); 2 | let Concepts = require('./Concepts'); 3 | let Regions = require('./Regions'); 4 | let {API} = require('./constants'); 5 | let {INPUTS_PATH} = API; 6 | 7 | /** 8 | * class representing an input 9 | * @class 10 | */ 11 | class Input { 12 | constructor(_config, data) { 13 | this.id = data.id; 14 | this.createdAt = data.created_at || data.createdAt; 15 | this.imageUrl = data.data.image.url; 16 | this.concepts = new Concepts(_config, data.data.concepts); 17 | this.regions = new Regions(_config, data.data.regions || []); 18 | this.score = data.score; 19 | this.metadata = data.data.metadata; 20 | if (data.data.geo && data.data.geo['geo_point']) { 21 | this.geo = {geoPoint: data.data.geo['geo_point']}; 22 | } 23 | this.rawData = data; 24 | this._config = _config; 25 | } 26 | 27 | /** 28 | * Merge concepts to an input 29 | * @param {object[]} concepts Object with keys explained below: 30 | * @param {object} concepts[].concept 31 | * @param {string} concepts[].concept.id The concept id (required) 32 | * @param {boolean} concepts[].concept.value Whether or not the input is a positive (true) or negative (false) example of the concept (default: true) 33 | * @param {object} metadata Object with key values to attach to the input (optional) 34 | * @return {Promise(Input, error)} A Promise that is fulfilled with an instance of Input or rejected with an error 35 | */ 36 | mergeConcepts(concepts, metadata) { 37 | return this._update('merge', concepts, metadata); 38 | } 39 | 40 | /** 41 | * Delete concept from an input 42 | * @param {object[]} concepts Object with keys explained below: 43 | * @param {object} concepts[].concept 44 | * @param {string} concepts[].concept.id The concept id (required) 45 | * @param {boolean} concepts[].concept.value Whether or not the input is a positive (true) or negative (false) example of the concept (default: true) 46 | * @param {object} metadata Object with key values to attach to the input (optional) 47 | * @return {Promise(Input, error)} A Promise that is fulfilled with an instance of Input or rejected with an error 48 | */ 49 | deleteConcepts(concepts, metadata) { 50 | return this._update('remove', concepts, metadata); 51 | } 52 | 53 | /** 54 | * Overwrite inputs 55 | * @param {object[]} concepts Array of object with keys explained below: 56 | * @param {object} concepts[].concept 57 | * @param {string} concepts[].concept.id The concept id (required) 58 | * @param {boolean} concepts[].concept.value Whether or not the input is a positive (true) or negative (false) example of the concept (default: true) 59 | * @param {object} metadata Object with key values to attach to the input (optional) 60 | * @return {Promise(Input, error)} A Promise that is fulfilled with an instance of Input or rejected with an error 61 | */ 62 | overwriteConcepts(concepts, metadata) { 63 | return this._update('overwrite', concepts, metadata); 64 | } 65 | 66 | _update(action, concepts = [], metadata = null) { 67 | let url = `${this._config.basePath}${INPUTS_PATH}`; 68 | let inputData = {}; 69 | if (concepts.length) { 70 | inputData.concepts = concepts; 71 | } 72 | if (metadata !== null) { 73 | inputData.metadata = metadata; 74 | } 75 | let data = { 76 | action, 77 | inputs: [ 78 | { 79 | id: this.id, 80 | data: inputData 81 | } 82 | ] 83 | }; 84 | return wrapToken(this._config, (headers) => { 85 | return new Promise((resolve, reject) => { 86 | return axios.patch(url, data, {headers}) 87 | .then((response) => { 88 | if (isSuccess(response)) { 89 | resolve(new Input(response.data.input)); 90 | } else { 91 | reject(response); 92 | } 93 | }, reject); 94 | }); 95 | }); 96 | } 97 | } 98 | ; 99 | 100 | module.exports = Input; 101 | -------------------------------------------------------------------------------- /tests/unit/concepts-unit-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {BASE_URL, SAMPLE_API_KEY} = require('./helpers'); 3 | const axios = require('axios'); 4 | const MockAdapter = require('axios-mock-adapter'); 5 | const {errorHandler} = require('../integration/helpers'); 6 | 7 | let app; 8 | 9 | let mock; 10 | 11 | describe('Unit Tests - Concepts', () => { 12 | beforeAll(() => { 13 | app = new Clarifai.App({ 14 | apiKey: SAMPLE_API_KEY, 15 | apiEndpoint: BASE_URL 16 | }); 17 | }); 18 | 19 | beforeEach(() => { 20 | mock = new MockAdapter(axios); 21 | }); 22 | 23 | it('Creates concepts', done => { 24 | mock.onPost(BASE_URL + '/v2/concepts').reply(200, JSON.parse(` 25 | { 26 | "status": { 27 | "code": 10000, 28 | "description": "Ok" 29 | }, 30 | "concepts": [ 31 | { 32 | "id": "@conceptID1", 33 | "name": "@conceptID1", 34 | "value": 1, 35 | "created_at": "2019-01-14T16:42:42.210598955Z", 36 | "language": "en", 37 | "app_id": "c102e505581f49d2956e3caa2e1a0dc9" 38 | }, 39 | { 40 | "id": "@conceptID2", 41 | "name": "@conceptID2", 42 | "value": 1, 43 | "created_at": "2019-01-14T16:42:42.210605836Z", 44 | "language": "en", 45 | "app_id": "c102e505581f49d2956e3caa2e1a0dc9" 46 | } 47 | ] 48 | } 49 | `)); 50 | 51 | app.concepts.create(['@conceptID1', '@conceptID2']) 52 | .then(concepts => { 53 | expect(mock.history.post.length).toBe(1); 54 | expect(JSON.parse(mock.history.post[0].data)).toEqual(JSON.parse(` 55 | { 56 | "concepts": [ 57 | { 58 | "id": "@conceptID1" 59 | }, 60 | { 61 | "id": "@conceptID2" 62 | } 63 | ] 64 | } 65 | `)); 66 | 67 | expect(concepts[0].id).toBe('@conceptID1'); 68 | expect(concepts[1].id).toBe('@conceptID2'); 69 | done(); 70 | }) 71 | .catch(errorHandler.bind(done)); 72 | }); 73 | 74 | it('Gets concept', done => { 75 | mock.onGet(BASE_URL + '/v2/concepts/%40conceptID').reply(200, JSON.parse(` 76 | { 77 | "status": { 78 | "code": 10000, 79 | "description": "Ok" 80 | }, 81 | "concept": { 82 | "id": "@conceptID", 83 | "name": "@conceptName", 84 | "created_at": "2017-10-02T11:34:20.419915Z", 85 | "language": "en", 86 | "app_id": "@appID" 87 | } 88 | } 89 | `)); 90 | 91 | app.concepts.get('@conceptID') 92 | .then(concept => { 93 | expect(mock.history.get.length).toBe(1); 94 | 95 | expect(concept.id).toBe('@conceptID'); 96 | expect(concept.name).toBe('@conceptName'); 97 | expect(concept.appId).toBe('@appID'); 98 | 99 | done(); 100 | }) 101 | .catch(errorHandler.bind(done)); 102 | }); 103 | 104 | it('Lists concepts', done => { 105 | mock.onGet(BASE_URL + '/v2/concepts').reply(200, JSON.parse(` 106 | { 107 | "status": { 108 | "code": 10000, 109 | "description": "Ok" 110 | }, 111 | "concepts": [{ 112 | "id": "@conceptID1", 113 | "name": "@conceptName1", 114 | "created_at": "2017-10-15T16:28:28.901994Z", 115 | "language": "en", 116 | "app_id": "@appID" 117 | }, { 118 | "id": "@conceptID2", 119 | "name": "@conceptName2", 120 | "created_at": "2017-10-15T16:26:46.667104Z", 121 | "language": "en", 122 | "app_id": "@appID" 123 | }] 124 | } 125 | `)); 126 | 127 | app.concepts.list() 128 | .then(concepts => { 129 | expect(mock.history.get.length).toBe(1); 130 | 131 | let concept1 = concepts[0]; 132 | expect(concept1.id).toBe('@conceptID1'); 133 | expect(concept1.name).toBe('@conceptName1'); 134 | expect(concept1.appId).toBe('@appID'); 135 | 136 | let concept2 = concepts[1]; 137 | expect(concept2.id).toBe('@conceptID2'); 138 | expect(concept2.name).toBe('@conceptName2'); 139 | expect(concept2.appId).toBe('@appID'); 140 | 141 | done(); 142 | }) 143 | .catch(errorHandler.bind(done)); 144 | }); 145 | 146 | it('Update concept', done => { 147 | mock.onPatch(BASE_URL + '/v2/concepts').reply(200, JSON.parse(` 148 | { 149 | "status": { 150 | "code": 10000, 151 | "description": "Ok" 152 | }, 153 | "concepts": [ 154 | { 155 | "id": "@conceptID", 156 | "name": "@newConceptName", 157 | "value": 1, 158 | "created_at": "2019-01-15T14:11:43.864812079Z", 159 | "language": "en", 160 | "app_id": "@appID" 161 | } 162 | ] 163 | } 164 | `)); 165 | 166 | app.concepts.update({id: '@conceptID', name: '@newConceptName'}) 167 | .then(concepts => { 168 | expect(mock.history.patch.length).toBe(1); 169 | expect(JSON.parse(mock.history.patch[0].data)).toEqual(JSON.parse(` 170 | { 171 | "concepts": [ 172 | { 173 | "id": "@conceptID", 174 | "name": "@newConceptName" 175 | } 176 | ], 177 | "action": "overwrite" 178 | } 179 | `)); 180 | 181 | expect(concepts[0].id).toBe('@conceptID'); 182 | expect(concepts[0].name).toBe('@newConceptName'); 183 | done(); 184 | }) 185 | .catch(errorHandler.bind(done)); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /tests/integration/workflows-int-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {errorHandler} = require('./helpers'); 3 | const {sampleImages} = require('../assets/test-data'); 4 | const generalModelVersionId = 'aa9ca48295b37401f8af92ad1af0d91d'; 5 | const Workflows = require('./../../src/Workflows'); 6 | 7 | let app; 8 | let testWorkflowId; 9 | 10 | describe('Integration Tests - Workflows', () => { 11 | beforeAll(function() { 12 | app = new Clarifai.App({ 13 | apiKey: process.env.CLARIFAI_API_KEY, 14 | apiEndpoint: process.env.API_ENDPOINT 15 | }); 16 | }); 17 | 18 | it('Gets all workflows', done => { 19 | app.workflows.list({ 20 | page: 1, 21 | perPage: 5 22 | }) 23 | .then(workflows => { 24 | expect(workflows).toBeDefined(); 25 | expect(workflows instanceof Workflows).toBe(true); 26 | 27 | for (let i = 0; i < workflows.length; i++) { 28 | let workflow = workflows[i]; 29 | expect(workflow.id).toBeDefined(); 30 | expect(workflow.appId).toBeDefined(); 31 | expect(workflow.createdAt).toBeDefined(); 32 | } 33 | 34 | done(); 35 | }) 36 | .catch(errorHandler.bind(done)); 37 | }); 38 | 39 | it('Call given workflow id with one input', done => { 40 | testWorkflowId = 'big-bang' + Date.now(); 41 | app.workflows.create(testWorkflowId, { 42 | modelId: Clarifai.GENERAL_MODEL, 43 | modelVersionId: generalModelVersionId 44 | }) 45 | .then(workflowId => { 46 | return app.workflow.predict(workflowId, sampleImages[0]); 47 | }) 48 | .then(response => { 49 | expect(response.workflow).toBeDefined(); 50 | const result = response.results[0]; 51 | const input = result.input; 52 | expect(input.id).toBeDefined(); 53 | expect(input.data).toBeDefined(); 54 | const outputs = result.outputs; 55 | const output = outputs[0]; 56 | expect(output.id).toBeDefined(); 57 | expect(output.status).toBeDefined(); 58 | expect(output.created_at).toBeDefined(); 59 | expect(output.model).toBeDefined(); 60 | expect(output.model.model_version).toBeDefined(); 61 | }) 62 | .then(() => { 63 | app.workflows.delete(testWorkflowId); 64 | done(); 65 | }) 66 | .catch(errorHandler.bind(done)); 67 | }); 68 | 69 | it('Call given workflow id with multiple inputs with specified types', done => { 70 | testWorkflowId = 'big-bang' + Date.now(); 71 | app.workflows.create(testWorkflowId, { 72 | modelId: Clarifai.GENERAL_MODEL, 73 | modelVersionId: generalModelVersionId 74 | }) 75 | .then(() => { 76 | return app.workflow.predict(testWorkflowId, [ 77 | { 78 | url: sampleImages[0], 79 | allowDuplicateUrl: true 80 | }, 81 | { 82 | url: sampleImages[1], 83 | allowDuplicateUrl: true 84 | } 85 | ]); 86 | }) 87 | .then(response => { 88 | expect(response.workflow).toBeDefined(); 89 | const results = response.results; 90 | expect(results.length).toBe(2); 91 | const result = results[0]; 92 | const input = result.input; 93 | expect(input.id).toBeDefined(); 94 | expect(input.data).toBeDefined(); 95 | const output = result.outputs[0]; 96 | expect(output.id).toBeDefined(); 97 | expect(output.status).toBeDefined(); 98 | expect(output.created_at).toBeDefined(); 99 | expect(output.model).toBeDefined(); 100 | expect(output.model.model_version).toBeDefined(); 101 | }) 102 | .then(() => { 103 | app.workflows.delete(testWorkflowId); 104 | done(); 105 | }) 106 | .catch(errorHandler.bind(done)); 107 | }); 108 | 109 | it('Call given workflow id with multiple inputs without specified types', done => { 110 | testWorkflowId = 'big-bang' + Date.now(); 111 | app.workflows.create(testWorkflowId, { 112 | modelId: Clarifai.GENERAL_MODEL, 113 | modelVersionId: generalModelVersionId 114 | }) 115 | .then(() => { 116 | return app.workflow.predict(testWorkflowId, [ 117 | sampleImages[2], sampleImages[3] 118 | ]); 119 | }) 120 | .then(response => { 121 | expect(response.workflow).toBeDefined(); 122 | const results = response.results; 123 | expect(results.length).toBe(2); 124 | const result = results[0]; 125 | const input = result.input; 126 | expect(input.id).toBeDefined(); 127 | expect(input.data).toBeDefined(); 128 | const output = result.outputs[0]; 129 | expect(output.id).toBeDefined(); 130 | expect(output.status).toBeDefined(); 131 | expect(output.created_at).toBeDefined(); 132 | expect(output.model).toBeDefined(); 133 | expect(output.model.model_version).toBeDefined(); 134 | return app.workflow.delete(testWorkflowId); 135 | }) 136 | .then(() => { 137 | app.workflows.delete(testWorkflowId); 138 | done(); 139 | }) 140 | .catch(errorHandler.bind(done)); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | let axios = require('axios'); 2 | let {checkType} = require('./helpers'); 3 | let Models = require('./Models'); 4 | let Inputs = require('./Inputs'); 5 | let Concepts = require('./Concepts'); 6 | let Workflow = require('./Workflow'); 7 | let Workflows = require('./Workflows'); 8 | let Solutions = require('./solutions/Solutions'); 9 | let {API, ERRORS, getBasePath} = require('./constants'); 10 | let {TOKEN_PATH} = API; 11 | 12 | if (typeof window !== 'undefined' && !('Promise' in window)) { 13 | window.Promise = require('promise'); 14 | } 15 | 16 | if (typeof global !== 'undefined' && !('Promise' in global)) { 17 | global.Promise = require('promise'); 18 | } 19 | 20 | /** 21 | * top-level class that allows access to models, inputs and concepts 22 | * @class 23 | */ 24 | class App { 25 | constructor(arg1, arg2, arg3) { 26 | let optionsObj = arg1; 27 | if (typeof arg1 !== 'object' || arg1 === null) { 28 | optionsObj = arg3 || {}; 29 | optionsObj.clientId = arg1; 30 | optionsObj.clientSecret = arg2; 31 | } 32 | this._validate(optionsObj); 33 | this._init(optionsObj); 34 | 35 | } 36 | 37 | /** 38 | * Gets a token from the API using client credentials 39 | * @return {Promise(token, error)} A Promise that is fulfilled with the token string or rejected with an error 40 | * 41 | * @deprecated Please switch to using the API key. 42 | */ 43 | getToken() { 44 | return this._config.token(); 45 | } 46 | 47 | /** 48 | * Sets the token to use for the API 49 | * @param {String} _token The token you are setting 50 | * @return {Boolean} true if token has valid fields, false if not 51 | * 52 | * @deprecated Please switch to using the API key. 53 | */ 54 | setToken(_token) { 55 | let token = _token; 56 | let now = new Date().getTime(); 57 | if (typeof _token === 'string') { 58 | token = { 59 | accessToken: _token, 60 | expiresIn: 176400 61 | }; 62 | } else { 63 | token = { 64 | accessToken: _token.access_token || _token.accessToken, 65 | expiresIn: _token.expires_in || _token.expiresIn 66 | }; 67 | } 68 | if ((token.accessToken && token.expiresIn) || 69 | (token.access_token && token.expires_in)) { 70 | if (!token.expireTime) { 71 | token.expireTime = now + (token.expiresIn * 1000); 72 | } 73 | this._config._token = token; 74 | return true; 75 | } 76 | return false; 77 | } 78 | 79 | _validate({clientId, clientSecret, token, apiKey, sessionToken}) { 80 | if (clientId || clientSecret) { 81 | console.warn('Client ID/secret has been deprecated. Please switch to using the API key. See here how to do ' + 82 | 'the switch: https://blog.clarifai.com/introducing-api-keys-a-safer-way-to-authenticate-your-applications'); 83 | } 84 | if ((!clientId || !clientSecret) && !token && !apiKey && !sessionToken) { 85 | throw ERRORS.paramsRequired(['apiKey']); 86 | } 87 | } 88 | 89 | _init(options) { 90 | let apiEndpoint = options.apiEndpoint || 91 | (process && process.env && process.env.API_ENDPOINT) || 'https://api.clarifai.com'; 92 | this._config = { 93 | apiEndpoint, 94 | clientId: options.clientId, 95 | clientSecret: options.clientSecret, 96 | apiKey: options.apiKey, 97 | sessionToken: options.sessionToken, 98 | basePath: getBasePath(apiEndpoint, options.userId, options.appId), 99 | token: () => { 100 | return new Promise((resolve, reject) => { 101 | let now = new Date().getTime(); 102 | if (checkType(/Object/, this._config._token) && this._config._token.expireTime > now) { 103 | resolve(this._config._token); 104 | } else { 105 | this._getToken(resolve, reject); 106 | } 107 | }); 108 | } 109 | }; 110 | if (options.token) { 111 | this.setToken(options.token); 112 | } 113 | this.models = new Models(this._config); 114 | this.inputs = new Inputs(this._config); 115 | this.concepts = new Concepts(this._config); 116 | this.workflow = new Workflow(this._config); 117 | this.workflows = new Workflows(this._config); 118 | this.solutions = new Solutions(this._config); 119 | } 120 | 121 | /** 122 | * @deprecated Please switch to using the API key. 123 | */ 124 | _getToken(resolve, reject) { 125 | this._requestToken().then( 126 | (response) => { 127 | if (response.status === 200) { 128 | this.setToken(response.data); 129 | resolve(this._config._token); 130 | } else { 131 | reject(response); 132 | } 133 | }, 134 | reject 135 | ); 136 | } 137 | 138 | /** 139 | * @deprecated Please switch to using the API key. 140 | */ 141 | _requestToken() { 142 | let url = `${this._config.basePath}${TOKEN_PATH}`; 143 | let clientId = this._config.clientId; 144 | let clientSecret = this._config.clientSecret; 145 | return axios({ 146 | 'url': url, 147 | 'method': 'POST', 148 | 'auth': { 149 | 'username': clientId, 150 | 'password': clientSecret 151 | } 152 | }); 153 | } 154 | } 155 | 156 | module.exports = App; 157 | -------------------------------------------------------------------------------- /tests/unit/search-inputs-unit-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {BASE_URL, SAMPLE_API_KEY} = require('./helpers'); 3 | const axios = require('axios'); 4 | const MockAdapter = require('axios-mock-adapter'); 5 | const {errorHandler} = require('../integration/helpers'); 6 | 7 | let app; 8 | 9 | let mock; 10 | 11 | describe('Unit Tests - Input Search', () => { 12 | beforeAll(() => { 13 | app = new Clarifai.App({ 14 | apiKey: SAMPLE_API_KEY, 15 | apiEndpoint: BASE_URL 16 | }); 17 | }); 18 | 19 | beforeEach(() => { 20 | mock = new MockAdapter(axios); 21 | }); 22 | 23 | it('Search inputs by concept ID', done => { 24 | mock.onPost(BASE_URL + '/v2/searches').reply(200, JSON.parse(` 25 | { 26 | "status": { 27 | "code": 10000, 28 | "description": "Ok" 29 | }, 30 | "hits": [ 31 | { 32 | "score": 0.99, 33 | "input": { 34 | "id": "@inputID", 35 | "created_at": "2016-11-22T17:06:02Z", 36 | "data": { 37 | "image": { 38 | "url": "@inputURL" 39 | } 40 | }, 41 | "status": { 42 | "code": 30000, 43 | "description": "Download complete" 44 | } 45 | } 46 | } 47 | ] 48 | } 49 | `)); 50 | 51 | 52 | app.inputs.search({concept: {id: '@conceptID'}}) 53 | .then(response => { 54 | expect(mock.history.post.length).toBe(1); 55 | expect(JSON.parse(mock.history.post[0].data)).toEqual(JSON.parse(` 56 | { 57 | "query": { 58 | "ands": [ 59 | { 60 | "output": { 61 | "data": { 62 | "concepts": [ 63 | { 64 | "id": "@conceptID" 65 | } 66 | ] 67 | } 68 | } 69 | } 70 | ] 71 | }, 72 | "pagination": { 73 | "page": 1, 74 | "per_page": 20 75 | } 76 | } 77 | `)); 78 | 79 | expect(response.hits[0].score).toEqual(0.99); 80 | expect(response.hits[0].input.id).toEqual('@inputID'); 81 | expect(response.hits[0].input.data.image.url).toEqual('@inputURL'); 82 | 83 | done(); 84 | }) 85 | .catch(errorHandler.bind(done)); 86 | }); 87 | 88 | it('Search inputs by concept name', done => { 89 | mock.onPost(BASE_URL + '/v2/searches').reply(200, JSON.parse(` 90 | { 91 | "status": { 92 | "code": 10000, 93 | "description": "Ok" 94 | }, 95 | "hits": [ 96 | { 97 | "score": 0.99, 98 | "input": { 99 | "id": "@inputID", 100 | "created_at": "2016-11-22T17:06:02Z", 101 | "data": { 102 | "image": { 103 | "url": "@inputURL" 104 | } 105 | }, 106 | "status": { 107 | "code": 30000, 108 | "description": "Download complete" 109 | } 110 | } 111 | } 112 | ] 113 | } 114 | `)); 115 | 116 | app.inputs.search({concept: {name: '@conceptName'}}) 117 | .then(response => { 118 | expect(mock.history.post.length).toBe(1); 119 | expect(JSON.parse(mock.history.post[0].data)).toEqual(JSON.parse(` 120 | { 121 | "query": { 122 | "ands": [ 123 | { 124 | "output": { 125 | "data": { 126 | "concepts": [ 127 | { 128 | "name": "@conceptName" 129 | } 130 | ] 131 | } 132 | } 133 | } 134 | ] 135 | }, 136 | "pagination": { 137 | "page": 1, 138 | "per_page": 20 139 | } 140 | } 141 | `)); 142 | 143 | expect(response.hits[0].score).toEqual(0.99); 144 | expect(response.hits[0].input.id).toEqual('@inputID'); 145 | expect(response.hits[0].input.data.image.url).toEqual('@inputURL'); 146 | 147 | done(); 148 | }) 149 | .catch(errorHandler.bind(done)); 150 | }); 151 | 152 | it('Search inputs by geo location', done => { 153 | mock.onPost(BASE_URL + '/v2/searches').reply(200, JSON.parse(` 154 | { 155 | "status": { 156 | "code": 10000, 157 | "description": "Ok" 158 | }, 159 | "hits": [ 160 | { 161 | "score": 0.99, 162 | "input": { 163 | "id": "@inputID", 164 | "created_at": "2016-11-22T17:06:02Z", 165 | "data": { 166 | "image": { 167 | "url": "@inputURL" 168 | } 169 | }, 170 | "status": { 171 | "code": 30000, 172 | "description": "Download complete" 173 | } 174 | } 175 | } 176 | ] 177 | } 178 | `)); 179 | 180 | app.inputs.search({input: {geo: {longitude: 1.5, latitude: -1, type: 'withinKilometers', value: 1}}}) 181 | .then(response => { 182 | expect(mock.history.post.length).toBe(1); 183 | expect(JSON.parse(mock.history.post[0].data)).toEqual(JSON.parse(` 184 | { 185 | "query": { 186 | "ands": [ 187 | { 188 | "input": { 189 | "data": { 190 | "geo": { 191 | "geo_point": { 192 | "longitude": 1.5, 193 | "latitude": -1 194 | }, 195 | "geo_limit": { 196 | "type": "withinKilometers", 197 | "value": 1 198 | } 199 | } 200 | } 201 | } 202 | } 203 | ] 204 | }, 205 | "pagination": { 206 | "page": 1, 207 | "per_page": 20 208 | } 209 | } 210 | `)); 211 | 212 | expect(response.hits[0].score).toEqual(0.99); 213 | expect(response.hits[0].input.id).toEqual('@inputID'); 214 | expect(response.hits[0].input.data.image.url).toEqual('@inputURL'); 215 | 216 | done(); 217 | }) 218 | .catch(errorHandler.bind(done)); 219 | }); 220 | }); 221 | -------------------------------------------------------------------------------- /tests/integration/delete-int-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const Model = require('./../../src/Model'); 3 | const {errorHandler, waitForInputsUpload} = require('./helpers'); 4 | const {sampleImages} = require('../assets/test-data'); 5 | 6 | let app; 7 | let testModelVersionId; 8 | let inputsIDs = []; 9 | let d = Date.now(); 10 | let testModelId = 'vroom-vroom' + d; 11 | 12 | describe('Integration Tests - Delete Resources', () => { 13 | 14 | beforeAll(done => { 15 | app = new Clarifai.App({ 16 | apiKey: process.env.CLARIFAI_API_KEY, 17 | apiEndpoint: process.env.API_ENDPOINT, 18 | }); 19 | 20 | app.inputs.create([ 21 | { 22 | url: sampleImages[0], 23 | allowDuplicateUrl: true 24 | }, 25 | { 26 | url: sampleImages[1], 27 | allowDuplicateUrl: true 28 | } 29 | ]) 30 | .then(inputs => { 31 | for (var i=0; i { 36 | return waitForInputsUpload(app); 37 | }) 38 | .then(() => { 39 | done(); 40 | }) 41 | .catch(errorHandler.bind(done)); 42 | }); 43 | 44 | it('Allows you to delete select inputs', done => { 45 | app.inputs.delete(inputsIDs.slice(0, 1)) 46 | .then(response => { 47 | var data = response.data; 48 | expect(data.status).toBeDefined(); 49 | expect(data.status.code).toBe(10000); 50 | expect(data.status.description).toBe('Ok'); 51 | done(); 52 | }) 53 | .catch(errorHandler.bind(done)); 54 | }); 55 | 56 | it('Allows you to delete all inputs', done => { 57 | app.inputs.delete() 58 | .then(response => { 59 | var data = response.data; 60 | expect(data.status).toBeDefined(); 61 | expect(data.status.code).toBe(10000); 62 | expect(data.status.description).toBe('Ok'); 63 | done(); 64 | }) 65 | .catch(errorHandler.bind(done)); 66 | }); 67 | 68 | it('Throws an error if model delete arguments list is incorrect', done => { 69 | expect(() => { 70 | app.models.delete(['model-1', 'model-2'], 'version-1'); 71 | }).toThrow(); 72 | done(); 73 | }); 74 | 75 | it('Allows you to delete a single model version', done => { 76 | app.models.create(testModelId) 77 | .then(response => { 78 | testModelVersionId = response.modelVersion.id; 79 | return app.models.delete(testModelId, testModelVersionId); 80 | }) 81 | .then(response => { 82 | expect(response.status).toBeDefined(); 83 | expect(response.status.code).toBe(10000); 84 | expect(response.status.description).toBe('Ok'); 85 | done(); 86 | }) 87 | .catch(errorHandler.bind(done)); 88 | }); 89 | 90 | it('Allows you to delete a list of models', done => { 91 | var modelIds = [ 92 | 'abc' + Date.now(), 93 | 'def' + Date.now(), 94 | 'ghi' + Date.now(), 95 | 'jkl' + Date.now() 96 | ]; 97 | var completed = 0; 98 | var totalToDelete = 4; 99 | 100 | modelIds.forEach(modelId => { 101 | app.models.create(modelId) 102 | .then(response => { 103 | completed++; 104 | if (completed === totalToDelete) { 105 | app.models.delete(modelIds) 106 | .then(response => { 107 | expect(response.status).toBeDefined(); 108 | expect(response.status.code).toBe(10000); 109 | expect(response.status.description).toBe('Ok'); 110 | done(); 111 | }) 112 | .catch(errorHandler.bind(done)); 113 | } 114 | }) 115 | .catch(errorHandler.bind(done)); 116 | }); 117 | }); 118 | 119 | it('Allows you to delete a single model', done => { 120 | var modelId = 'abc' + Date.now(); 121 | app.models.create(modelId) 122 | .then(response => { 123 | return app.models.delete(modelId); 124 | }) 125 | .then(response => { 126 | expect(response.status).toBeDefined(); 127 | expect(response.status.code).toBe(10000); 128 | expect(response.status.description).toBe('Ok'); 129 | done(); 130 | }) 131 | .catch(errorHandler.bind(done)); 132 | }); 133 | 134 | it('Allows you to delete a single model with id given in array', done => { 135 | var modelId = 'abc' + Date.now(); 136 | app.models.create(modelId) 137 | .then(response => { 138 | return app.models.delete([modelId]); 139 | }) 140 | .then(response => { 141 | expect(response.status).toBeDefined(); 142 | expect(response.status.code).toBe(10000); 143 | expect(response.status.description).toBe('Ok'); 144 | done(); 145 | }) 146 | .catch(errorHandler.bind(done)); 147 | }); 148 | 149 | it('Allows you to have special chars in model id', done => { 150 | var modelId = 'whois?' + Date.now(); 151 | app.models.create(modelId) 152 | .then(response => { 153 | return app.models.get(modelId); 154 | }) 155 | .then(response => { 156 | expect(response instanceof Model).toBe(true); 157 | expect(response.rawData.id).toBe(modelId); 158 | done(); 159 | }) 160 | .catch(errorHandler.bind(done)); 161 | }); 162 | 163 | it('Allows you to delete all models', done => { 164 | app.models.delete() 165 | .then(response => { 166 | expect(response.status).toBeDefined(); 167 | expect(response.status.code).toBe(10000); 168 | expect(response.status.description).toBe('Ok'); 169 | done(); 170 | }) 171 | .catch(errorHandler.bind(done)); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /src/Concepts.js: -------------------------------------------------------------------------------- 1 | let axios = require('axios'); 2 | let Concept = require('./Concept'); 3 | let {API, replaceVars} = require('./constants'); 4 | let {CONCEPTS_PATH, CONCEPT_PATH, CONCEPT_SEARCH_PATH} = API; 5 | let {wrapToken, formatConcept} = require('./utils'); 6 | let {isSuccess, checkType} = require('./helpers'); 7 | 8 | /** 9 | * class representing a collection of concepts 10 | * @class 11 | */ 12 | class Concepts { 13 | constructor(_config, rawData = []) { 14 | this._config = _config; 15 | this.rawData = rawData; 16 | rawData.forEach((conceptData, index) => { 17 | this[index] = new Concept(this._config, conceptData); 18 | }); 19 | this.length = rawData.length; 20 | } 21 | 22 | /** 23 | * List all the concepts 24 | * @param {object} options Object with keys explained below: (optional) 25 | * @param {number} options.page The page number (optional, default: 1) 26 | * @param {number} options.perPage Number of images to return per page (optional, default: 20) 27 | * @return {Promise(Concepts, error)} A Promise that is fulfilled with a Concepts instance or rejected with an error 28 | */ 29 | list(options = {page: 1, perPage: 20}) { 30 | let url = `${this._config.basePath}${CONCEPTS_PATH}`; 31 | return wrapToken(this._config, (headers) => { 32 | return new Promise((resolve, reject) => { 33 | axios.get(url, { 34 | headers, 35 | params: { 36 | 'page': options.page, 37 | 'per_page': options.perPage, 38 | } 39 | }).then((response) => { 40 | if (isSuccess(response)) { 41 | resolve(new Concepts(this._config, response.data.concepts)); 42 | } else { 43 | reject(response); 44 | } 45 | }, reject); 46 | }); 47 | }); 48 | } 49 | 50 | /** 51 | * List a single concept given an id 52 | * @param {String} id The concept's id 53 | * @return {Promise(Concept, error)} A Promise that is fulfilled with a Concept instance or rejected with an error 54 | */ 55 | get(id) { 56 | let url = `${this._config.basePath}${replaceVars(CONCEPT_PATH, [id])}`; 57 | return wrapToken(this._config, (headers) => { 58 | return new Promise((resolve, reject) => { 59 | axios.get(url, {headers}).then((response) => { 60 | if (isSuccess(response)) { 61 | resolve(new Concept(this._config, response.data.concept)); 62 | } else { 63 | reject(response); 64 | } 65 | }, reject); 66 | }); 67 | }); 68 | } 69 | 70 | /** 71 | * Add a list of concepts given an id and name 72 | * @param {object|object[]} concepts Can be a single media object or an array of media objects 73 | * @param {object|string} concepts[].concept If string, this is assumed to be the concept id. Otherwise, an object with the following attributes 74 | * @param {object} concepts[].concept.id The new concept's id (Required) 75 | * @param {object} concepts[].concept.name The new concept's name 76 | * @return {Promise(Concepts, error)} A Promise that is fulfilled with a Concepts instance or rejected with an error 77 | */ 78 | create(concepts = []) { 79 | if (checkType(/(Object|String)/, concepts)) { 80 | concepts = [concepts]; 81 | } 82 | let data = { 83 | 'concepts': concepts.map(formatConcept) 84 | }; 85 | let url = `${this._config.basePath}${CONCEPTS_PATH}`; 86 | return wrapToken(this._config, (headers) => { 87 | return new Promise((resolve, reject) => { 88 | axios.post(url, data, {headers}) 89 | .then((response) => { 90 | if (isSuccess(response)) { 91 | resolve(new Concepts(this._config, response.data.concepts)); 92 | } else { 93 | reject(response); 94 | } 95 | }, reject); 96 | }); 97 | }); 98 | } 99 | 100 | /** 101 | * Search for a concept given a name. A wildcard can be given (example: The name "bo*" will match with "boat" and "bow" given those concepts exist 102 | * @param {string} name The name of the concept to search for 103 | * @return {Promise(Concepts, error)} A Promise that is fulfilled with a Concepts instance or rejected with an error 104 | */ 105 | search(name, language = null) { 106 | let url = `${this._config.basePath}${CONCEPT_SEARCH_PATH}`; 107 | return wrapToken(this._config, (headers) => { 108 | let params = { 109 | 'concept_query': {name, language} 110 | }; 111 | return new Promise((resolve, reject) => { 112 | axios.post(url, params, {headers}).then((response) => { 113 | if (isSuccess(response)) { 114 | resolve(new Concepts(this._config, response.data.concepts)); 115 | } else { 116 | reject(response); 117 | } 118 | }, reject); 119 | }); 120 | }); 121 | } 122 | 123 | /** 124 | * Update a concepts 125 | * @param {object|object[]} concepts Can be a single concept object or an array of concept objects 126 | * @param {object} concepts[].concept A concept object with the following attributes 127 | * @param {object} concepts[].concept.id The concept's id (Required) 128 | * @param {object} concepts[].concept.name The concept's new name 129 | * @param {string} [action=overwrite] The action to use for the PATCH 130 | * @return {Promise(Concepts, error)} A Promise that is fulfilled with a Concepts instance or rejected with an error 131 | */ 132 | update(concepts = [], action = 'overwrite') { 133 | if (!checkType(/Array/, concepts)) { 134 | concepts = [concepts]; 135 | } 136 | const data = { 137 | concepts, 138 | action 139 | }; 140 | const url = `${this._config.basePath}${CONCEPTS_PATH}`; 141 | return wrapToken(this._config, headers => { 142 | return new Promise((resolve, reject) => { 143 | axios.patch(url, data, { headers }) 144 | .then((response) => { 145 | if (isSuccess(response)) { 146 | resolve(new Concepts(this._config, response.data.concepts)); 147 | } else { 148 | reject(response); 149 | } 150 | }, reject); 151 | }); 152 | }); 153 | } 154 | }; 155 | 156 | module.exports = Concepts; 157 | -------------------------------------------------------------------------------- /scripts/app_and_key_for_tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | 5 | try: 6 | from urllib.parse import urlparse, urlencode 7 | from urllib.request import urlopen, Request, build_opener, HTTPHandler 8 | from urllib.error import HTTPError 9 | except ImportError: 10 | from urlparse import urlparse 11 | from urllib import urlencode 12 | from urllib2 import urlopen, Request, HTTPError, build_opener, HTTPHandler 13 | 14 | 15 | EMAIL = os.environ['CLARIFAI_USER_EMAIL'] 16 | PASSWORD = os.environ['CLARIFAI_USER_PASSWORD'] 17 | 18 | 19 | BASE = 'https://api.clarifai.com/v2' 20 | 21 | 22 | def _request(method, url, payload={}, headers={}): 23 | opener = build_opener(HTTPHandler) 24 | full_url = '%s%s' % (BASE, url) 25 | request = Request(full_url, data=json.dumps(payload).encode()) 26 | for k in headers.keys(): 27 | request.add_header(k, headers[k]) 28 | request.get_method = lambda: method 29 | return json.loads(opener.open(request).read().decode()) 30 | 31 | 32 | def create_app(env_name): 33 | session_token, user_id = _login() 34 | 35 | url = '/users/%s/apps' % user_id 36 | payload = {'apps': [{'name': 'auto-created-in-%s-ci-test-run' % env_name}]} 37 | 38 | response = _request(method='POST', url=url, payload=payload, headers=_auth_headers(session_token)) 39 | 40 | _raise_on_http_error(response) 41 | data = response 42 | app_id = data['apps'][0]['id'] 43 | 44 | # This print needs to be present so we can read the value in CI. 45 | print(app_id) 46 | 47 | 48 | def create_key(app_id): 49 | session_token, user_id = _login() 50 | 51 | url = '/users/%s/keys' % user_id 52 | payload = { 53 | 'keys': [{ 54 | 'description': 'Auto-created in a CI test run', 55 | 'scopes': ['All'], 56 | 'apps': [{'id': app_id, 'user_id': user_id}] 57 | }] 58 | } 59 | response = _request(method='POST', url=url, payload=payload, headers=_auth_headers(session_token)) 60 | _raise_on_http_error(response) 61 | data = response 62 | key_id = data['keys'][0]['id'] 63 | 64 | # This print needs to be present so we can read the value in CI. 65 | print(key_id) 66 | 67 | 68 | def delete(app_id): 69 | session_token, user_id = _login() 70 | 71 | # All the related keys will be deleted automatically when the app is deleted 72 | _delete_app(session_token, user_id, app_id) 73 | 74 | 75 | def create_sample_workflow(api_key): 76 | url = '/workflows' 77 | payload = { 78 | 'workflows': [ 79 | { 80 | 'id': 'food-and-general', 81 | 'nodes': [ 82 | { 83 | 'id': 'food-workflow-node', 84 | 'model': { 85 | 'id': 'bd367be194cf45149e75f01d59f77ba7', 86 | 'model_version': { 87 | 'id': 'dfebc169854e429086aceb8368662641' 88 | } 89 | } 90 | }, 91 | { 92 | 'id': 'general-workflow-node', 93 | 'model': { 94 | 'id': 'aaa03c23b3724a16a56b629203edc62c', 95 | 'model_version': { 96 | 'id': 'aa9ca48295b37401f8af92ad1af0d91d' 97 | } 98 | } 99 | } 100 | ] 101 | } 102 | ] 103 | } 104 | response = _request(method='POST', url=url, payload=payload, headers=_auth_headers_for_api_key_key(api_key)) 105 | _raise_on_http_error(response) 106 | 107 | 108 | def _delete_app(session_token, user_id, app_id): 109 | url = '/users/%s/apps/%s' % (user_id, app_id) 110 | response = _request(method='DELETE', url=url, headers=_auth_headers(session_token)) 111 | _raise_on_http_error(response) 112 | 113 | 114 | def _auth_headers(session_token): 115 | headers = {'Content-Type': 'application/json', 'X-Clarifai-Session-Token': session_token} 116 | return headers 117 | 118 | 119 | def _auth_headers_for_api_key_key(api_key): 120 | headers = {'Content-Type': 'application/json', 'Authorization': 'Key ' + api_key} 121 | return headers 122 | 123 | 124 | def _login(): 125 | url = '/login' 126 | payload = {'email': EMAIL, 'password': PASSWORD} 127 | response = _request(method='POST', url=url, payload=payload) 128 | _raise_on_http_error(response) 129 | data = response 130 | user_id = data['v2_user_id'] 131 | session_token = data['session_token'] 132 | return session_token, user_id 133 | 134 | 135 | def _raise_on_http_error(response): 136 | # TODO: Make this work with urllib. 137 | # if int(response.status_code) // 100 != 2: 138 | # raise Exception('Unexpected response %s: %s' % (response.status_code, response.text)) 139 | pass 140 | 141 | 142 | def run(arguments): 143 | command = arguments[0] if arguments else '--help' 144 | if command == '--create-app': 145 | if len(arguments) != 2: 146 | raise Exception('--create-app takes one argument') 147 | 148 | env_name = arguments[1] 149 | create_app(env_name) 150 | elif command == '--create-key': 151 | if len(arguments) != 2: 152 | raise Exception('--create-key takes one argument') 153 | 154 | app_id = arguments[1] 155 | create_key(app_id) 156 | elif command == '--delete-app': 157 | if len(arguments) != 2: 158 | raise Exception('--delete-app takes one argument') 159 | app_id = arguments[1] 160 | delete(app_id) 161 | elif command == '--create-workflow': 162 | if len(arguments) != 2: 163 | raise Exception('--create-workflow takes one argument') 164 | api_key = arguments[1] 165 | create_sample_workflow(api_key) 166 | elif command == '--help': 167 | print('''DESCRIPTION: Creates and delete applications and API keys 168 | ARGUMENTS: 169 | --create-app [env_name] ... Creates a new application. 170 | --create-key [app_id] ... Creates a new API key. 171 | --delete-app [app_id] ... Deletes an application (API keys that use it are deleted as well). 172 | --create-workflow [api_key] ... Creates a sample workflow to be used in int. tests. 173 | --help ... This text.''') 174 | else: 175 | print('Unknown argument. Please see --help') 176 | exit(1) 177 | 178 | 179 | if __name__ == '__main__': 180 | run(arguments=sys.argv[1:]) 181 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs'); 2 | let Promise = require('promise'); 3 | let validUrl = require('valid-url'); 4 | let {GEO_LIMIT_TYPES, ERRORS} = require('./constants'); 5 | let {checkType, clone} = require('./helpers'); 6 | let {version: VERSION} = require('./../package.json'); 7 | 8 | module.exports = { 9 | wrapToken: (_config, requestFn) => { 10 | return new Promise((resolve, reject) => { 11 | if (_config.apiKey) { 12 | let headers = { 13 | Authorization: `Key ${_config.apiKey}`, 14 | 'X-Clarifai-Client': `js:${VERSION}` 15 | }; 16 | return requestFn(headers).then(resolve, reject); 17 | } 18 | if (_config.sessionToken) { 19 | let headers = { 20 | 'X-Clarifai-Session-Token': _config.sessionToken, 21 | 'X-Clarifai-Client': `js:${VERSION}` 22 | }; 23 | return requestFn(headers).then(resolve, reject); 24 | } 25 | _config.token().then((token) => { 26 | let headers = { 27 | Authorization: `Bearer ${token.accessToken}`, 28 | 'X-Clarifai-Client': `js:${VERSION}` 29 | }; 30 | requestFn(headers).then(resolve, reject); 31 | }, reject); 32 | }); 33 | }, 34 | formatModel: (data = {}) => { 35 | let formatted = {}; 36 | if (data.id === null || data.id === undefined) { 37 | throw ERRORS.paramsRequired('Model ID'); 38 | } 39 | formatted.id = data.id; 40 | if (data.name) { 41 | formatted.name = data.name; 42 | } 43 | formatted.output_info = {}; 44 | if (data.conceptsMutuallyExclusive !== undefined) { 45 | formatted.output_info.output_config = formatted.output_info.output_config || {}; 46 | formatted.output_info.output_config.concepts_mutually_exclusive = !!data.conceptsMutuallyExclusive; 47 | } 48 | if (data.closedEnvironment !== undefined) { 49 | formatted.output_info.output_config = formatted.output_info.output_config || {}; 50 | formatted.output_info.output_config.closed_environment = !!data.closedEnvironment; 51 | } 52 | if (data.concepts) { 53 | formatted.output_info.data = { 54 | concepts: data.concepts.map(module.exports.formatConcept) 55 | }; 56 | } 57 | return formatted; 58 | }, 59 | formatInput: (data, includeImage) => { 60 | let input = checkType(/String/, data) ? 61 | {url: data} : 62 | data; 63 | let formatted = { 64 | id: input.id || null, 65 | data: {} 66 | }; 67 | if (input.concepts) { 68 | formatted.data.concepts = input.concepts; 69 | } 70 | if (input.metadata) { 71 | formatted.data.metadata = input.metadata; 72 | } 73 | if (input.geo) { 74 | formatted.data.geo = {geo_point: input.geo}; 75 | } 76 | if (input.regions) { 77 | formatted.data.regions = input.regions; 78 | } 79 | if (includeImage !== false) { 80 | formatted.data.image = { 81 | url: input.url, 82 | base64: input.base64 83 | }; 84 | if (data.allowDuplicateUrl) { 85 | formatted.data.image.allow_duplicate_url = true; 86 | } 87 | } 88 | return formatted; 89 | }, 90 | formatMediaPredict: (data, type = 'image') => { 91 | let media; 92 | if (checkType(/String/, data)) { 93 | if (validUrl.isWebUri(data)) { 94 | media = { 95 | url: data 96 | }; 97 | } else { 98 | media = { 99 | base64: data 100 | }; 101 | } 102 | } else { 103 | media = Object.assign({}, data); 104 | } 105 | 106 | if (media.hasOwnProperty('file')) { 107 | const bitmap = fs.readFileSync(media['file']); 108 | media = { 109 | base64: Buffer.from(bitmap).toString('base64') 110 | }; 111 | } 112 | 113 | // Users can specify their own id to distinguish batch results 114 | let id; 115 | if (media.id) { 116 | id = media.id; 117 | delete media.id; 118 | } 119 | 120 | let object = { 121 | data: { 122 | [type]: media 123 | } 124 | }; 125 | 126 | if (id) { 127 | object.id = id; 128 | } 129 | 130 | return object; 131 | }, 132 | formatImagesSearch: (image) => { 133 | let imageQuery; 134 | let input = {input: {data: {}}}; 135 | let formatted = []; 136 | if (checkType(/String/, image)) { 137 | imageQuery = {url: image}; 138 | } else { 139 | imageQuery = (image.url || image.base64) ? { 140 | image: { 141 | url: image.url, 142 | base64: image.base64 143 | } 144 | } : {}; 145 | } 146 | 147 | input.input.data = imageQuery; 148 | if (image.id) { 149 | input.input.id = image.id; 150 | input.input.data = {image: {}}; 151 | } 152 | if (image.metadata !== undefined) { 153 | input.input.data.metadata = image.metadata; 154 | } 155 | if (image.geo !== undefined) { 156 | if (checkType(/Array/, image.geo)) { 157 | input.input.data.geo = { 158 | geo_box: image.geo.map(p => { 159 | return {geo_point: p}; 160 | }) 161 | }; 162 | } else if (checkType(/Object/, image.geo)) { 163 | if (GEO_LIMIT_TYPES.indexOf(image.geo.type) === -1) { 164 | throw ERRORS.INVALID_GEOLIMIT_TYPE; 165 | } 166 | input.input.data.geo = { 167 | geo_point: { 168 | latitude: image.geo.latitude, 169 | longitude: image.geo.longitude 170 | }, 171 | geo_limit: { 172 | type: image.geo.type, 173 | value: image.geo.value 174 | } 175 | }; 176 | } 177 | } 178 | if (image.type !== 'input' && input.input.data.image) { 179 | if (input.input.data.metadata || input.input.data.geo) { 180 | let dataCopy = {input: {data: clone(input.input.data)}}; 181 | let imageCopy = {input: {data: clone(input.input.data)}}; 182 | delete dataCopy.input.data.image; 183 | delete imageCopy.input.data.metadata; 184 | delete imageCopy.input.data.geo; 185 | input = [ 186 | {output: imageCopy}, 187 | dataCopy 188 | ]; 189 | } else { 190 | input = [{output: input}]; 191 | } 192 | } 193 | formatted = formatted.concat(input); 194 | return formatted; 195 | }, 196 | formatConcept: (concept) => { 197 | let formatted = concept; 198 | if (checkType(/String/, concept)) { 199 | formatted = { 200 | id: concept 201 | }; 202 | } 203 | return formatted; 204 | }, 205 | formatConceptsSearch: (query) => { 206 | if (checkType(/String/, query)) { 207 | query = {id: query}; 208 | } 209 | let v = {}; 210 | let type = query.type === 'input' ? 'input' : 'output'; 211 | delete query.type; 212 | v[type] = { 213 | data: { 214 | concepts: [query] 215 | } 216 | }; 217 | return v; 218 | }, 219 | formatObjectForSnakeCase(obj) { 220 | return Object.keys(obj).reduce((o, k) => { 221 | o[k.replace(/([A-Z])/g, r => '_'+r.toLowerCase())] = obj[k]; 222 | return o; 223 | }, {}); 224 | } 225 | }; 226 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const gutil = require('gulp-util'); 3 | const browserify = require('gulp-browserify'); 4 | const babel = require('gulp-babel'); 5 | const notify = require('gulp-notify'); 6 | const uglify = require('gulp-uglify'); 7 | const replace = require('gulp-replace-task'); 8 | const rename = require('gulp-rename'); 9 | const insert = require('gulp-insert'); 10 | const eslint = require('gulp-eslint'); 11 | const jasmine = require('gulp-jasmine'); 12 | const del = require('del'); 13 | const VERSION = require('./package.json').version; 14 | 15 | const buildVars = { 16 | dev: { 17 | 'stage': 'dev', 18 | 'browserifyDebug': true, 19 | 'uglify': false, 20 | 'lintFailOnError': false, 21 | 'browserifyFailOnError': false 22 | }, 23 | test: { 24 | 'stage': 'test', 25 | 'browserifyDebug': true, 26 | 'uglify': false, 27 | 'lintFailOnError': true, 28 | 'browserifyFailOnError': true 29 | }, 30 | unittest: { 31 | 'stage': 'unittest', 32 | 'browserifyDebug': true, 33 | 'uglify': false, 34 | 'lintFailOnError': true, 35 | 'browserifyFailOnError': true 36 | }, 37 | staging: { 38 | 'stage': 'staging', 39 | 'browserifyDebug': false, 40 | 'uglify': true, 41 | 'lintFailOnError': true, 42 | 'browserifyFailOnError': true 43 | }, 44 | 45 | prod: { 46 | 'stage': 'prod', 47 | 'browserifyDebug': false, 48 | 'uglify': true, 49 | 'lintFailOnError': true, 50 | 'buildMock': false, 51 | 'bowserifyFailOnError': true 52 | } 53 | }; 54 | 55 | 56 | const BROWSER_HEADER = ( 57 | '/**\n' + 58 | ' * Clarifai JavaScript SDK v' + VERSION + '\n' + 59 | ' *\n' + 60 | ' * Last updated: ' + new Date() + '\n' + 61 | ' *\n' + 62 | ' * Visit https://developer.clarifai.com\n' + 63 | ' *\n' + 64 | ' * Copyright (c) 2016-present, Clarifai, Inc.\n' + 65 | ' * All rights reserved.\n' + 66 | ' * Licensed under the Apache License, Version 2.0.\n' + 67 | ' *\n' + 68 | ' * The source tree of this library can be found at\n' + 69 | ' * https://github.com/Clarifai/clarifai-javascript\n' + 70 | ' */\n' 71 | ); 72 | 73 | const lintOptions = { 74 | env: [ 75 | 'browser', 76 | 'node' 77 | ], 78 | rules: { 79 | 'spaced-comment': [2, 'always'], 80 | 'semi': [2, 'always'], 81 | 'curly': [2, 'all'], 82 | 'no-else-return': 2, 83 | 'no-unreachable': 2, 84 | 'no-return-assign': 2, 85 | 'indent': [2, 2], 86 | 'no-unused-vars': [2, {vars: 'all', args: 'none'}], 87 | 'key-spacing': [2, {afterColon: true}], 88 | 'quotes': [2, 'single'], 89 | 'camelcase': 2, 90 | 'new-cap': 2, 91 | 'no-const-assign': 2, 92 | 'eqeqeq': 2, 93 | 'no-multi-str': 2 94 | } 95 | }; 96 | 97 | 98 | function getBuildVars() { 99 | const stageString = process.env.CLARIFAI_DEPLOY || gutil.env.stage || 'dev'; 100 | if (!buildVars.hasOwnProperty(stageString)) { 101 | throw 'There are no defined build variables for stage `' + stageString + '`'; 102 | } 103 | 104 | return buildVars[stageString]; 105 | } 106 | 107 | function dontFailOnError() { 108 | return gulp.src(['./src/**/*.js']) 109 | .pipe(eslint(lintOptions)) 110 | .pipe(eslint.format()) 111 | .pipe(eslint.failOnError().on('error', notify.onError('Error: <%= error.message %>'))); 112 | } 113 | 114 | function failOnError() { 115 | return gulp.src(['./src/**/*.js']) 116 | .pipe(eslint(lintOptions)) 117 | .pipe(eslint.format()) 118 | .pipe(eslint.failOnError().on('error', function(e) { 119 | console.log('jslint error:', e); 120 | process.exit(1); 121 | })); 122 | } 123 | 124 | gulp.task('integrationtest', function() { 125 | return gulp.src('./tests/integration/*.js') 126 | .pipe(jasmine({ 127 | 'includeStackTrace': false, 128 | 'verbose': true, 129 | 'timeout': 60000, 130 | 'config': { 131 | 'helpers': [ 132 | './node_modules/babel-register/lib/node.js' 133 | ], 134 | 'random': false, 135 | } 136 | })); 137 | }); 138 | 139 | gulp.task('unittest', (done, error) => { 140 | return gulp.src('./tests/unit/*.js') 141 | .pipe(jasmine({ 142 | 'includeStackTrace': true, 143 | 'verbose': true, 144 | 'timeout': 60000, 145 | 'config': { 146 | 'helpers': [ 147 | './node_modules/babel-register/lib/node.js' 148 | ] 149 | } 150 | })); 151 | }); 152 | 153 | gulp.task('test', gulp.series('integrationtest', 'unittest')); 154 | 155 | gulp.task('jslint', function() { 156 | const buildVars = getBuildVars(); 157 | if (buildVars.lintFailOnError === true) { 158 | return failOnError(); 159 | } 160 | return dontFailOnError(); 161 | }); 162 | 163 | 164 | // delete the contents of build folder 165 | gulp.task('cleanbuild', function() { 166 | return del([ 167 | './dist/**', 168 | './sdk/**', 169 | './docs/**', 170 | ], {'force': true}); 171 | }); 172 | 173 | // browserify src/js to dist/browser/js 174 | gulp.task('browserify', gulp.series('cleanbuild'), function() { 175 | const buildVars = getBuildVars(); 176 | const replacePatterns = [ 177 | { 178 | 'match': 'buildTimestamp', 179 | 'replacement': new Date().getTime() 180 | }, 181 | { 182 | 'match': 'stage', 183 | 'replacement': buildVars.stage 184 | }, 185 | { 186 | 'match': 'VERSION', 187 | 'replacement': VERSION 188 | } 189 | ]; 190 | 191 | return gulp.src('./src/index.js') 192 | .pipe(browserify({ 193 | 'insertGlobals': true, 194 | 'debug': false, 195 | 'transform': ['babelify'] 196 | }).on('error', notify.onError(function(error) { 197 | const message = 'Browserify error: ' + error.message; 198 | if (buildVars.browserifyFailOnError === true) { 199 | console.log(error); 200 | process.exit(1); 201 | } 202 | return message; 203 | }))) 204 | .pipe(replace({ 205 | patterns: replacePatterns 206 | })) 207 | .pipe(rename(function(path) { 208 | path.basename = 'clarifai-' + VERSION; 209 | })) 210 | .pipe(insert.prepend(BROWSER_HEADER)) 211 | .pipe(gulp.dest('./sdk')) 212 | .pipe(rename(function(path) { 213 | path.basename = 'clarifai-latest'; 214 | })) 215 | .pipe(gulp.dest('./sdk')) 216 | .pipe(uglify()) 217 | .pipe(rename(function(path) { 218 | path.basename = 'clarifai-' + VERSION + '.min'; 219 | })) 220 | .pipe(gulp.dest('./sdk')) 221 | .pipe(rename(function(path) { 222 | path.basename = 'clarifai-latest.min'; 223 | })) 224 | .pipe(gulp.dest('./sdk')); 225 | }); 226 | 227 | gulp.task('dist', gulp.series('browserify'), function() { 228 | return gulp.src('./src/**/*.js') 229 | .pipe(babel({ 230 | presets: ['@babel/preset-env'] 231 | })) 232 | .pipe(gulp.dest('./dist')); 233 | }); 234 | 235 | const tasks = [ 236 | 'jslint', 237 | 'browserify', 238 | 'dist' 239 | ]; 240 | 241 | gulp.task('build', gulp.series(tasks)); 242 | 243 | // will do an initial build, then build on any changes to src 244 | gulp.task('watch', gulp.series(tasks), function () { 245 | gulp.watch('./src/**', ['jslint', 'browserify']); 246 | }); 247 | -------------------------------------------------------------------------------- /tests/unit/input-unit-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {BASE_URL, SAMPLE_API_KEY} = require('./helpers'); 3 | const axios = require('axios'); 4 | const MockAdapter = require('axios-mock-adapter'); 5 | const {errorHandler} = require('../integration/helpers'); 6 | 7 | let app; 8 | 9 | let mock; 10 | 11 | describe('Unit Tests - Inputs', () => { 12 | beforeAll(() => { 13 | app = new Clarifai.App({ 14 | apiKey: SAMPLE_API_KEY, 15 | apiEndpoint: BASE_URL 16 | }); 17 | }); 18 | 19 | beforeEach(() => { 20 | mock = new MockAdapter(axios); 21 | }); 22 | 23 | it('Creates inputs', done => { 24 | mock.onPost(BASE_URL + '/v2/inputs').reply(200, JSON.parse(` 25 | { 26 | "status": { 27 | "code": 10000, 28 | "description": "Ok" 29 | }, 30 | "inputs": [ 31 | { 32 | "id": "@inputID1", 33 | "data": { 34 | "image": { 35 | "url": "https://some.image.url1" 36 | }, 37 | "geo": { 38 | "geo_point": { 39 | "longitude": 55, 40 | "latitude": 66 41 | } 42 | } 43 | }, 44 | "created_at": "2019-01-17T12:43:04.895006174Z", 45 | "modified_at": "2019-01-17T12:43:04.895006174Z", 46 | "status": { 47 | "code": 30001, 48 | "description": "Download pending" 49 | } 50 | }, 51 | { 52 | "id": "@inputID2", 53 | "data": { 54 | "image": { 55 | "url": "https://some.image.url2" 56 | } 57 | }, 58 | "created_at": "2019-01-17T12:43:04.895006174Z", 59 | "modified_at": "2019-01-17T12:43:04.895006174Z", 60 | "status": { 61 | "code": 30001, 62 | "description": "Download pending" 63 | } 64 | } 65 | ] 66 | } 67 | `)); 68 | 69 | app.inputs.create([ 70 | { 71 | url: 'https://some.image.url1', 72 | id: '@inputID1', 73 | allowDuplicateUrl: true, 74 | geo: { 75 | longitude: 55, 76 | latitude: 66, 77 | } 78 | }, 79 | { 80 | url: 'https://some.image.url2', 81 | id: '@inputID2', 82 | allowDuplicateUrl: true, 83 | } 84 | ]) 85 | .then(inputs => { 86 | expect(mock.history.post.length).toBe(1); 87 | 88 | expect(JSON.parse(mock.history.post[0].data)).toEqual(JSON.parse(` 89 | { 90 | "inputs": [ 91 | { 92 | "id": "@inputID1", 93 | "data": { 94 | "image": { 95 | "url": "https://some.image.url1", 96 | "allow_duplicate_url": true 97 | }, 98 | "geo": { 99 | "geo_point": { 100 | "longitude": 55, 101 | "latitude": 66 102 | } 103 | } 104 | } 105 | }, 106 | { 107 | "id": "@inputID2", 108 | "data": { 109 | "image": { 110 | "url": "https://some.image.url2", 111 | "allow_duplicate_url": true 112 | } 113 | } 114 | } 115 | ] 116 | } 117 | `)); 118 | 119 | expect(inputs[0].id).toEqual('@inputID1'); 120 | expect(inputs[0].imageUrl).toEqual('https://some.image.url1'); 121 | expect(inputs[0].geo).toEqual({geoPoint: {longitude: 55, latitude: 66}}); 122 | 123 | expect(inputs[1].id).toEqual('@inputID2'); 124 | expect(inputs[1].imageUrl).toEqual('https://some.image.url2'); 125 | expect(inputs[1].geo).toBeUndefined(); 126 | 127 | done(); 128 | }) 129 | .catch(errorHandler.bind(done)); 130 | }); 131 | 132 | it('Updates inputs', done => { 133 | mock.onPatch(BASE_URL + '/v2/inputs').reply(200, JSON.parse(` 134 | { 135 | "status": { 136 | "code": 10000, 137 | "description": "Ok" 138 | }, 139 | "inputs": [{ 140 | "id": "@inputID", 141 | "data": { 142 | "image": { 143 | "url": "@imageURL" 144 | }, 145 | "concepts": [ 146 | { 147 | "id": "@positiveConcept1", 148 | "name": "@positiveConceptName1", 149 | "value": 1 150 | }, 151 | { 152 | "id": "@positiveConcept2", 153 | "value": 1 154 | }, 155 | { 156 | "id": "@negativeConcept1", 157 | "name": "@negativeConceptName1", 158 | "value": 0 159 | }, 160 | { 161 | "id": "@negativeConcept2", 162 | "value": 0 163 | } 164 | ] 165 | }, 166 | "created_at": "2017-10-13T20:53:00.253139Z", 167 | "modified_at": "2017-10-13T20:53:00.868659782Z", 168 | "status": { 169 | "code": 30200, 170 | "description": "Input image modification success" 171 | } 172 | }] 173 | } 174 | `)); 175 | 176 | app.inputs.update( 177 | { 178 | id: '@inputID', 179 | concepts: [ 180 | { 181 | id: "@positiveConcept1", 182 | name: "@positiveConceptName1" 183 | }, 184 | { 185 | id: "@positiveConcept2" 186 | }, 187 | { 188 | id: "@negativeConcept1", 189 | name: "@negativeConceptName1", 190 | value: 0 191 | }, 192 | { 193 | id: "@negativeConcept2", 194 | value: 0 195 | } 196 | ], 197 | action: 'merge' 198 | } 199 | ) 200 | .then(inputs => { 201 | expect(mock.history.patch.length).toBe(1); 202 | 203 | expect(JSON.parse(mock.history.patch[0].data)).toEqual(JSON.parse(` 204 | { 205 | "inputs": [ 206 | { 207 | "id": "@inputID", 208 | "data": { 209 | "concepts": [ 210 | { 211 | "id": "@positiveConcept1", 212 | "name": "@positiveConceptName1" 213 | }, 214 | { 215 | "id": "@positiveConcept2" 216 | }, 217 | { 218 | "id": "@negativeConcept1", 219 | "name": "@negativeConceptName1", 220 | "value": 0 221 | }, 222 | { 223 | "id": "@negativeConcept2", 224 | "value": 0 225 | } 226 | ] 227 | } 228 | } 229 | ], 230 | "action":"merge" 231 | } 232 | `)); 233 | 234 | expect(inputs[0].id).toEqual('@inputID'); 235 | expect(inputs[0].imageUrl).toEqual('@imageURL'); 236 | 237 | done(); 238 | }) 239 | .catch(errorHandler.bind(done)); 240 | }); 241 | 242 | it('Updates input with metadata', done => { 243 | mock.onPatch(BASE_URL + '/v2/inputs').reply(200, JSON.parse(` 244 | { 245 | "status": { 246 | "code": 10000, 247 | "description": "Ok" 248 | }, 249 | "inputs": [{ 250 | "id": "@inputID", 251 | "data": { 252 | "image": { 253 | "url": "@imageURL" 254 | }, 255 | "concepts": [{ 256 | "id": "concept1", 257 | "name": "concept1", 258 | "value": 1, 259 | "app_id": "@appID" 260 | }], 261 | "metadata": { 262 | "@key1": "@value1", 263 | "@key2": "@value2" 264 | } 265 | }, 266 | "created_at": "2017-11-02T15:08:22.005157Z", 267 | "modified_at": "2017-11-02T15:08:23.071624222Z", 268 | "status": { 269 | "code": 30200, 270 | "description": "Input image modification success" 271 | } 272 | }] 273 | } 274 | `)); 275 | 276 | app.inputs.update( 277 | { 278 | id: '@inputID', 279 | metadata: { 280 | '@key1': '@value1', 281 | '@key2': '@value2', 282 | }, 283 | action: 'overwrite' 284 | } 285 | ) 286 | .then(inputs => { 287 | expect(mock.history.patch.length).toBe(1); 288 | 289 | expect(JSON.parse(mock.history.patch[0].data)).toEqual(JSON.parse(` 290 | { 291 | "inputs": [ 292 | { 293 | "id": "@inputID", 294 | "data": { 295 | "metadata": { 296 | "@key1": "@value1", 297 | "@key2": "@value2" 298 | } 299 | } 300 | } 301 | ], 302 | "action":"overwrite" 303 | } 304 | `)); 305 | 306 | expect(inputs[0].id).toEqual('@inputID'); 307 | expect(inputs[0].imageUrl).toEqual('@imageURL'); 308 | 309 | done(); 310 | }) 311 | .catch(errorHandler.bind(done)); 312 | }); 313 | 314 | it('Lists inputs', done => { 315 | mock.onGet(BASE_URL + '/v2/inputs').reply(200, JSON.parse(` 316 | { 317 | "status": { 318 | "code": 10000, 319 | "description": "Ok" 320 | }, 321 | "inputs": [ 322 | { 323 | "id": "@inputID1", 324 | "data": { 325 | "image": { 326 | "url": "https://some.image.url1" 327 | }, 328 | "geo": { 329 | "geo_point": { 330 | "longitude": 55, 331 | "latitude": 66 332 | } 333 | } 334 | }, 335 | "created_at": "2019-01-17T14:02:21.216473Z", 336 | "modified_at": "2019-01-17T14:02:21.800792Z", 337 | "status": { 338 | "code": 30000, 339 | "description": "Download complete" 340 | } 341 | }, 342 | { 343 | "id": "@inputID2", 344 | "data": { 345 | "image": { 346 | "url": "https://some.image.url2" 347 | } 348 | }, 349 | "created_at": "2019-01-17T14:02:21.216473Z", 350 | "modified_at": "2019-01-17T14:02:21.800792Z", 351 | "status": { 352 | "code": 30000, 353 | "description": "Download complete" 354 | } 355 | } 356 | ] 357 | } 358 | `)); 359 | 360 | app.inputs.list().then(inputs => { 361 | expect(mock.history.get.length).toBe(1); 362 | 363 | expect(inputs[0].id).toEqual('@inputID1'); 364 | expect(inputs[0].imageUrl).toEqual('https://some.image.url1'); 365 | expect(inputs[0].geo.geoPoint).toEqual({longitude: 55, latitude: 66}); 366 | 367 | expect(inputs[1].id).toEqual('@inputID2'); 368 | expect(inputs[1].imageUrl).toEqual('https://some.image.url2'); 369 | expect(inputs[1].geo).toBeUndefined(); 370 | 371 | done(); 372 | }) 373 | .catch(errorHandler.bind(done)); 374 | }); 375 | 376 | it('Gets input', done => { 377 | mock.onGet(BASE_URL + '/v2/inputs/%40inputID').reply(200, JSON.parse(` 378 | { 379 | "status": { 380 | "code": 10000, 381 | "description": "Ok" 382 | }, 383 | "input": { 384 | "id": "@inputID", 385 | "data": { 386 | "image": { 387 | "url": "https://some.image.url" 388 | }, 389 | "geo": { 390 | "geo_point": { 391 | "longitude": 55, 392 | "latitude": 66 393 | } 394 | } 395 | }, 396 | "created_at": "2019-01-17T14:02:21.216473Z", 397 | "modified_at": "2019-01-17T14:02:21.800792Z", 398 | "status": { 399 | "code": 30000, 400 | "description": "Download complete" 401 | } 402 | } 403 | } 404 | `)); 405 | 406 | app.inputs.get("@inputID").then(input => { 407 | expect(mock.history.get.length).toBe(1); 408 | 409 | expect(input.id).toEqual('@inputID'); 410 | expect(input.imageUrl).toEqual('https://some.image.url'); 411 | expect(input.geo.geoPoint).toEqual({longitude: 55, latitude: 66}); 412 | 413 | done(); 414 | }) 415 | .catch(errorHandler.bind(done)); 416 | }); 417 | 418 | it("Gets inputs' status", done => { 419 | mock.onGet(BASE_URL + '/v2/inputs/status').reply(200, JSON.parse(` 420 | { 421 | "status": { 422 | "code": 10000, 423 | "description": "Ok" 424 | }, 425 | "counts": { 426 | "processed": 1, 427 | "to_process": 2, 428 | "errors": 3, 429 | "processing": 4, 430 | "reindexed": 5, 431 | "to_reindex": 6, 432 | "reindex_errors": 7, 433 | "reindexing": 8 434 | } 435 | } 436 | `)); 437 | 438 | app.inputs.getStatus().then(inputsStatus => { 439 | expect(mock.history.get.length).toBe(1); 440 | 441 | expect(inputsStatus.counts.processed).toEqual(1); 442 | expect(inputsStatus.counts.to_process).toEqual(2); 443 | expect(inputsStatus.counts.errors).toEqual(3); 444 | expect(inputsStatus.counts.processing).toEqual(4); 445 | expect(inputsStatus.counts.reindexed).toEqual(5); 446 | expect(inputsStatus.counts.to_reindex).toEqual(6); 447 | expect(inputsStatus.counts.reindex_errors).toEqual(7); 448 | expect(inputsStatus.counts.reindexing).toEqual(8); 449 | 450 | done(); 451 | }) 452 | .catch(errorHandler.bind(done)); 453 | }); 454 | 455 | it('Deletes all inputs', done => { 456 | mock.onDelete(BASE_URL + '/v2/inputs').reply(200, JSON.parse(` 457 | { 458 | "status": { 459 | "code": 10000, 460 | "description": "Ok" 461 | } 462 | } 463 | `)); 464 | 465 | app.inputs.delete() 466 | .then(response => { 467 | expect(mock.history.delete.length).toBe(1); 468 | 469 | expect(JSON.parse(mock.history.delete[0].data)).toEqual(JSON.parse(` 470 | { 471 | "delete_all": true 472 | } 473 | `)); 474 | 475 | expect(response.data.status.code).toEqual(10000); 476 | 477 | done(); 478 | }) 479 | .catch(errorHandler.bind(done)); 480 | }); 481 | 482 | it('Deletes inputs', done => { 483 | mock.onDelete(BASE_URL + '/v2/inputs').reply(200, JSON.parse(` 484 | { 485 | "status": { 486 | "code": 10000, 487 | "description": "Ok" 488 | } 489 | } 490 | `)); 491 | 492 | app.inputs.delete(['@inputID1', '@inputID2']) 493 | .then(response => { 494 | expect(mock.history.delete.length).toBe(1); 495 | 496 | expect(JSON.parse(mock.history.delete[0].data)).toEqual(JSON.parse(` 497 | { 498 | "ids": [ 499 | "@inputID1", 500 | "@inputID2" 501 | ] 502 | } 503 | `)); 504 | 505 | expect(response.data.status.code).toEqual(10000); 506 | 507 | done(); 508 | }) 509 | .catch(errorHandler.bind(done)); 510 | }); 511 | }); 512 | -------------------------------------------------------------------------------- /tests/unit/models-unit-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {BASE_URL, SAMPLE_API_KEY} = require('./helpers'); 3 | const axios = require('axios'); 4 | const MockAdapter = require('axios-mock-adapter'); 5 | const {errorHandler} = require('../integration/helpers'); 6 | 7 | let app; 8 | 9 | let mock; 10 | 11 | describe('Unit Tests - Models', () => { 12 | beforeAll(() => { 13 | app = new Clarifai.App({ 14 | apiKey: SAMPLE_API_KEY, 15 | apiEndpoint: BASE_URL 16 | }); 17 | }); 18 | 19 | beforeEach(() => { 20 | mock = new MockAdapter(axios); 21 | }); 22 | 23 | it('Creates model', done => { 24 | mock.onPost(BASE_URL + '/v2/models').reply(200, JSON.parse(` 25 | { 26 | "status": { 27 | "code": 10000, 28 | "description": "Ok" 29 | }, 30 | "model": { 31 | "id": "@modelID", 32 | "name": "@modelName", 33 | "created_at": "2019-01-22T11:54:12.375436048Z", 34 | "app_id": "@appID", 35 | "output_info": { 36 | "output_config": { 37 | "concepts_mutually_exclusive": false, 38 | "closed_environment": false, 39 | "max_concepts": 0, 40 | "min_value": 0 41 | }, 42 | "message": "Show output_info with: GET /models/{model_id}/output_info", 43 | "type": "concept", 44 | "type_ext": "concept" 45 | }, 46 | "model_version": { 47 | "id": "@modelVersionID", 48 | "created_at": "2019-01-22T11:54:12.406406642Z", 49 | "status": { 50 | "code": 21102, 51 | "description": "Model not yet trained" 52 | }, 53 | "active_concept_count": 2, 54 | "train_stats": {} 55 | } 56 | } 57 | } 58 | `)); 59 | 60 | 61 | app.models.create({id: '@modelID', name: '@modelName'}, [ 62 | { 63 | id: 'dog' 64 | }, 65 | { 66 | id: 'cat' 67 | } 68 | ]) 69 | .then(model => { 70 | expect(mock.history.post.length).toBe(1); 71 | expect(JSON.parse(mock.history.post[0].data)).toEqual(JSON.parse(` 72 | { 73 | "model": { 74 | "id": "@modelID", 75 | "name": "@modelName", 76 | "output_info": { 77 | "data": { 78 | "concepts": [ 79 | { 80 | "id": "dog" 81 | }, 82 | { 83 | "id": "cat" 84 | } 85 | ] 86 | }, 87 | "output_config": { 88 | "concepts_mutually_exclusive": false, 89 | "closed_environment": false 90 | } 91 | } 92 | } 93 | } 94 | `)); 95 | 96 | expect(model.id).toBe('@modelID'); 97 | expect(model.name).toBe('@modelName'); 98 | expect(model.modelVersion.id).toBe('@modelVersionID'); 99 | done(); 100 | }) 101 | .catch(errorHandler.bind(done)); 102 | }); 103 | 104 | it('Gets model', done => { 105 | mock.onGet(BASE_URL + '/v2/models/%40modelID').reply(200, JSON.parse(` 106 | { 107 | "status": { 108 | "code": 10000, 109 | "description": "Ok" 110 | }, 111 | "model": { 112 | "id": "@modelID", 113 | "name": "@modelName", 114 | "created_at": "2017-05-16T19:20:38.733764Z", 115 | "app_id": "main", 116 | "output_info": { 117 | "data": { 118 | "concepts": [{ 119 | "id": "@conceptID11", 120 | "name": "safe", 121 | "created_at": "2017-05-16T19:20:38.450157Z", 122 | "language": "en", 123 | "app_id": "main" 124 | }] 125 | }, 126 | "type": "concept", 127 | "type_ext": "concept" 128 | }, 129 | "model_version": { 130 | "id": "@modelVersionID", 131 | "created_at": "2017-05-16T19:20:38.733764Z", 132 | "status": { 133 | "code": 21100, 134 | "description": "Model trained successfully" 135 | }, 136 | "active_concept_count": 5 137 | }, 138 | "display_name": "Moderation" 139 | } 140 | } 141 | `)); 142 | 143 | app.models.get('@modelID').then(model => { 144 | expect(mock.history.get.length).toBe(1); 145 | 146 | expect(model.id).toBe('@modelID'); 147 | expect(model.name).toBe('@modelName'); 148 | expect(model.modelVersion.id).toBe('@modelVersionID'); 149 | expect(model.outputInfo.data.concepts[0].id).toBe('@conceptID11'); 150 | done(); 151 | }) 152 | .catch(errorHandler.bind(done)); 153 | }); 154 | 155 | it('Gets models', done => { 156 | mock.onGet(BASE_URL + '/v2/models').reply(200, JSON.parse(` 157 | { 158 | "status": { 159 | "code": 10000, 160 | "description": "Ok" 161 | }, 162 | "models": [ 163 | { 164 | "id": "@modelID1", 165 | "name": "@modelName1", 166 | "created_at": "2019-01-16T23:33:46.605294Z", 167 | "app_id": "main", 168 | "output_info": { 169 | "message": "Show output_info with: GET /models/{model_id}/output_info", 170 | "type": "detect", 171 | "type_ext": "detect" 172 | }, 173 | "model_version": { 174 | "id": "28b2ff6148684aa2b18a34cd004b4fac", 175 | "created_at": "2019-01-16T23:33:46.605294Z", 176 | "status": { 177 | "code": 21100, 178 | "description": "Model trained successfully" 179 | }, 180 | "train_stats": {} 181 | }, 182 | "display_name": "Face Detection" 183 | }, 184 | { 185 | "id": "@modelID2", 186 | "name": "@modelName2", 187 | "created_at": "2019-01-16T23:33:46.605294Z", 188 | "app_id": "main", 189 | "output_info": { 190 | "message": "Show output_info with: GET /models/{model_id}/output_info", 191 | "type": "embed", 192 | "type_ext": "detect-embed" 193 | }, 194 | "model_version": { 195 | "id": "fc6999e5eb274dfdba826f6b1c7ffdab", 196 | "created_at": "2019-01-16T23:33:46.605294Z", 197 | "status": { 198 | "code": 21100, 199 | "description": "Model trained successfully" 200 | }, 201 | "train_stats": {} 202 | }, 203 | "display_name": "Face Embedding" 204 | } 205 | ] 206 | } 207 | `)); 208 | 209 | app.models.list().then(models => { 210 | expect(mock.history.get.length).toBe(1); 211 | 212 | const model1 = models[0]; 213 | expect(model1.id).toBe('@modelID1'); 214 | expect(model1.name).toBe('@modelName1'); 215 | expect(model1.outputInfo.type_ext).toBe('detect'); 216 | 217 | const model2 = models[1]; 218 | expect(model2.id).toBe('@modelID2'); 219 | expect(model2.name).toBe('@modelName2'); 220 | expect(model2.outputInfo.type_ext).toBe('detect-embed'); 221 | 222 | done(); 223 | }) 224 | .catch(errorHandler.bind(done)); 225 | }); 226 | 227 | it('Get model outputinfo', done => { 228 | mock.onGet(BASE_URL + '/v2/models/%40modelID/output_info').reply(200, JSON.parse(` 229 | { 230 | "status": { 231 | "code": 10000, 232 | "description": "Ok" 233 | }, 234 | "model": { 235 | "id": "@modelID", 236 | "name": "@modelName", 237 | "created_at": "2017-05-16T19:20:38.733764Z", 238 | "app_id": "main", 239 | "output_info": { 240 | "data": { 241 | "concepts": [{ 242 | "id": "@conceptID11", 243 | "name": "safe", 244 | "created_at": "2017-05-16T19:20:38.450157Z", 245 | "language": "en", 246 | "app_id": "main" 247 | }] 248 | }, 249 | "type": "concept", 250 | "type_ext": "concept" 251 | }, 252 | "model_version": { 253 | "id": "@modelVersionID", 254 | "created_at": "2017-05-16T19:20:38.733764Z", 255 | "status": { 256 | "code": 21100, 257 | "description": "Model trained successfully" 258 | }, 259 | "active_concept_count": 5 260 | }, 261 | "display_name": "Moderation" 262 | } 263 | } 264 | `)); 265 | 266 | app.models.getOutputInfo('@modelID') 267 | .then(outputinfo => { 268 | expect(mock.history.get.length).toBe(1); 269 | 270 | expect(outputinfo.id).toBe("@modelID"); 271 | expect(outputinfo.name).toBe("@modelName"); 272 | expect(outputinfo.modelVersion.id).toBe("@modelVersionID"); 273 | expect(outputinfo.outputInfo.type).toBe("concept"); 274 | done(); 275 | }) 276 | .catch(errorHandler.bind(done)); 277 | }); 278 | 279 | it('Gets model', done => { 280 | mock.onGet(BASE_URL + '/v2/models/%40modelID').reply(200, JSON.parse(` 281 | { 282 | "status": { 283 | "code": 10000, 284 | "description": "Ok" 285 | }, 286 | "model": { 287 | "id": "@modelID", 288 | "name": "@modelName", 289 | "created_at": "2017-05-16T19:20:38.733764Z", 290 | "app_id": "main", 291 | "output_info": { 292 | "data": { 293 | "concepts": [{ 294 | "id": "@conceptID11", 295 | "name": "safe", 296 | "created_at": "2017-05-16T19:20:38.450157Z", 297 | "language": "en", 298 | "app_id": "main" 299 | }] 300 | }, 301 | "type": "concept", 302 | "type_ext": "concept" 303 | }, 304 | "model_version": { 305 | "id": "@modelVersionID", 306 | "created_at": "2017-05-16T19:20:38.733764Z", 307 | "status": { 308 | "code": 21100, 309 | "description": "Model trained successfully" 310 | }, 311 | "active_concept_count": 5 312 | }, 313 | "display_name": "Moderation" 314 | } 315 | } 316 | `)); 317 | 318 | app.models.get('@modelID').then(model => { 319 | expect(mock.history.get.length).toBe(1); 320 | 321 | expect(model.id).toBe('@modelID'); 322 | expect(model.name).toBe('@modelName'); 323 | expect(model.modelVersion.id).toBe('@modelVersionID'); 324 | expect(model.outputInfo.data.concepts[0].id).toBe('@conceptID11'); 325 | done(); 326 | }) 327 | .catch(errorHandler.bind(done)); 328 | }); 329 | 330 | it('Modifies models', done => { 331 | mock.onPatch(BASE_URL + '/v2/models').reply(200, JSON.parse(` 332 | { 333 | "status": { 334 | "code": 10000, 335 | "description": "Ok" 336 | }, 337 | "models": [{ 338 | "id": "@modelID", 339 | "name": "@newModelName", 340 | "created_at": "2017-11-27T08:35:13.911899Z", 341 | "app_id": "@appID", 342 | "output_info": { 343 | "output_config": { 344 | "concepts_mutually_exclusive": true, 345 | "closed_environment": true 346 | }, 347 | "message": "Show output_info with: GET /models/{model_id}/output_info", 348 | "type": "concept", 349 | "type_ext": "concept" 350 | }, 351 | "model_version": { 352 | "id": "@modelVersionID", 353 | "created_at": "2017-11-27T08:35:14.298376733Z", 354 | "status": { 355 | "code": 21102, 356 | "description": "Model not yet trained" 357 | } 358 | } 359 | }] 360 | } 361 | `)); 362 | 363 | app.models.update({ 364 | id: '@modelID', 365 | name: '@newModelName', 366 | conceptsMutuallyExclusive: true, 367 | closedEnvironment: true, 368 | concepts: [{id: '@conceptID1'}], 369 | action: 'merge', 370 | }).then(models => { 371 | expect(mock.history.patch.length).toBe(1); 372 | expect(JSON.parse(mock.history.patch[0].data)).toEqual(JSON.parse(` 373 | { 374 | "models": [ 375 | { 376 | "id": "@modelID", 377 | "name": "@newModelName", 378 | "output_info": { 379 | "data": { 380 | "concepts": [ 381 | { 382 | "id": "@conceptID1" 383 | } 384 | ] 385 | }, 386 | "output_config": { 387 | "concepts_mutually_exclusive": true, 388 | "closed_environment": true 389 | } 390 | } 391 | } 392 | ], 393 | "action": "merge" 394 | } 395 | `)); 396 | 397 | let model = models[0]; 398 | expect(model.id).toEqual('@modelID'); 399 | expect(model.name).toEqual('@newModelName'); 400 | 401 | done(); 402 | }).catch(errorHandler.bind(done)); 403 | }); 404 | 405 | it('Trains model', done => { 406 | mock.onPost(BASE_URL + '/v2/models/%40modelID/versions').reply(200, JSON.parse(` 407 | { 408 | "status": { 409 | "code": 10000, 410 | "description": "Ok" 411 | }, 412 | "model": { 413 | "id": "@modelID", 414 | "name": "@modelName", 415 | "created_at": "2019-01-20T15:51:21.641006Z", 416 | "app_id": "@appID", 417 | "output_info": { 418 | "output_config": { 419 | "concepts_mutually_exclusive": false, 420 | "closed_environment": false, 421 | "max_concepts": 0, 422 | "min_value": 0 423 | }, 424 | "message": "Show output_info with: GET /models/{model_id}/output_info", 425 | "type": "concept", 426 | "type_ext": "concept" 427 | }, 428 | "model_version": { 429 | "id": "@modelVersionID", 430 | "created_at": "2019-01-20T15:51:25.093744401Z", 431 | "status": { 432 | "code": 21103, 433 | "description": "Custom model is currently in queue for training, waiting on inputs to process." 434 | }, 435 | "active_concept_count": 2, 436 | "train_stats": {} 437 | } 438 | } 439 | } 440 | `)); 441 | 442 | app.models.train('@modelID').then(model => { 443 | expect(mock.history.post.length).toBe(1); 444 | 445 | expect(model.id).toEqual('@modelID'); 446 | expect(model.name).toEqual('@modelName'); 447 | 448 | done(); 449 | }).catch(errorHandler.bind(done)); 450 | }); 451 | 452 | it('Deletes all models', done => { 453 | mock.onDelete(BASE_URL + '/v2/models').reply(200, JSON.parse(` 454 | { 455 | "status": { 456 | "code": 10000, 457 | "description": "Ok" 458 | } 459 | } 460 | `)); 461 | 462 | app.models.delete().then(response => { 463 | expect(mock.history.delete.length).toBe(1); 464 | expect(JSON.parse(mock.history.delete[0].data)).toEqual(JSON.parse(` 465 | { 466 | "delete_all": true 467 | } 468 | `)); 469 | 470 | expect(response.status.code).toEqual(10000); 471 | 472 | done(); 473 | }).catch(errorHandler.bind(done)); 474 | }); 475 | 476 | it('Delete model', done => { 477 | mock.onDelete(BASE_URL + '/v2/models/%40modelID').reply(200, JSON.parse(` 478 | { 479 | "status": { 480 | "code": 10000, 481 | "description": "Ok" 482 | } 483 | } 484 | `)); 485 | 486 | app.models.delete('@modelID').then(response => { 487 | expect(mock.history.delete.length).toBe(1); 488 | 489 | expect(response.status.code).toEqual(10000); 490 | 491 | done(); 492 | }).catch(errorHandler.bind(done)); 493 | }); 494 | }); 495 | -------------------------------------------------------------------------------- /src/Model.js: -------------------------------------------------------------------------------- 1 | let axios = require('axios'); 2 | let ModelVersion = require('./ModelVersion'); 3 | let {isSuccess, checkType, clone} = require('./helpers'); 4 | let { 5 | API, 6 | SYNC_TIMEOUT, 7 | replaceVars, 8 | STATUS, 9 | POLLTIME 10 | } = require('./constants'); 11 | let {MODEL_QUEUED_FOR_TRAINING, MODEL_TRAINING} = STATUS; 12 | let {wrapToken, formatMediaPredict, formatModel, formatObjectForSnakeCase} = require('./utils'); 13 | let { 14 | MODEL_VERSIONS_PATH, 15 | MODEL_VERSION_PATH, 16 | MODELS_PATH, 17 | PREDICT_PATH, 18 | VERSION_PREDICT_PATH, 19 | MODEL_INPUTS_PATH, 20 | MODEL_VERSION_OUTPUT_PATH, 21 | MODEL_OUTPUT_PATH, 22 | MODEL_VERSION_INPUTS_PATH, 23 | MODEL_VERSION_METRICS_PATH 24 | } = API; 25 | 26 | /** 27 | * class representing a model 28 | * @class 29 | */ 30 | class Model { 31 | constructor(_config, data) { 32 | this._config = _config; 33 | this.name = data.name; 34 | this.id = data.id; 35 | this.createdAt = data.created_at || data.createdAt; 36 | this.appId = data.app_id || data.appId; 37 | this.outputInfo = data.output_info || data.outputInfo; 38 | if (checkType(/(String)/, data.version)) { 39 | this.modelVersion = {}; 40 | this.versionId = data.version; 41 | } else { 42 | if (data.model_version || data.modelVersion || data.version) { 43 | this.modelVersion = new ModelVersion(this._config, data.model_version || data.modelVersion || data.version); 44 | } 45 | this.versionId = (this.modelVersion || {}).id; 46 | } 47 | this.rawData = data; 48 | } 49 | 50 | /** 51 | * Merge concepts to a model 52 | * @param {object[]} concepts List of concept objects with id 53 | * @return {Promise(Model, error)} A Promise that is fulfilled with a Model instance or rejected with an error 54 | */ 55 | mergeConcepts(concepts = []) { 56 | let conceptsArr = Array.isArray(concepts) ? concepts : [concepts]; 57 | return this.update({action: 'merge', concepts: conceptsArr}); 58 | } 59 | 60 | /** 61 | * Remove concepts from a model 62 | * @param {object[]} concepts List of concept objects with id 63 | * @return {Promise(Model, error)} A Promise that is fulfilled with a Model instance or rejected with an error 64 | */ 65 | deleteConcepts(concepts = []) { 66 | let conceptsArr = Array.isArray(concepts) ? concepts : [concepts]; 67 | return this.update({action: 'remove', concepts: conceptsArr}); 68 | } 69 | 70 | /** 71 | * Overwrite concepts in a model 72 | * @param {object[]} concepts List of concept objects with id 73 | * @return {Promise(Model, error)} A Promise that is fulfilled with a Model instance or rejected with an error 74 | */ 75 | overwriteConcepts(concepts = []) { 76 | let conceptsArr = Array.isArray(concepts) ? concepts : [concepts]; 77 | return this.update({action: 'overwrite', concepts: conceptsArr}); 78 | } 79 | 80 | /** 81 | * Start a model evaluation job 82 | * @return {Promise(ModelVersion, error)} A Promise that is fulfilled with a ModelVersion instance or rejected with an error 83 | */ 84 | runModelEval() { 85 | let url = `${this._config.basePath}${replaceVars(MODEL_VERSION_METRICS_PATH, [this.id, this.versionId])}`; 86 | return wrapToken(this._config, (headers) => { 87 | return new Promise((resolve, reject) => { 88 | axios.post(url, {}, {headers}).then((response) => { 89 | if (isSuccess(response)) { 90 | resolve(new ModelVersion(this._config, response.data.model_version)); 91 | } else { 92 | reject(response); 93 | } 94 | }, reject); 95 | }); 96 | }); 97 | } 98 | 99 | /** 100 | * Update a model's output config or concepts 101 | * @param {object} model An object with any of the following attrs: 102 | * @param {string} name The new name of the model to update with 103 | * @param {boolean} conceptsMutuallyExclusive Do you expect to see more than one of the concepts in this model in the SAME image? Set to false (default) if so. Otherwise, set to true. 104 | * @param {boolean} closedEnvironment Do you expect to run the trained model on images that do not contain ANY of the concepts in the model? Set to false (default) if so. Otherwise, set to true. 105 | * @param {object[]} concepts An array of concept objects or string 106 | * @param {object|string} concepts[].concept If string is given, this is interpreted as concept id. Otherwise, if object is given, client expects the following attributes 107 | * @param {string} concepts[].concept.id The id of the concept to attach to the model 108 | * @param {object[]} action The action to perform on the given concepts. Possible values are 'merge', 'remove', or 'overwrite'. Default: 'merge' 109 | * @return {Promise(Model, error)} A Promise that is fulfilled with a Model instance or rejected with an error 110 | */ 111 | update(obj) { 112 | let url = `${this._config.basePath}${MODELS_PATH}`; 113 | let modelData = [obj]; 114 | let data = {models: modelData.map(m => formatModel(Object.assign(m, {id: this.id})))}; 115 | if (Array.isArray(obj.concepts)) { 116 | data['action'] = obj.action || 'merge'; 117 | } 118 | 119 | return wrapToken(this._config, (headers) => { 120 | return new Promise((resolve, reject) => { 121 | axios.patch(url, data, {headers}).then((response) => { 122 | if (isSuccess(response)) { 123 | resolve(new Model(this._config, response.data.models[0])); 124 | } else { 125 | reject(response); 126 | } 127 | }, reject); 128 | }); 129 | }); 130 | } 131 | 132 | /** 133 | * Create a new model version 134 | * @param {boolean} sync If true, this returns after model has completely trained. If false, this immediately returns default api response. 135 | * @return {Promise(Model, error)} A Promise that is fulfilled with a Model instance or rejected with an error 136 | */ 137 | train(sync) { 138 | let url = `${this._config.basePath}${replaceVars(MODEL_VERSIONS_PATH, [this.id])}`; 139 | return wrapToken(this._config, (headers) => { 140 | return new Promise((resolve, reject) => { 141 | axios.post(url, null, {headers}).then((response) => { 142 | if (isSuccess(response)) { 143 | let model = new Model(this._config, response.data.model); 144 | if (sync) { 145 | let timeStart = Date.now(); 146 | model._pollTrain.bind(model)(timeStart, resolve, reject); 147 | } else { 148 | resolve(model); 149 | } 150 | } else { 151 | reject(response); 152 | } 153 | }, reject); 154 | }); 155 | }); 156 | } 157 | 158 | _pollTrain(timeStart, resolve, reject) { 159 | clearTimeout(this.pollTimeout); 160 | if ((Date.now() - timeStart) >= SYNC_TIMEOUT) { 161 | return reject({ 162 | status: 'Error', 163 | message: 'Sync call timed out' 164 | }); 165 | } 166 | this.getOutputInfo().then((model) => { 167 | let modelStatusCode = model.modelVersion.status.code.toString(); 168 | if (modelStatusCode === MODEL_QUEUED_FOR_TRAINING || modelStatusCode === MODEL_TRAINING) { 169 | model.pollTimeout = setTimeout(() => model._pollTrain(timeStart, resolve, reject), POLLTIME); 170 | } else { 171 | resolve(model); 172 | } 173 | }, reject) 174 | .catch(reject); 175 | } 176 | 177 | /** 178 | * Returns model ouputs according to inputs 179 | * @param {object[]|object|string} inputs An array of objects/object/string pointing to an image resource. A string can either be a url or base64 image bytes. Object keys explained below: 180 | * @param {object} inputs[].image Object with keys explained below: 181 | * @param {string} inputs[].image.(url|base64|file) Can be a publicly accessibly url, base64 string representing image bytes, or file path (required) 182 | * @param {object|string} config An object with keys explained below. If a string is passed instead, it will be treated as the language (backwards compatibility) 183 | * @param {string} config.language A string code representing the language to return results in (example: 'zh' for simplified Chinese, 'ru' for Russian, 'ja' for Japanese) 184 | * @param {boolean} config.video indicates if the input should be processed as a video 185 | * @param {object[]} config.selectConcepts An array of concepts to return. Each object in the array will have a form of {name: } or {id: CONCEPT_ID} 186 | * @param {float} config.minValue The minimum confidence threshold that a result must meet. From 0.0 to 1.0 187 | * @param {number} config.maxConcepts The maximum number of concepts to return 188 | * @param {boolean} isVideo Deprecated: indicates if the input should be processed as a video (default false). Deprecated in favor of using config object 189 | * @return {Promise(response, error)} A Promise that is fulfilled with the API response or rejected with an error 190 | */ 191 | predict(inputs, config = {}, isVideo = false) { 192 | if (checkType(/String/, config)) { 193 | console.warn('passing the language as a string is deprecated, consider using the configuration object instead'); 194 | config = { 195 | language: config 196 | }; 197 | } 198 | 199 | if (isVideo) { 200 | console.warn('"isVideo" argument is deprecated, consider using the configuration object instead'); 201 | config.video = isVideo; 202 | } 203 | const video = config.video || false; 204 | delete config.video; 205 | if (checkType(/(Object|String)/, inputs)) { 206 | inputs = [inputs]; 207 | } 208 | let url = `${this._config.basePath}${this.versionId ? 209 | replaceVars(VERSION_PREDICT_PATH, [this.id, this.versionId]) : 210 | replaceVars(PREDICT_PATH, [this.id])}`; 211 | return wrapToken(this._config, (headers) => { 212 | let params = {inputs: inputs.map(input => formatMediaPredict(input, video ? 'video' : 'image'))}; 213 | if (config && Object.getOwnPropertyNames(config).length > 0) { 214 | params['model'] = { 215 | output_info: { 216 | output_config: formatObjectForSnakeCase(config) 217 | } 218 | }; 219 | } 220 | return new Promise((resolve, reject) => { 221 | axios.post(url, params, {headers}).then((response) => { 222 | let data = clone(response.data); 223 | data.rawData = clone(response.data); 224 | resolve(data); 225 | }, reject); 226 | }); 227 | }); 228 | } 229 | 230 | /** 231 | * Returns a version of the model specified by its id 232 | * @param {string} versionId The model's id 233 | * @return {Promise(response, error)} A Promise that is fulfilled with the API response or rejected with an error 234 | */ 235 | getVersion(versionId) { 236 | // TODO(Rok) MEDIUM: The version ID isn't URI encoded, as opposed to the model ID. This should probably be 237 | // consistent - i.e. the same in both cases. 238 | let url = `${this._config.basePath}${replaceVars(MODEL_VERSION_PATH, [this.id, versionId])}`; 239 | return wrapToken(this._config, (headers) => { 240 | return new Promise((resolve, reject) => { 241 | axios.get(url, {headers}).then((response) => { 242 | let data = clone(response.data); 243 | data.rawData = clone(response.data); 244 | resolve(data); 245 | }, reject); 246 | }); 247 | }); 248 | } 249 | 250 | /** 251 | * Returns a list of versions of the model 252 | * @param {object} options Object with keys explained below: (optional) 253 | * @param {number} options.page The page number (optional, default: 1) 254 | * @param {number} options.perPage Number of images to return per page (optional, default: 20) 255 | * @return {Promise(response, error)} A Promise that is fulfilled with the API response or rejected with an error 256 | */ 257 | getVersions(options = {page: 1, perPage: 20}) { 258 | let url = `${this._config.basePath}${replaceVars(MODEL_VERSIONS_PATH, [this.id])}`; 259 | return wrapToken(this._config, (headers) => { 260 | let data = { 261 | headers, 262 | params: {'per_page': options.perPage, 'page': options.page}, 263 | }; 264 | return new Promise((resolve, reject) => { 265 | axios.get(url, data).then((response) => { 266 | let data = clone(response.data); 267 | data.rawData = clone(response.data); 268 | resolve(data); 269 | }, reject); 270 | }); 271 | }); 272 | } 273 | 274 | /** 275 | * Returns all the model's output info 276 | * @return {Promise(Model, error)} A Promise that is fulfilled with a Model instance or rejected with an error 277 | */ 278 | getOutputInfo() { 279 | let url = `${this._config.basePath}${this.versionId ? 280 | replaceVars(MODEL_VERSION_OUTPUT_PATH, [this.id, this.versionId]) : 281 | replaceVars(MODEL_OUTPUT_PATH, [this.id])}`; 282 | return wrapToken(this._config, (headers) => { 283 | return new Promise((resolve, reject) => { 284 | axios.get(url, {headers}).then((response) => { 285 | resolve(new Model(this._config, response.data.model)); 286 | }, reject); 287 | }); 288 | }); 289 | } 290 | 291 | /** 292 | * Returns all the model's inputs 293 | * @param {object} options Object with keys explained below: (optional) 294 | * @param {number} options.page The page number (optional, default: 1) 295 | * @param {number} options.perPage Number of images to return per page (optional, default: 20) 296 | * @return {Promise(response, error)} A Promise that is fulfilled with the API response or rejected with an error 297 | */ 298 | getInputs(options = {page: 1, perPage: 20}) { 299 | let url = `${this._config.basePath}${this.versionId ? 300 | replaceVars(MODEL_VERSION_INPUTS_PATH, [this.id, this.versionId]) : 301 | replaceVars(MODEL_INPUTS_PATH, [this.id])}`; 302 | return wrapToken(this._config, (headers) => { 303 | return new Promise((resolve, reject) => { 304 | axios.get(url, { 305 | params: {'per_page': options.perPage, 'page': options.page}, 306 | headers 307 | }).then((response) => { 308 | let data = clone(response.data); 309 | data.rawData = clone(response.data); 310 | resolve(data); 311 | }, reject); 312 | }); 313 | }); 314 | } 315 | 316 | } 317 | 318 | module 319 | .exports = Model; 320 | -------------------------------------------------------------------------------- /src/Inputs.js: -------------------------------------------------------------------------------- 1 | let axios = require('axios'); 2 | let Input = require('./Input'); 3 | let {API, ERRORS, MAX_BATCH_SIZE, replaceVars} = require('./constants'); 4 | let {INPUT_PATH, INPUTS_PATH, INPUTS_STATUS_PATH, SEARCH_PATH} = API; 5 | let {wrapToken, formatInput, formatImagesSearch, formatConceptsSearch} = require('./utils'); 6 | let {isSuccess, checkType, clone} = require('./helpers'); 7 | 8 | /** 9 | * class representing a collection of inputs 10 | * @class 11 | */ 12 | class Inputs { 13 | constructor(_config, rawData = []) { 14 | this.rawData = rawData; 15 | rawData.forEach((inputData, index) => { 16 | if (inputData.input && inputData.score) { 17 | inputData.input.score = inputData.score; 18 | inputData = inputData.input; 19 | } 20 | this[index] = new Input(this._config, inputData); 21 | }); 22 | this.length = rawData.length; 23 | this._config = _config; 24 | } 25 | 26 | /** 27 | * Get all inputs in app 28 | * @param {Object} options Object with keys explained below: (optional) 29 | * @param {Number} options.page The page number (optional, default: 1) 30 | * @param {Number} options.perPage Number of images to return per page (optional, default: 20) 31 | * @return {Promise(Inputs, error)} A Promise that is fulfilled with an instance of Inputs or rejected with an error 32 | */ 33 | list(options = {page: 1, perPage: 20}) { 34 | let url = `${this._config.basePath}${INPUTS_PATH}`; 35 | return wrapToken(this._config, (headers) => { 36 | return new Promise((resolve, reject) => { 37 | axios.get(url, { 38 | headers, 39 | params: { 40 | page: options.page, 41 | per_page: options.perPage, 42 | } 43 | }).then((response) => { 44 | if (isSuccess(response)) { 45 | resolve(new Inputs(this._config, response.data.inputs)); 46 | } else { 47 | reject(response); 48 | } 49 | }, reject); 50 | }); 51 | }); 52 | } 53 | 54 | /** 55 | * Adds an input or multiple inputs 56 | * @param {object|object[]} inputs Can be a single media object or an array of media objects (max of 128 inputs/call; passing > 128 will throw an exception) 57 | * @param {object|string} inputs[].input If string, is given, this is assumed to be an image url 58 | * @param {string} inputs[].input.(url|base64) Can be a publicly accessibly url or base64 string representing image bytes (required) 59 | * @param {string} inputs[].input.id ID of input (optional) 60 | * @param {boolean} inputs[].input.allowDuplicateUrl Whether to allow duplicate URL 61 | * @param {object[]} inputs[].input.metadata Object with key and values pair (value can be string, array or other objects) to attach to the input (optional) 62 | * @param {object} inputs[].input.geo Object with latitude and longitude coordinates to associate with an input. Can be used in search query as the proximity of an input to a reference point (optional) 63 | * @param {number} inputs[].input.geo.latitude +/- latitude val of geodata 64 | * @param {number} inputs[].input.geo.longitude +/- longitude val of geodata 65 | * @param {object[]} inputs[].input.concepts An array of concepts to attach to media object (optional) 66 | * @param {object|string} inputs[].input.concepts[].concept If string, is given, this is assumed to be concept id with value equals true 67 | * @param {string} inputs[].input.concepts[].concept.id The concept id (required) 68 | * @param {boolean} inputs[].input.concepts[].concept.value Whether or not the input is a positive (true) or negative (false) example of the concept (default: true) 69 | * @return {Promise(Inputs, error)} A Promise that is fulfilled with an instance of Inputs or rejected with an error 70 | */ 71 | create(inputs) { 72 | if (checkType(/(String|Object)/, inputs)) { 73 | inputs = [inputs]; 74 | } 75 | let url = `${this._config.basePath}${INPUTS_PATH}`; 76 | if (inputs.length > MAX_BATCH_SIZE) { 77 | throw ERRORS.MAX_INPUTS; 78 | } 79 | return wrapToken(this._config, (headers) => { 80 | let data = { 81 | inputs: inputs.map(formatInput) 82 | }; 83 | return new Promise((resolve, reject) => { 84 | axios.post(url, data, {headers}) 85 | .then((response) => { 86 | if (isSuccess(response)) { 87 | resolve(new Inputs(this._config, response.data.inputs)); 88 | } else { 89 | reject(response); 90 | } 91 | }, reject); 92 | }); 93 | }); 94 | } 95 | 96 | /** 97 | * Get input by id 98 | * @param {String} id The input id 99 | * @return {Promise(Input, error)} A Promise that is fulfilled with an instance of Input or rejected with an error 100 | */ 101 | get(id) { 102 | let url = `${this._config.basePath}${replaceVars(INPUT_PATH, [id])}`; 103 | return wrapToken(this._config, (headers) => { 104 | return new Promise((resolve, reject) => { 105 | axios.get(url, {headers}).then((response) => { 106 | if (isSuccess(response)) { 107 | resolve(new Input(this._config, response.data.input)); 108 | } else { 109 | reject(response); 110 | } 111 | }, reject); 112 | }); 113 | }); 114 | } 115 | 116 | /** 117 | * Delete an input or a list of inputs by id or all inputs if no id is passed 118 | * @param {string|string[]} id The id of input to delete (optional) 119 | * @return {Promise(response, error)} A Promise that is fulfilled with the API response or rejected with an error 120 | */ 121 | delete(id = null) { 122 | let val; 123 | // delete an input 124 | if (checkType(/String/, id)) { 125 | let url = `${this._config.basePath}${replaceVars(INPUT_PATH, [id])}`; 126 | val = wrapToken(this._config, (headers) => { 127 | return axios.delete(url, {headers}); 128 | }); 129 | } else { 130 | val = this._deleteInputs(id); 131 | } 132 | return val; 133 | } 134 | 135 | _deleteInputs(id = null) { 136 | let url = `${this._config.basePath}${INPUTS_PATH}`; 137 | return wrapToken(this._config, (headers) => { 138 | let data = id === null ? {delete_all: true} : 139 | {ids: id}; 140 | return axios({ 141 | url, 142 | method: 'delete', 143 | headers, 144 | data 145 | }); 146 | }); 147 | } 148 | 149 | /** 150 | * Merge concepts to inputs in bulk 151 | * @param {object[]} inputs List of concepts to update (max of 128 inputs/call; passing > 128 will throw an exception) 152 | * @param {object} inputs[].input 153 | * @param {string} inputs[].input.id The id of the input to update 154 | * @param {string} inputs[].input.concepts Object with keys explained below: 155 | * @param {object} inputs[].input.concepts[].concept 156 | * @param {string} inputs[].input.concepts[].concept.id The concept id (required) 157 | * @param {boolean} inputs[].input.concepts[].concept.value Whether or not the input is a positive (true) or negative (false) example of the concept (default: true) 158 | * @return {Promise(Inputs, error)} A Promise that is fulfilled with an instance of Inputs or rejected with an error 159 | */ 160 | mergeConcepts(inputs) { 161 | inputs.action = 'merge'; 162 | return this.update(inputs); 163 | } 164 | 165 | /** 166 | * Delete concepts to inputs in bulk 167 | * @param {object[]} inputs List of concepts to update (max of 128 inputs/call; passing > 128 will throw an exception) 168 | * @param {object} inputs[].input 169 | * @param {string} inputs[].input.id The id of the input to update 170 | * @param {string} inputs[].input.concepts Object with keys explained below: 171 | * @param {object} inputs[].input.concepts[].concept 172 | * @param {string} inputs[].input.concepts[].concept.id The concept id (required) 173 | * @param {boolean} inputs[].input.concepts[].concept.value Whether or not the input is a positive (true) or negative (false) example of the concept (default: true) 174 | * @return {Promise(Inputs, error)} A Promise that is fulfilled with an instance of Inputs or rejected with an error 175 | */ 176 | deleteConcepts(inputs) { 177 | inputs.action = 'remove'; 178 | return this.update(inputs); 179 | } 180 | 181 | /** 182 | * Overwrite inputs in bulk 183 | * @param {object[]} inputs List of concepts to update (max of 128 inputs/call; passing > 128 will throw an exception) 184 | * @param {object} inputs[].input 185 | * @param {string} inputs[].input.id The id of the input to update 186 | * @param {string} inputs[].input.concepts Object with keys explained below: 187 | * @param {object} inputs[].input.concepts[].concept 188 | * @param {string} inputs[].input.concepts[].concept.id The concept id (required) 189 | * @param {boolean} inputs[].input.concepts[].concept.value Whether or not the input is a positive (true) or negative (false) example of the concept (default: true) 190 | * @return {Promise(Inputs, error)} A Promise that is fulfilled with an instance of Inputs or rejected with an error 191 | */ 192 | overwriteConcepts(inputs) { 193 | inputs.action = 'overwrite'; 194 | return this.update(inputs); 195 | } 196 | 197 | /** 198 | * @param {object[]} inputs List of inputs to update (max of 128 inputs/call; passing > 128 will throw an exception) 199 | * @param {object} inputs[].input 200 | * @param {string} inputs[].input.id The id of the input to update 201 | * @param {object} inputs[].input.metadata Object with key values to attach to the input (optional) 202 | * @param {object} inputs[].input.geo Object with latitude and longitude coordinates to associate with an input. Can be used in search query as the proximity of an input to a reference point (optional) 203 | * @param {number} inputs[].input.geo.latitude +/- latitude val of geodata 204 | * @param {number} inputs[].input.geo.longitude +/- longitude val of geodata 205 | * @param {string} inputs[].input.concepts Object with keys explained below (optional): 206 | * @param {object} inputs[].input.concepts[].concept 207 | * @param {string} inputs[].input.concepts[].concept.id The concept id (required) 208 | * @param {boolean} inputs[].input.concepts[].concept.value Whether or not the input is a positive (true) or negative (false) example of the concept (default: true) 209 | * @return {Promise(Inputs, error)} A Promise that is fulfilled with an instance of Inputs or rejected with an error 210 | */ 211 | update(inputs) { 212 | let url = `${this._config.basePath}${INPUTS_PATH}`; 213 | let inputsList = Array.isArray(inputs) ? inputs : [inputs]; 214 | if (inputsList.length > MAX_BATCH_SIZE) { 215 | throw ERRORS.MAX_INPUTS; 216 | } 217 | let data = { 218 | action: inputs.action, 219 | inputs: inputsList.map((input) => formatInput(input, false)) 220 | }; 221 | return wrapToken(this._config, (headers) => { 222 | return new Promise((resolve, reject) => { 223 | axios.patch(url, data, {headers}) 224 | .then((response) => { 225 | if (isSuccess(response)) { 226 | resolve(new Inputs(this._config, response.data.inputs)); 227 | } else { 228 | reject(response); 229 | } 230 | }, reject); 231 | }); 232 | }); 233 | } 234 | 235 | /** 236 | * Search for inputs or outputs based on concepts or images 237 | * @param {object[]} queries List of all predictions to match with 238 | * @param {object} queries[].concept An object with the following keys: 239 | * @param {string} queries[].concept.id The concept id 240 | * @param {string} queries[].concept.type Search over 'input' to get input matches to criteria or 'output' to get inputs that are visually similar to the criteria (default: 'output') 241 | * @param {string} queries[].concept.name The concept name 242 | * @param {boolean} queries[].concept.value Indicates whether or not the term should match with the prediction returned (default: true) 243 | * @param {object} queries[].input An image object that contains the following keys: 244 | * @param {string} queries[].input.id The input id 245 | * @param {string} queries[].input.type Search over 'input' to get input matches to criteria or 'output' to get inputs that are visually similar to the criteria (default: 'output') 246 | * @param {string} queries[].input.(base64|url) Can be a publicly accessibly url or base64 string representing image bytes (required) 247 | * @param {object} queries[].input.metadata An object with key and value specified by user to refine search with (optional) 248 | * @param {Object} options Object with keys explained below: (optional) 249 | * @param {Number} options.page The page number (optional, default: 1) 250 | * @param {Number} options.perPage Number of images to return per page (optional, default: 20) 251 | * @return {Promise(response, error)} A Promise that is fulfilled with the API response or rejected with an error 252 | */ 253 | search(queries = [], options = {page: 1, perPage: 20}) { 254 | let formattedAnds = []; 255 | let url = `${this._config.basePath}${SEARCH_PATH}`; 256 | let data = { 257 | query: { 258 | ands: [] 259 | }, 260 | pagination: { 261 | page: options.page, 262 | per_page: options.perPage 263 | } 264 | }; 265 | 266 | if (!Array.isArray(queries)) { 267 | queries = [queries]; 268 | } 269 | if (queries.length > 0) { 270 | queries.forEach(function(query) { 271 | if (query.input) { 272 | formattedAnds = formattedAnds.concat(formatImagesSearch(query.input)); 273 | } else if (query.concept) { 274 | formattedAnds = formattedAnds.concat(formatConceptsSearch(query.concept)); 275 | } 276 | }); 277 | data.query.ands = formattedAnds; 278 | } 279 | return wrapToken(this._config, (headers) => { 280 | return new Promise((resolve, reject) => { 281 | axios.post(url, data, {headers}) 282 | .then((response) => { 283 | if (isSuccess(response)) { 284 | let data = clone(response.data); 285 | data.rawData = clone(response.data); 286 | resolve(data); 287 | } else { 288 | reject(response); 289 | } 290 | }, reject); 291 | }); 292 | }); 293 | } 294 | 295 | /** 296 | * Get inputs status (number of uploaded, in process or failed inputs) 297 | * @return {Promise(response, error)} A Promise that is fulfilled with the API response or rejected with an error 298 | */ 299 | getStatus() { 300 | let url = `${this._config.basePath}${INPUTS_STATUS_PATH}`; 301 | return wrapToken(this._config, (headers) => { 302 | return new Promise((resolve, reject) => { 303 | axios.get(url, {headers}) 304 | .then((response) => { 305 | if (isSuccess(response)) { 306 | let data = clone(response.data); 307 | data.rawData = clone(response.data); 308 | resolve(data); 309 | } else { 310 | reject(response); 311 | } 312 | }, reject); 313 | }); 314 | }); 315 | } 316 | } 317 | ; 318 | 319 | module.exports = Inputs; 320 | -------------------------------------------------------------------------------- /tests/integration/inputs-search-int-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {sampleImages} = require('../assets/test-data'); 3 | const {errorHandler, pollStatus, waitForInputsUpload} = require('./helpers'); 4 | const Inputs = require('./../../src/Inputs'); 5 | const d = Date.now(); 6 | const ferrariId = 'ferrari' + d; 7 | const inputId1 = 'foobar' + d; 8 | const inputId2 = 'foobaz' + d; 9 | const inputId3 = 'input-with-geodata-1' + d; 10 | const inputId4 = 'input-with-geodata-2' + d; 11 | const inputId5 = 'input-with-geodata-3' + d; 12 | const langConceptId = '的な' + d; 13 | const beerId = 'beer' + d; 14 | const testModelId = 'vroom-vroom' + d; 15 | 16 | let app; 17 | let inputId; 18 | let lastCount; 19 | 20 | describe('Integration Tests - Inputs', () => { 21 | 22 | beforeAll(done => { 23 | app = new Clarifai.App({ 24 | apiKey: process.env.CLARIFAI_API_KEY, 25 | apiEndpoint: process.env.API_ENDPOINT 26 | }); 27 | 28 | app.inputs.delete() 29 | .then(response => { 30 | done(); 31 | }) 32 | .catch(errorHandler.bind(done)); 33 | }); 34 | 35 | it('Adds an input', done => { 36 | app.inputs.create([ 37 | { 38 | url: sampleImages[0], 39 | allowDuplicateUrl: true 40 | } 41 | ]) 42 | .then(inputs => { 43 | expect(inputs).toBeDefined(); 44 | expect(inputs instanceof Inputs).toBe(true); 45 | expect(inputs.length).toBe(1); 46 | expect(inputs[0].createdAt).toBeDefined(); 47 | expect(inputs[0].id).toBeDefined(); 48 | inputId = inputs[0].id; 49 | expect(inputs[0].rawData).toBeDefined(); 50 | done(); 51 | }) 52 | .catch(errorHandler.bind(done)); 53 | }); 54 | 55 | it('Adds an input with concepts', done => { 56 | app.inputs.create([ 57 | { 58 | url: sampleImages[0], 59 | allowDuplicateUrl: true, 60 | concepts: [ 61 | { 62 | id: 'train', 63 | value: true 64 | }, 65 | { 66 | id: 'car', 67 | value: false 68 | }, 69 | { 70 | id: '的な' 71 | } 72 | ] 73 | } 74 | ]) 75 | .then(inputs => { 76 | expect(inputs).toBeDefined(); 77 | expect(inputs instanceof Inputs).toBe(true); 78 | expect(inputs.length).toBe(1); 79 | expect(inputs[0].createdAt).toBeDefined(); 80 | expect(inputs[0].id).toBeDefined(); 81 | expect(inputs[0].rawData).toBeDefined(); 82 | done(); 83 | }) 84 | .catch(errorHandler.bind(done)); 85 | }); 86 | 87 | it('Adds input with metadata', done => { 88 | app.inputs.create({ 89 | id: inputId1, 90 | url: sampleImages[1], 91 | allowDuplicateUrl: true, 92 | concepts: [{id: beerId}], 93 | metadata: {foo: 'bar', baz: 'blah'} 94 | }) 95 | .then(inputs => { 96 | expect(inputs).toBeDefined(); 97 | expect(inputs instanceof Inputs).toBe(true); 98 | expect(inputs[0].id).toBe(inputId1); 99 | expect(inputs[0].rawData.data.metadata.foo).toBe('bar'); 100 | expect(inputs[0].rawData.status.code === 30000); 101 | done(); 102 | }) 103 | .catch(errorHandler.bind(done)); 104 | 105 | app.inputs.create({ 106 | id: inputId2, 107 | url: sampleImages[2], 108 | allowDuplicateUrl: true, 109 | concepts: [{id: beerId}], 110 | metadata: {foo: 'bar', baz2: 'blah2'} 111 | }) 112 | .then(inputs => { 113 | expect(inputs).toBeDefined(); 114 | expect(inputs instanceof Inputs).toBe(true); 115 | expect(inputs[0].id).toBe(inputId2); 116 | expect(inputs[0].rawData.data.metadata.foo).toBe('bar'); 117 | expect(inputs[0].rawData.status.code === 30000); 118 | done(); 119 | }) 120 | .catch(errorHandler.bind(done)); 121 | }); 122 | 123 | it('Adds input with geodata', done => { 124 | app.inputs.create([ 125 | { 126 | id: inputId3, 127 | url: sampleImages[0], 128 | allowDuplicateUrl: true, 129 | concepts: [{id: beerId}], 130 | geo: {longitude: -30, latitude: 40} 131 | }, 132 | { 133 | id: inputId4, 134 | url: sampleImages[1], 135 | allowDuplicateUrl: true, 136 | concepts: [{id: beerId}], 137 | geo: {longitude: -20, latitude: 42.05}, 138 | metadata: {test: [1, 2, 3, 4]} 139 | }, 140 | { 141 | id: inputId5, 142 | url: sampleImages[2], 143 | allowDuplicateUrl: true, 144 | concepts: [{id: beerId}], 145 | geo: {longitude: -20, latitude: 42.05}, 146 | metadata: {test: [1, 2, 3, 4]} 147 | } 148 | ]) 149 | .then(inputs => { 150 | expect(inputs).toBeDefined(); 151 | expect(inputs instanceof Inputs).toBe(true); 152 | expect(inputs.length).toBe(3); 153 | expect(inputs[0].id).toBe(inputId3); 154 | expect(inputs[1].id).toBe(inputId4); 155 | expect(inputs[0].geo.geoPoint.latitude).toBe(40); 156 | expect(inputs[0].geo.geoPoint.longitude).toBe(-30); 157 | expect(inputs[1].geo.geoPoint.latitude).toBe(42.05); 158 | expect(inputs[1].geo.geoPoint.longitude).toBe(-20); 159 | expect(inputs[1].rawData.data.metadata.test).toBeDefined(); 160 | expect(inputs[1].rawData.data.metadata.test[0]).toBe(1); 161 | expect(inputs[1].rawData.data.metadata.test[1]).toBe(2); 162 | expect(inputs[1].metadata.test).toBeDefined(); 163 | expect(inputs[1].metadata.test[2]).toBe(3); 164 | expect(inputs[1].metadata.test[3]).toBe(4); 165 | pollStatus(interval => { 166 | app.inputs.getStatus() 167 | .then(data => { 168 | if (data['counts']['to_process'] === 0 && data['counts']['processing'] === 0) { 169 | clearInterval(interval); 170 | if (data['errors'] > 0) { 171 | throw new Error('Error processing inputs', data); 172 | } else { 173 | done(); 174 | } 175 | } 176 | }) 177 | .catch(errorHandler.bind(done)); 178 | }); 179 | }) 180 | .catch(errorHandler.bind(done)); 181 | }); 182 | 183 | it('Bulk adds inputs', done => { 184 | app.inputs.create([ 185 | { 186 | url: sampleImages[0], 187 | allowDuplicateUrl: true 188 | }, 189 | { 190 | url: sampleImages[4], 191 | allowDuplicateUrl: true 192 | } 193 | ]) 194 | .then(inputs => { 195 | expect(inputs).toBeDefined(); 196 | expect(inputs instanceof Inputs).toBe(true); 197 | expect(inputs.length).toBe(2); 198 | expect(inputs[0].createdAt).toBeDefined(); 199 | expect(inputs[0].id).toBeDefined(); 200 | expect(inputs[0].rawData).toBeDefined(); 201 | pollStatus(interval => { 202 | app.inputs.getStatus() 203 | .then(data => { 204 | if (data['counts']['to_process'] === 0 && data['counts']['processing'] === 0) { 205 | clearInterval(interval); 206 | if (data['errors'] > 0) { 207 | throw new Error('Error processing inputs', data); 208 | } else { 209 | done(); 210 | } 211 | } 212 | }) 213 | .catch(errorHandler.bind(done)); 214 | }); 215 | }) 216 | .catch(errorHandler.bind(done)); 217 | }); 218 | 219 | it('Bulk adds inputs with concepts', done => { 220 | app.inputs.create([ 221 | { 222 | url: sampleImages[5], 223 | allowDuplicateUrl: true, 224 | concepts: [ 225 | {id: ferrariId}, 226 | {id: 'outdoors', value: false}, 227 | {id: langConceptId} 228 | ] 229 | }, 230 | { 231 | url: sampleImages[6], 232 | allowDuplicateUrl: true, 233 | concepts: [ 234 | { 235 | id: ferrariId 236 | }, 237 | { 238 | id: 'outdoors', 239 | value: false 240 | } 241 | ] 242 | } 243 | ]) 244 | .then(inputs => { 245 | expect(inputs).toBeDefined(); 246 | expect(inputs instanceof Inputs).toBe(true); 247 | expect(inputs.length).toBe(2); 248 | expect(inputs[0].createdAt).toBeDefined(); 249 | expect(inputs[0].id).toBeDefined(); 250 | expect(inputs[0].rawData).toBeDefined(); 251 | pollStatus(interval => { 252 | app.inputs.getStatus() 253 | .then(data => { 254 | lastCount = data['counts']['processed']; 255 | if (data['counts']['to_process'] === 0 && data['counts']['processing'] === 0) { 256 | clearInterval(interval); 257 | if (data['errors'] > 0) { 258 | throw new Error('Error processing inputs', data); 259 | } else { 260 | done(); 261 | } 262 | } 263 | }) 264 | .catch(errorHandler.bind(done)); 265 | }) 266 | }) 267 | .catch(errorHandler.bind(done)); 268 | }); 269 | 270 | it('Gets all inputs', done => { 271 | app.inputs.list({ 272 | page: 1, 273 | perPage: 5 274 | }) 275 | .then(inputs => { 276 | expect(inputs).toBeDefined(); 277 | expect(inputs instanceof Inputs).toBe(true); 278 | expect(inputs.length).toBe(5); 279 | var input = inputs[0]; 280 | expect(input.id).toBeDefined(); 281 | expect(input.createdAt).toBeDefined(); 282 | expect(input.rawData).toBeDefined(); 283 | done(); 284 | }) 285 | .catch(errorHandler.bind(done)); 286 | }); 287 | 288 | it('Gets a single input by id', done => { 289 | app.inputs.get(inputId) 290 | .then(input => { 291 | expect(input).toBeDefined(); 292 | expect(input.id).toBe(inputId); 293 | expect(input.createdAt).toBeDefined(); 294 | expect(input.rawData).toBeDefined(); 295 | done(); 296 | }) 297 | .catch(errorHandler.bind(done)); 298 | }); 299 | 300 | it('Gets inputs status', done => { 301 | app.inputs.getStatus() 302 | .then(response => { 303 | expect(response.counts).toBeDefined(); 304 | var counts = response.counts; 305 | expect(counts.processed).toBeDefined(); 306 | expect(counts.to_process).toBeDefined(); 307 | expect(counts.errors).toBeDefined(); 308 | expect(counts.errors).toBe(0); 309 | done(); 310 | }) 311 | .catch(errorHandler.bind(done)); 312 | }); 313 | 314 | it('Updates an input by merging concepts', done => { 315 | app.inputs.mergeConcepts([ 316 | { 317 | id: inputId, 318 | concepts: [ 319 | {'id': 'train', 'value': true}, 320 | {'id': 'car', 'value': false} 321 | ] 322 | } 323 | ]) 324 | .then(inputs => { 325 | expect(inputs).toBeDefined(); 326 | expect(inputs.length).toBe(1); 327 | expect(inputs instanceof Inputs).toBe(true); 328 | expect(inputs[0].createdAt).toBeDefined(); 329 | expect(inputs[0].id).toBeDefined(); 330 | expect(inputs[0].rawData).toBeDefined(); 331 | done(); 332 | }) 333 | .catch(errorHandler.bind(done)); 334 | }); 335 | 336 | it('Updates an input by overwriting concepts', done => { 337 | app.inputs.overwriteConcepts([ 338 | { 339 | id: inputId, 340 | concepts: [ 341 | {id: 'train', value: false}, 342 | {id: 'car', value: true}, 343 | {id: 'car2', value: false}, 344 | {id: 'car3', value: false} 345 | ] 346 | } 347 | ]) 348 | .then(inputs => { 349 | expect(inputs).toBeDefined(); 350 | expect(inputs.length).toBe(1); 351 | expect(inputs instanceof Inputs).toBe(true); 352 | expect(inputs[0].concepts.length).toBe(4); 353 | for (let i = 0; i < inputs[0].concepts; i++) { 354 | switch (inputs[0].concepts[i].name) { 355 | case 'train': 356 | expect(inputs[0].concepts[i].value).toBe(0); 357 | break; 358 | case 'car': 359 | expect(inputs[0].concepts[i].value).toBe(1); 360 | break; 361 | case 'car2': 362 | expect(inputs[0].concepts[i].value).toBe(0); 363 | break; 364 | case 'car3': 365 | expect(inputs[0].concepts[i].value).toBe(0); 366 | break; 367 | } 368 | } 369 | done(); 370 | }) 371 | .catch(errorHandler.bind(done)); 372 | }); 373 | 374 | it('Updates an input by deleting concepts', done => { 375 | app.inputs.deleteConcepts([ 376 | { 377 | id: inputId, 378 | concepts: [ 379 | {id: 'train'}, 380 | {id: 'car'}, 381 | {id: 'car2'}, 382 | {id: 'car3'} 383 | ] 384 | } 385 | ]) 386 | .then(inputs => { 387 | expect(inputs).toBeDefined(); 388 | expect(inputs.length).toBe(1); 389 | expect(inputs instanceof Inputs).toBe(true); 390 | expect(inputs[0].concepts.length).toBe(0); 391 | done(); 392 | }) 393 | .catch(errorHandler.bind(done)); 394 | }); 395 | }); 396 | 397 | 398 | describe('Integration Tests - Inputs Search', () => { 399 | beforeAll(done => { 400 | app.inputs.create({ 401 | url: sampleImages[0], 402 | allowDuplicateUrl: true, 403 | concepts: [{id: ferrariId, value: true}] 404 | }) 405 | .then(_ => { 406 | return waitForInputsUpload(app); 407 | }) 408 | .then(_ => { 409 | return app.models.create(testModelId, [ 410 | { id: ferrariId } 411 | ]) 412 | }) 413 | .then(testModel => { 414 | return testModel.train(true); 415 | }) 416 | .then(model => { 417 | done(); 418 | }) 419 | .catch(errorHandler.bind(done)); 420 | }); 421 | 422 | it('Filter by image url only', done => { 423 | app.inputs.search( 424 | { 425 | input: 426 | { 427 | type: 'input', 428 | url: sampleImages[0] 429 | } 430 | }) 431 | .then(response => { 432 | expect(response.hits.length).not.toBeLessThan(1); 433 | expect(response.hits[0].score).toBeDefined(); 434 | done(); 435 | }) 436 | .catch(errorHandler.bind(done)); 437 | 438 | app.inputs.search( 439 | { 440 | input: { 441 | type: 'input', 442 | url: sampleImages[3] 443 | } 444 | }) 445 | .then(response => { 446 | expect(response.hits.length).toBe(0); 447 | done(); 448 | }) 449 | .catch(errorHandler.bind(done)); 450 | }); 451 | 452 | it('Filter by concepts/inputs only', done => { 453 | app.inputs.search([ 454 | { input: { url: sampleImages[0] } }, 455 | { input: { url: sampleImages[4]} } 456 | ]) 457 | .then(response => { 458 | expect(response.hits[0].score).toBeDefined(); 459 | done(); 460 | }) 461 | .catch(errorHandler.bind(done)); 462 | }); 463 | 464 | it('Filter by images and concepts', done => { 465 | app.inputs.search([ 466 | {input: {url: sampleImages[0]}}, 467 | {concept: {name: ferrariId}} 468 | ]) 469 | .then(response => { 470 | expect(response.hits[0].score).toBeDefined(); 471 | done(); 472 | }) 473 | .catch(errorHandler.bind(done)); 474 | }); 475 | 476 | it('Filter by image id only', done => { 477 | app.inputs.search( 478 | { 479 | input: 480 | { 481 | type: 'input', 482 | id: inputId1 483 | } 484 | }) 485 | .then(response => { 486 | expect(response.hits.length).toBe(1); 487 | expect(response.hits[0].score).toBeDefined(); 488 | done(); 489 | }) 490 | .catch(errorHandler.bind(done)); 491 | }); 492 | 493 | it('Filter by image id and url', done => { 494 | app.inputs.search( 495 | [{ 496 | input: { 497 | type: 'input', 498 | id: inputId3, 499 | } 500 | }, 501 | { 502 | input: { 503 | type: 'input', 504 | url: sampleImages[0] 505 | } 506 | }]) 507 | .then(response => { 508 | expect(response.hits.length).toBe(1); 509 | expect(response.hits[0].score).toBeDefined(); 510 | done(); 511 | }) 512 | .catch(errorHandler.bind(done)); 513 | }); 514 | 515 | it('Filter with metadata only', done => { 516 | app.inputs.search([ 517 | { input: { metadata: { baz: 'blah' }, type: 'input' } } 518 | ]) 519 | .then(response => { 520 | expect(response.hits[0].score).toBeDefined(); 521 | expect(response.hits.length).toBe(1); 522 | done(); 523 | }) 524 | .catch(errorHandler.bind(done)); 525 | }); 526 | 527 | it('Filter with geopoint and a radius', done => { 528 | app.inputs.search({ 529 | input: { 530 | geo: { 531 | longitude: -19, 532 | latitude: 43, 533 | type: 'withinRadians', 534 | value: 1 535 | } 536 | } 537 | }) 538 | .then(response => { 539 | expect(response.hits.length).toBe(3); 540 | expect(response.hits[0].score).toBeDefined(); 541 | done(); 542 | }) 543 | .catch(errorHandler.bind(done)); 544 | }); 545 | 546 | it('Filter with geo box', done => { 547 | app.inputs.search({ 548 | input: { 549 | geo: [{ 550 | latitude: 41, 551 | longitude: -31 552 | }, { 553 | latitude: 43.05, 554 | longitude: -19 555 | }] 556 | } 557 | }) 558 | .then(response => { 559 | expect(response.hits.length).toBe(2); 560 | expect(response.hits[0].score).toBeDefined(); 561 | expect(response.hits[1].score).toBeDefined(); 562 | done(); 563 | }) 564 | .catch(errorHandler.bind(done)); 565 | }); 566 | 567 | it('Filter with metadata and geodata', done => { 568 | app.inputs.search({ 569 | input: { 570 | metadata: { 571 | test: [1, 2, 3, 4] 572 | }, 573 | geo: [{ 574 | latitude: 41, 575 | longitude: -31 576 | }, { 577 | latitude: 43.05, 578 | longitude: -19 579 | }] 580 | } 581 | }) 582 | .then(response => { 583 | expect(response.hits[0].score).toBeDefined(); 584 | done(); 585 | }) 586 | .catch(errorHandler.bind(done)); 587 | }); 588 | 589 | it('Filter with metadata and image url', done => { 590 | app.inputs.search([ 591 | { 592 | input: { 593 | type: 'input', 594 | url: sampleImages[3] 595 | } 596 | }, 597 | { 598 | input: { 599 | metadata: { 600 | foo: 'bar' 601 | } 602 | }, 603 | }]) 604 | .then(response => { 605 | expect(response.hits.length).toBe(0); 606 | done(); 607 | }) 608 | .catch(errorHandler.bind(done)); 609 | }); 610 | 611 | it('Search with concept in a different language (japanese)', done => { 612 | app.inputs.search({ 613 | concept: { 614 | name: langConceptId, 615 | type: 'input' 616 | }, 617 | language: 'ja' 618 | }) 619 | .then(response => { 620 | expect(response.hits.length).toBe(1); 621 | 622 | app.inputs.delete() 623 | .then(() => { 624 | return app.models.delete(); 625 | }) 626 | .then(response => { 627 | expect(response.status).toBeDefined(); 628 | done(); 629 | }) 630 | }) 631 | .catch(errorHandler.bind(done)); 632 | }); 633 | }); 634 | -------------------------------------------------------------------------------- /tests/integration/models-int-tests.js: -------------------------------------------------------------------------------- 1 | const Clarifai = require('./../../src'); 2 | const {sampleImages, sampleVideos, TINY_IMAGE_BASE64} = require('../assets/test-data'); 3 | const {errorHandler, waitForInputsUpload} = require('./helpers'); 4 | const d = Date.now(); 5 | const ferrariId = 'ferrari' + d; 6 | const langConceptId = '的な' + d; 7 | const beerId = 'beer' + d; 8 | const testModelId = 'vroom-vroom' + d; 9 | 10 | let app; 11 | let testModelVersionId; 12 | let conceptsCount; 13 | 14 | const conceptsIds = [ 15 | 'porsche' + Date.now(), 16 | 'rolls royce' + Date.now(), 17 | 'lamborghini' + Date.now(), 18 | langConceptId, 19 | beerId, 20 | ferrariId 21 | ]; 22 | 23 | describe('Integration Tests - Models', () => { 24 | // TODO: The tests below that depend on testModel are required to be run after 25 | // 'Creates a new model' since it initializes testModel. Refactor the tests 26 | // so each one creates their own resources. 27 | var testModel; 28 | 29 | beforeAll(() => { 30 | app = new Clarifai.App({ 31 | apiKey: process.env.CLARIFAI_API_KEY, 32 | apiEndpoint: process.env.API_ENDPOINT 33 | }); 34 | }); 35 | 36 | it('Creates a new model', done => { 37 | app.models.create(testModelId, [ 38 | { 39 | id: ferrariId 40 | }, 41 | { 42 | id: langConceptId 43 | } 44 | ]) 45 | .then(model => { 46 | expect(model).toBeDefined(); 47 | expect(model.name).toBeDefined(); 48 | expect(model.id).toBeDefined(); 49 | testModel = model; 50 | expect(model.createdAt).toBeDefined(); 51 | expect(model.appId).toBeDefined(); 52 | expect(model.outputInfo).toBeDefined(); 53 | expect(model.modelVersion).toBeDefined(); 54 | done(); 55 | }) 56 | .catch(errorHandler.bind(done)); 57 | }); 58 | 59 | it('Throws an error if no model id is given', done => { 60 | expect(() => { 61 | app.models.create({name: 'asdf'}, [{id: ferrariId}]); 62 | }).toThrow(new Error('The following param is required: Model ID')); 63 | done(); 64 | }); 65 | 66 | it('Gets status_code 21110 if train with no data', done => { 67 | testModel.versionId = null; 68 | testModel.train(true) 69 | .then(model => { 70 | expect(model).toBeDefined(); 71 | expect(model.modelVersion).toBeDefined(); 72 | expect(model.rawData).toBeDefined(); 73 | var version = model.modelVersion; 74 | expect(version.id).toBeDefined(); 75 | expect(version.created_at).toBeDefined(); 76 | expect(version.status.code).toBe(21110); 77 | done(); 78 | }) 79 | .catch(errorHandler.bind(done)); 80 | }); 81 | 82 | it('Adds inputs and trains the model', done => { 83 | app.inputs.delete() 84 | .then(() => { 85 | return app.inputs.create([ 86 | { 87 | url: sampleImages[5], 88 | allowDuplicateUrl: true, 89 | concepts: [{id: ferrariId, value: true}] 90 | }, 91 | { 92 | url: sampleImages[6], 93 | allowDuplicateUrl: true, 94 | concepts: [{id: langConceptId, value: true}] 95 | } 96 | ]) 97 | }) 98 | .then(() => { 99 | return waitForInputsUpload(app); 100 | }) 101 | .then(() => testModel.train(true)) 102 | .then(model => { 103 | expect(model).toBeDefined(); 104 | expect(model.modelVersion).toBeDefined(); 105 | expect(model.rawData).toBeDefined(); 106 | var version = model.modelVersion; 107 | expect(version.id).toBeDefined(); 108 | expect(version.created_at).toBeDefined(); 109 | expect(version.status.code).toBe(21100); 110 | done(); 111 | }) 112 | .catch(errorHandler.bind(done)); 113 | }); 114 | 115 | it('Searches for a model', done => { 116 | app.models.search(testModelId) 117 | .then(models => { 118 | expect(models).toBeDefined(); 119 | var model = models[0]; 120 | expect(model).toBeDefined(); 121 | expect(model.name).toBe(testModelId); 122 | expect(model.id).toBeDefined(); 123 | expect(model.createdAt).toBeDefined(); 124 | expect(model.appId).toBeDefined(); 125 | expect(model.outputInfo).toBeDefined(); 126 | expect(model.modelVersion).toBeDefined(); 127 | testModelVersionId = model.modelVersion.id; 128 | done(); 129 | }) 130 | .catch(errorHandler.bind(done)); 131 | }); 132 | 133 | it('Starts a model eval job and returns the result of creating it', done => { 134 | testModel.train(true) 135 | .then(model => model.runModelEval()) 136 | .then(modelVersion => { 137 | expect(modelVersion).toBeDefined(); 138 | expect(modelVersion.metrics).toBeDefined(); 139 | expect(modelVersion.metrics.status).toBeDefined(); 140 | expect([21300, 21301, 21303]).toContain(modelVersion.metrics.status.code); 141 | done(); 142 | }) 143 | .catch(errorHandler.bind(done)); 144 | }); 145 | 146 | it('Call predict on models collection given a model id', done => { 147 | app.models.predict(Clarifai.GENERAL_MODEL, [ 148 | { 149 | url: sampleImages[7] 150 | }, 151 | { 152 | url: sampleImages[8] 153 | } 154 | ]) 155 | .then(response => { 156 | expect(response.outputs).toBeDefined(); 157 | var outputs = response.outputs; 158 | expect(outputs.length).toBe(2); 159 | var output = outputs[0]; 160 | expect(output.id).toBeDefined(); 161 | expect(output.status).toBeDefined(); 162 | expect(output.input).toBeDefined(); 163 | expect(output.model).toBeDefined(); 164 | expect(output.created_at).toBeDefined(); 165 | expect(output.data).toBeDefined(); 166 | done(); 167 | }) 168 | .catch(errorHandler.bind(done)); 169 | }); 170 | 171 | it('Call predict with a declared input id', done => { 172 | app.models.predict(Clarifai.GENERAL_MODEL, [ 173 | { 174 | id: 'test-id-1', 175 | url: sampleImages[7] 176 | }, 177 | { 178 | id: 'test-id-2', 179 | url: sampleImages[8] 180 | } 181 | ]) 182 | .then(response => { 183 | expect(response.outputs).toBeDefined(); 184 | expect(response.outputs[0].input.id).toBe('test-id-1'); 185 | expect(response.outputs[1].input.id).toBe('test-id-2'); 186 | done(); 187 | }) 188 | .catch(errorHandler.bind(done)); 189 | }); 190 | 191 | it('Call predict on base64', done => { 192 | app.models.predict(Clarifai.GENERAL_MODEL, [ 193 | { 194 | base64: TINY_IMAGE_BASE64 195 | } 196 | ]) 197 | .then(response => { 198 | expect(response.outputs).toBeDefined(); 199 | var outputs = response.outputs; 200 | expect(outputs.length).toBe(1); 201 | var output = outputs[0]; 202 | expect(output.id).toBeDefined(); 203 | expect(output.status).toBeDefined(); 204 | expect(output.input).toBeDefined(); 205 | expect(output.model).toBeDefined(); 206 | expect(output.created_at).toBeDefined(); 207 | expect(output.data).toBeDefined(); 208 | done(); 209 | }) 210 | .catch(errorHandler.bind(done)); 211 | }); 212 | 213 | it('Call predict on file path', done => { 214 | app.models.predict(Clarifai.GENERAL_MODEL, [ 215 | { 216 | file: 'tests/assets/tiny-image.png' 217 | } 218 | ]) 219 | .then(response => { 220 | expect(response.outputs).toBeDefined(); 221 | var outputs = response.outputs; 222 | expect(outputs.length).toBe(1); 223 | var output = outputs[0]; 224 | expect(output.id).toBeDefined(); 225 | expect(output.status).toBeDefined(); 226 | expect(output.input).toBeDefined(); 227 | expect(output.model).toBeDefined(); 228 | expect(output.created_at).toBeDefined(); 229 | expect(output.data).toBeDefined(); 230 | done(); 231 | }) 232 | .catch(errorHandler.bind(done)); 233 | }); 234 | 235 | 236 | it('Call predict on video inputs', done => { 237 | app.models.predict(Clarifai.GENERAL_MODEL, sampleVideos[0], {video: true}) 238 | .then(response => { 239 | expect(response.outputs).toBeDefined(); 240 | var outputs = response.outputs; 241 | expect(outputs.length).toBe(1); 242 | var output = outputs[0]; 243 | expect(output.id).toBeDefined(); 244 | expect(output.status).toBeDefined(); 245 | expect(output.input).toBeDefined(); 246 | expect(output.model).toBeDefined(); 247 | expect(output.created_at).toBeDefined(); 248 | expect(output.data).toBeDefined(); 249 | done(); 250 | }) 251 | .catch(errorHandler.bind(done)); 252 | }); 253 | 254 | it('Call predict on URL video with sample MS', done => { 255 | app.models.predict(Clarifai.GENERAL_MODEL, sampleVideos[1], {video: true, sampleMs: 2000}) 256 | .then(response => { 257 | expect(response.outputs).toBeDefined(); 258 | var outputs = response.outputs; 259 | expect(outputs.length).toBe(1); 260 | var output = outputs[0]; 261 | expect(output.id).toBeDefined(); 262 | expect(output.status).toBeDefined(); 263 | expect(output.input).toBeDefined(); 264 | expect(output.model).toBeDefined(); 265 | expect(output.created_at).toBeDefined(); 266 | expect(output.data).toBeDefined(); 267 | 268 | for (let i = 0; i < output.data.frames.length; i++) { 269 | let frame = output.data.frames[i]; 270 | 271 | expect(frame.frame_info.index).toBeDefined(); 272 | expect(frame.frame_info.time).toBeDefined(); 273 | 274 | expect((frame.frame_info.time+1000) % 2000).toEqual(0); 275 | } 276 | 277 | done(); 278 | }) 279 | .catch(errorHandler.bind(done)); 280 | }); 281 | 282 | it('Attaches model outputs', done => { 283 | app.models.initModel(Clarifai.GENERAL_MODEL) 284 | .then(generalModel => { 285 | return generalModel.predict([ 286 | { 287 | url: sampleImages[7] 288 | }, 289 | { 290 | url: sampleImages[8] 291 | } 292 | ]) 293 | }) 294 | .then(response => { 295 | expect(response.outputs).toBeDefined(); 296 | var outputs = response.outputs; 297 | expect(outputs.length).toBe(2); 298 | var output = outputs[0]; 299 | expect(output.id).toBeDefined(); 300 | expect(output.status).toBeDefined(); 301 | expect(output.input).toBeDefined(); 302 | expect(output.model).toBeDefined(); 303 | expect(output.created_at).toBeDefined(); 304 | expect(output.data).toBeDefined(); 305 | done(); 306 | }) 307 | .catch(errorHandler.bind(done)); 308 | }); 309 | 310 | it('Can predict on public models in a different language (simplified chinese)', done => { 311 | app.models.initModel(Clarifai.GENERAL_MODEL) 312 | .then(generalModel => { 313 | return generalModel.predict(sampleImages[0], {language: 'zh'}); 314 | }) 315 | .then(response => { 316 | expect(response.outputs).toBeDefined(); 317 | var concepts = response['outputs'][0]['data']['concepts'] 318 | expect(concepts[0]['id']).toBe('ai_HLmqFqBf'); 319 | expect(concepts[0]['name']).toBe('铁路列车'); 320 | expect(concepts[1]['id']).toBe('ai_fvlBqXZR'); 321 | expect(concepts[1]['name']).toBe('铁路'); 322 | done(); 323 | }) 324 | .catch(errorHandler.bind(done)); 325 | }); 326 | 327 | it('Can predict on public models in a different language (japanese)', done => { 328 | app.models.initModel(Clarifai.GENERAL_MODEL) 329 | .then(generalModel => { 330 | return generalModel.predict(sampleImages[0], {language: 'ja'}); 331 | }) 332 | .then(response => { 333 | expect(response.outputs).toBeDefined(); 334 | var concepts = response['outputs'][0]['data']['concepts']; 335 | expect(concepts[0]['id']).toBe('ai_HLmqFqBf'); 336 | expect(concepts[0]['name']).toBe('列車'); 337 | expect(concepts[1]['id']).toBe('ai_fvlBqXZR'); 338 | expect(concepts[1]['name']).toBe('鉄道'); 339 | done(); 340 | }) 341 | .catch(errorHandler.bind(done)); 342 | }); 343 | 344 | it('Can predict on public models in a different language (russian)', done => { 345 | app.models.initModel(Clarifai.GENERAL_MODEL) 346 | .then(generalModel => { 347 | return generalModel.predict(sampleImages[0], {language: 'ru'}); 348 | }) 349 | .then(response => { 350 | expect(response.outputs).toBeDefined(); 351 | var concepts = response['outputs'][0]['data']['concepts'] 352 | expect(concepts[0]['id']).toBe('ai_HLmqFqBf'); 353 | expect(concepts[0]['name']).toBe('поезд'); 354 | expect(concepts[1]['id']).toBe('ai_fvlBqXZR'); 355 | expect(concepts[1]['name']).toBe('железная дорога'); 356 | done(); 357 | }) 358 | .catch(errorHandler.bind(done)); 359 | }); 360 | 361 | it('Can set a max number of concepts returned for a model', done => { 362 | const MAX_CONCEPTS = 2; 363 | app.models.initModel(Clarifai.GENERAL_MODEL) 364 | .then(generalModel => generalModel.predict(sampleImages[0], {maxConcepts: MAX_CONCEPTS})) 365 | .then(response => { 366 | expect(response.outputs).toBeDefined(); 367 | const concepts = response['outputs'][0]['data']['concepts']; 368 | expect(concepts.length).toBe(MAX_CONCEPTS); 369 | expect(concepts[0]['id']).toBe('ai_HLmqFqBf'); 370 | expect(concepts[0]['name']).toBe('train'); 371 | expect(concepts[1]['id']).toBe('ai_fvlBqXZR'); 372 | expect(concepts[1]['name']).toBe('railway'); 373 | done(); 374 | }) 375 | .catch(errorHandler.bind(done)); 376 | }); 377 | 378 | it('Can set a min value threshold for concepts', done => { 379 | const MIN_VALUE = 0.95; 380 | app.models.initModel(Clarifai.GENERAL_MODEL) 381 | .then(generalModel => generalModel.predict(sampleImages[0], {minValue: MIN_VALUE})) 382 | .then(response => { 383 | expect(response.outputs).toBeDefined(); 384 | const concepts = response['outputs'][0]['data']['concepts']; 385 | concepts.forEach(c => expect(c.value).toBeGreaterThan(MIN_VALUE)); 386 | expect(concepts[0]['id']).toBe('ai_HLmqFqBf'); 387 | expect(concepts[0]['name']).toBe('train'); 388 | expect(concepts[1]['id']).toBe('ai_fvlBqXZR'); 389 | expect(concepts[1]['name']).toBe('railway'); 390 | done(); 391 | }) 392 | .catch(errorHandler.bind(done)); 393 | }); 394 | 395 | it('Can select concepts to return', done => { 396 | const selectConcepts = [ 397 | {name: 'train'}, 398 | {id: 'ai_6kTjGfF6'} 399 | ]; 400 | 401 | app.models.initModel(Clarifai.GENERAL_MODEL) 402 | .then(generalModel => generalModel.predict(sampleImages[0], {selectConcepts})) 403 | .then(response => { 404 | expect(response.outputs).toBeDefined(); 405 | const concepts = response['outputs'][0]['data']['concepts']; 406 | expect(concepts.length).toBe(selectConcepts.length); 407 | expect(concepts[0]['id']).toBe('ai_HLmqFqBf'); 408 | expect(concepts[0]['name']).toBe('train'); 409 | expect(concepts[1]['id']).toBe('ai_6kTjGfF6'); 410 | expect(concepts[1]['name']).toBe('station'); 411 | done(); 412 | }) 413 | .catch(errorHandler.bind(done)); 414 | }); 415 | 416 | 417 | it('Update model name and config', done => { 418 | app.models.update({ 419 | id: testModel.id, 420 | name: 'Super Cars', 421 | conceptsMutuallyExclusive: true, 422 | closedEnvironment: true 423 | }) 424 | .then(models => { 425 | expect(models).toBeDefined(); 426 | expect(models[0]).toBeDefined(); 427 | expect(models[0].id).toBe(testModel.id); 428 | expect(models[0].name).toBe('Super Cars'); 429 | expect(models[0].outputInfo.output_config.concepts_mutually_exclusive).toBe(true); 430 | expect(models[0].outputInfo.output_config.closed_environment).toBe(true); 431 | done(); 432 | }) 433 | .catch(errorHandler.bind(done)); 434 | }); 435 | 436 | it('Update model concepts', done => { 437 | app.models.update({ 438 | id: testModel.id, 439 | concepts: conceptsIds 440 | }) 441 | .then(models => { 442 | return models[0].getOutputInfo(); 443 | }) 444 | .then(model => { 445 | expect(model.outputInfo).toBeDefined(); 446 | expect(model.outputInfo.data.concepts).toBeDefined(); 447 | expect(model.outputInfo.data.concepts.length).toBe(conceptsIds.length); 448 | var conceptsIdsCopy = conceptsIds.slice(0); 449 | var totalFound = 0; 450 | for (var i = 0; i < model.outputInfo.data.concepts.length; i++) { 451 | var currConcept = model.outputInfo.data.concepts[i]; 452 | var pos = conceptsIdsCopy.indexOf(currConcept.id); 453 | if (pos > -1) { 454 | totalFound++; 455 | conceptsIdsCopy.splice(pos, 1) 456 | } 457 | } 458 | expect(totalFound).toBe(conceptsIds.length); 459 | done(); 460 | }) 461 | .catch(errorHandler.bind(done)); 462 | }); 463 | 464 | it('Updates model by merging concepts', done => { 465 | var testConcepts = ['random-concept-1', 'random-concept-2']; 466 | app.models.mergeConcepts({ 467 | id: testModel.id, 468 | concepts: testConcepts 469 | }) 470 | .then(models => { 471 | return models[0].getOutputInfo(); 472 | }) 473 | .then(model => { 474 | expect(model.outputInfo).toBeDefined(); 475 | expect(model.outputInfo.data.concepts.length).toBe(conceptsIds.length + testConcepts.length); 476 | conceptsCount = model.outputInfo.data.concepts.length; 477 | var conceptsCopy = Array.from(model.outputInfo.data.concepts).map(el => { 478 | return el.id; 479 | }); 480 | var totalFound = 0; 481 | for (var i = 0; i < testConcepts.length; i++) { 482 | var pos = conceptsCopy.indexOf(testConcepts[i]); 483 | if (pos > -1) { 484 | totalFound++; 485 | conceptsCopy.splice(pos, 1); 486 | } 487 | } 488 | expect(totalFound).toBe(2); 489 | done(); 490 | }) 491 | .catch(errorHandler.bind(done)); 492 | }); 493 | 494 | it('Updates a single model', done => { 495 | var testConcepts = ['random-concept-0']; 496 | app.models.initModel(testModel.id) 497 | .then(model => { 498 | return model.mergeConcepts(testConcepts); 499 | }) 500 | .then(response => { 501 | expect(response.outputInfo).toBeDefined(); 502 | expect(response.modelVersion.active_concept_count > conceptsCount).toBe(true); 503 | done(); 504 | }) 505 | .catch(errorHandler.bind(done)); 506 | }); 507 | 508 | it('Updates model by overwriting concepts', done => { 509 | var testConcepts = ['random-concept-1', 'random-concept-3', 'random-concept-4']; 510 | app.models.overwriteConcepts({ 511 | id: testModel.id, 512 | concepts: testConcepts 513 | }) 514 | .then(models => { 515 | return models[0].getOutputInfo(); 516 | }) 517 | .then(model => { 518 | expect(model.outputInfo).toBeDefined(); 519 | expect(model.outputInfo.data.concepts.length).toBe(testConcepts.length); 520 | done(); 521 | }) 522 | .catch(errorHandler.bind(done)); 523 | }); 524 | 525 | it('Updates model by deleting concepts', done => { 526 | app.models.deleteConcepts({ 527 | id: testModel.id, 528 | concepts: [ 529 | 'random-concept-1', 530 | 'random-concept-2', 531 | 'random-concept-3', 532 | 'random-concept-4' 533 | ] 534 | }) 535 | .then(models => { 536 | return models[0].getOutputInfo(); 537 | }) 538 | .then(model => { 539 | expect(model.outputInfo.data).toBeUndefined(); 540 | return app.models.delete(); 541 | }) 542 | .then(response => { 543 | expect(response.status).toBeDefined(); 544 | done(); 545 | }) 546 | .catch(errorHandler.bind(done)); 547 | }); 548 | 549 | it('Gets output info', done => { 550 | app.models.getOutputInfo(Clarifai.GENERAL_MODEL) 551 | .then(model => { 552 | expect(model.id).toEqual(Clarifai.GENERAL_MODEL); 553 | done(); 554 | }) 555 | .catch(errorHandler.bind(done)); 556 | }); 557 | 558 | it('Gets output info when specifying model version', done => { 559 | let olderGeneralModelVersionID = 'aa9ca48295b37401f8af92ad1af0d91d'; 560 | app.models.getOutputInfo({id: Clarifai.GENERAL_MODEL, version: olderGeneralModelVersionID}) 561 | .then(model => { 562 | expect(model.modelVersion.id).toEqual(olderGeneralModelVersionID); 563 | done(); 564 | }) 565 | .catch(errorHandler.bind(done)); 566 | }); 567 | }); 568 | -------------------------------------------------------------------------------- /src/Models.js: -------------------------------------------------------------------------------- 1 | let axios = require('axios'); 2 | let Promise = require('promise'); 3 | let Model = require('./Model'); 4 | let Concepts = require('./Concepts'); 5 | let {API, ERRORS, replaceVars} = require('./constants'); 6 | let {isSuccess, checkType, clone} = require('./helpers'); 7 | let {wrapToken, formatModel} = require('./utils'); 8 | let {MODELS_PATH, MODEL_PATH, MODEL_SEARCH_PATH, MODEL_VERSION_PATH} = API; 9 | 10 | /** 11 | * class representing a collection of models 12 | * @class 13 | */ 14 | class Models { 15 | constructor(_config, rawData = []) { 16 | this._config = _config; 17 | this.rawData = rawData; 18 | rawData.forEach((modelData, index) => { 19 | this[index] = new Model(this._config, modelData); 20 | }); 21 | this.length = rawData.length; 22 | } 23 | 24 | /** 25 | * Returns a Model instance given model id or name. It will call search if name is given. 26 | * @param {string|object} model If string, it is assumed to be model id. Otherwise, if object is given, it can have any of the following keys: 27 | * @param {string} model.id Model id 28 | * @param {string} model.name Model name 29 | * @param {string} model.version Model version 30 | * @param {string} model.type This can be "concept", "color", "embed", "detect", "cluster", etc. 31 | * @return {Promise(Model, error)} A Promise that is fulfilled with a Model instance or rejected with an error 32 | */ 33 | initModel(model) { 34 | let data = {}; 35 | let fn; 36 | if (checkType(/String/, model)) { 37 | data.id = model; 38 | } else { 39 | data = model; 40 | } 41 | if (data.id) { 42 | fn = (resolve, reject) => { 43 | resolve(new Model(this._config, data)); 44 | }; 45 | } else { 46 | fn = (resolve, reject) => { 47 | this.search(data.name, data.type).then((models) => { 48 | if (data.version) { 49 | resolve(models.rawData.filter((model) => model.modelVersion.id === data.version)); 50 | } else { 51 | resolve(models[0]); 52 | } 53 | }, reject).catch(reject); 54 | }; 55 | } 56 | return new Promise(fn); 57 | } 58 | 59 | /** 60 | * Calls predict given model info and inputs to predict on 61 | * @param {string|object} model If string, it is assumed to be model id. Otherwise, if object is given, it can have any of the following keys: 62 | * @param {string} model.id Model id 63 | * @param {string} model.name Model name 64 | * @param {string} model.version Model version 65 | * @param {string} model.language Model language (only for Clarifai's public models) 66 | * @param {string} model.type This can be "concept", "color", "embed", "detect", "cluster", etc. 67 | * @param {object[]|object|string} inputs An array of objects/object/string pointing to an image resource. A string can either be a url or base64 image bytes. Object keys explained below: 68 | * @param {object} inputs[].image Object with keys explained below: 69 | * @param {string} inputs[].image.(url|base64|file) Can be a publicly accessibly url, base64 string representing image bytes, or file path (required) 70 | * @param {boolean} isVideo indicates if the input should be processed as a video (default false) 71 | * @return {Promise(response, error)} A Promise that is fulfilled with the API response or rejected with an error 72 | */ 73 | predict(model, inputs, config = {}) { 74 | if (checkType(/Boolean/, config)) { 75 | console.warn('"isVideo" argument is deprecated, consider using the configuration object instead'); 76 | config = { 77 | video: config 78 | }; 79 | } 80 | if (model.language) { 81 | config.language = model.language; 82 | } 83 | return new Promise((resolve, reject) => { 84 | this.initModel(model).then((modelObj) => { 85 | modelObj.predict(inputs, config) 86 | .then(resolve, reject) 87 | .catch(reject); 88 | }, reject); 89 | }); 90 | } 91 | 92 | /** 93 | * Calls train on a model and creates a new model version given model info 94 | * @param {string|object} model If string, it is assumed to be model id. Otherwise, if object is given, it can have any of the following keys: 95 | * @param {string} model.id Model id 96 | * @param {string} model.name Model name 97 | * @param {string} model.version Model version 98 | * @param {string} model.type This can be "concept", "color", "embed", "detect", "cluster", etc. 99 | * @param {boolean} sync If true, this returns after model has completely trained. If false, this immediately returns default api response. 100 | * @return {Promise(Model, error)} A Promise that is fulfilled with a Model instance or rejected with an error 101 | */ 102 | train(model, sync = false) { 103 | return new Promise((resolve, reject) => { 104 | this.initModel(model).then((model) => { 105 | model.train(sync) 106 | .then(resolve, reject) 107 | .catch(reject); 108 | }, reject); 109 | }); 110 | } 111 | 112 | /** 113 | * Returns a version of the model specified by its id 114 | * @param {string|object} model If string, it is assumed to be model id. Otherwise, if object is given, it can have any of the following keys: 115 | * @param {string} model.id Model id 116 | * @param {string} model.name Model name 117 | * @param {string} model.version Model version 118 | * @param {string} model.type This can be "concept", "color", "embed", "detect", "cluster", etc. 119 | * @param {string} versionId The model's id 120 | * @return {Promise(response, error)} A Promise that is fulfilled with the API response or rejected with an error 121 | */ 122 | getVersion(model, versionId) { 123 | return new Promise((resolve, reject) => { 124 | this.initModel(model).then((model) => { 125 | model.getVersion(versionId) 126 | .then(resolve, reject) 127 | .catch(reject); 128 | }, reject); 129 | }); 130 | } 131 | 132 | /** 133 | * Returns a list of versions of the model 134 | * @param {string|object} model If string, it is assumed to be model id. Otherwise, if object is given, it can have any of the following keys: 135 | * @param {string} model.id Model id 136 | * @param {string} model.name Model name 137 | * @param {string} model.version Model version 138 | * @param {string} model.type This can be "concept", "color", "embed", "detect", "cluster", etc. 139 | * @param {object} options Object with keys explained below: (optional) 140 | * @param {number} options.page The page number (optional, default: 1) 141 | * @param {number} options.perPage Number of images to return per page (optional, default: 20) 142 | * @return {Promise(response, error)} A Promise that is fulfilled with the API response or rejected with an error 143 | */ 144 | getVersions(model, options = {page: 1, perPage: 20}) { 145 | return new Promise((resolve, reject) => { 146 | this.initModel(model).then((model) => { 147 | model.getVersions(options) 148 | .then(resolve, reject) 149 | .catch(reject); 150 | }, reject); 151 | }); 152 | } 153 | 154 | /** 155 | * Returns all the model's output info 156 | * @param {string|object} model If string, it is assumed to be model id. Otherwise, if object is given, it can have any of the following keys: 157 | * @param {string} model.id Model id 158 | * @param {string} model.name Model name 159 | * @param {string} model.version Model version 160 | * @param {string} model.type This can be "concept", "color", "embed", "detect", "cluster", etc. 161 | * @return {Promise(Model, error)} A Promise that is fulfilled with a Model instance or rejected with an error 162 | */ 163 | getOutputInfo(model) { 164 | return new Promise((resolve, reject) => { 165 | this.initModel(model).then((model) => { 166 | model.getOutputInfo() 167 | .then(resolve, reject) 168 | .catch(reject); 169 | }, reject); 170 | }); 171 | } 172 | 173 | /** 174 | * Returns all the models 175 | * @param {Object} options Object with keys explained below: (optional) 176 | * @param {Number} options.page The page number (optional, default: 1) 177 | * @param {Number} options.perPage Number of images to return per page (optional, default: 20) 178 | * @return {Promise(Models, error)} A Promise that is fulfilled with an instance of Models or rejected with an error 179 | */ 180 | list(options = {page: 1, perPage: 20}) { 181 | let url = `${this._config.basePath}${MODELS_PATH}`; 182 | return wrapToken(this._config, (headers) => { 183 | return new Promise((resolve, reject) => { 184 | axios.get(url, { 185 | params: {'per_page': options.perPage, 'page': options.page}, 186 | headers 187 | }).then((response) => { 188 | if (isSuccess(response)) { 189 | resolve(new Models(this._config, response.data.models)); 190 | } else { 191 | reject(response); 192 | } 193 | }, reject); 194 | }); 195 | }); 196 | } 197 | 198 | /** 199 | * Create a model 200 | * @param {string|object} model If string, it is assumed to be the model id. Otherwise, if object is given, it can have any of the following keys: 201 | * @param {string} model.id Model id 202 | * @param {string} model.name Model name 203 | * @param {object[]|string[]|Concepts[]} conceptsData List of objects with ids, concept id strings or an instance of Concepts object 204 | * @param {Object} options Object with keys explained below: 205 | * @param {boolean} options.conceptsMutuallyExclusive Do you expect to see more than one of the concepts in this model in the SAME image? Set to false (default) if so. Otherwise, set to true. 206 | * @param {boolean} options.closedEnvironment Do you expect to run the trained model on images that do not contain ANY of the concepts in the model? Set to false (default) if so. Otherwise, set to true. 207 | * @return {Promise(Model, error)} A Promise that is fulfilled with an instance of Model or rejected with an error 208 | */ 209 | create(model, conceptsData = [], options = {}) { 210 | let concepts = conceptsData instanceof Concepts ? 211 | conceptsData.toObject('id') : 212 | conceptsData.map((concept) => { 213 | let val = concept; 214 | if (checkType(/String/, concept)) { 215 | val = {'id': concept}; 216 | } 217 | return val; 218 | }); 219 | let modelObj = model; 220 | if (checkType(/String/, model)) { 221 | modelObj = {id: model, name: model}; 222 | } 223 | if (modelObj.id === undefined) { 224 | throw ERRORS.paramsRequired('Model ID'); 225 | } 226 | let url = `${this._config.basePath}${MODELS_PATH}`; 227 | let data = {model: modelObj}; 228 | data['model']['output_info'] = { 229 | 'data': { 230 | concepts 231 | }, 232 | 'output_config': { 233 | 'concepts_mutually_exclusive': !!options.conceptsMutuallyExclusive, 234 | 'closed_environment': !!options.closedEnvironment 235 | } 236 | }; 237 | 238 | return wrapToken(this._config, (headers) => { 239 | return new Promise((resolve, reject) => { 240 | axios.post(url, data, {headers}).then((response) => { 241 | if (isSuccess(response)) { 242 | resolve(new Model(this._config, response.data.model)); 243 | } else { 244 | reject(response); 245 | } 246 | }, reject); 247 | }); 248 | }); 249 | } 250 | 251 | /** 252 | * Returns a model specified by ID 253 | * @param {String} id The model's id 254 | * @return {Promise(Model, error)} A Promise that is fulfilled with an instance of Model or rejected with an error 255 | */ 256 | get(id) { 257 | let url = `${this._config.basePath}${replaceVars(MODEL_PATH, [id])}`; 258 | return wrapToken(this._config, (headers) => { 259 | return new Promise((resolve, reject) => { 260 | axios.get(url, {headers}).then((response) => { 261 | if (isSuccess(response)) { 262 | resolve(new Model(this._config, response.data.model)); 263 | } else { 264 | reject(response); 265 | } 266 | }, reject); 267 | }); 268 | }); 269 | } 270 | 271 | /** 272 | * Update a model's or a list of models' output config or concepts 273 | * @param {object|object[]} models Can be a single model object or list of model objects with the following attrs: 274 | * @param {string} models.id The id of the model to apply changes to (Required) 275 | * @param {string} models.name The new name of the model to update with 276 | * @param {boolean} models.conceptsMutuallyExclusive Do you expect to see more than one of the concepts in this model in the SAME image? Set to false (default) if so. Otherwise, set to true. 277 | * @param {boolean} models.closedEnvironment Do you expect to run the trained model on images that do not contain ANY of the concepts in the model? Set to false (default) if so. Otherwise, set to true. 278 | * @param {object[]} models.concepts An array of concept objects or string 279 | * @param {object|string} models.concepts[].concept If string is given, this is interpreted as concept id. Otherwise, if object is given, client expects the following attributes 280 | * @param {string} models.concepts[].concept.id The id of the concept to attach to the model 281 | * @param {object[]} models.action The action to perform on the given concepts. Possible values are 'merge', 'remove', or 'overwrite'. Default: 'merge' 282 | * @return {Promise(Models, error)} A Promise that is fulfilled with an instance of Models or rejected with an error 283 | */ 284 | update(models) { 285 | let url = `${this._config.basePath}${MODELS_PATH}`; 286 | let modelsList = Array.isArray(models) ? models : [models]; 287 | let data = {models: modelsList.map(formatModel)}; 288 | data['action'] = models.action || 'merge'; 289 | return wrapToken(this._config, (headers) => { 290 | return new Promise((resolve, reject) => { 291 | axios.patch(url, data, {headers}).then((response) => { 292 | if (isSuccess(response)) { 293 | resolve(new Models(this._config, response.data.models)); 294 | } else { 295 | reject(response); 296 | } 297 | }, reject); 298 | }); 299 | }); 300 | } 301 | 302 | /** 303 | * Update model by merging concepts 304 | * @param {object|object[]} model Can be a single model object or list of model objects with the following attrs: 305 | * @param {string} model.id The id of the model to apply changes to (Required) 306 | * @param {object[]} model.concepts An array of concept objects or string 307 | * @param {object|string} model.concepts[].concept If string is given, this is interpreted as concept id. Otherwise, if object is given, client expects the following attributes 308 | * @param {string} model.concepts[].concept.id The id of the concept to attach to the model 309 | */ 310 | mergeConcepts(model = {}) { 311 | model.action = 'merge'; 312 | return this.update(model); 313 | } 314 | 315 | /** 316 | * Update model by removing concepts 317 | * @param {object|object[]} model Can be a single model object or list of model objects with the following attrs: 318 | * @param {string} model.id The id of the model to apply changes to (Required) 319 | * @param {object[]} model.concepts An array of concept objects or string 320 | * @param {object|string} model.concepts[].concept If string is given, this is interpreted as concept id. Otherwise, if object is given, client expects the following attributes 321 | * @param {string} model.concepts[].concept.id The id of the concept to attach to the model 322 | */ 323 | deleteConcepts(model = {}) { 324 | model.action = 'remove'; 325 | return this.update(model); 326 | } 327 | 328 | /** 329 | * Update model by overwriting concepts 330 | * @param {object|object[]} model Can be a single model object or list of model objects with the following attrs: 331 | * @param {string} model.id The id of the model to apply changes to (Required) 332 | * @param {object[]} model.concepts An array of concept objects or string 333 | * @param {object|string} model.concepts[].concept If string is given, this is interpreted as concept id. Otherwise, if object is given, client expects the following attributes 334 | * @param {string} model.concepts[].concept.id The id of the concept to attach to the model 335 | */ 336 | overwriteConcepts(model = {}) { 337 | model.action = 'overwrite'; 338 | return this.update(model); 339 | } 340 | 341 | /** 342 | * Deletes all models (if no ids and versionId given) or a model (if given id) or a model version (if given id and verion id) 343 | * @param {String|String[]} ids Can be a single string or an array of strings representing the model ids 344 | * @param {String} versionId The model's version id 345 | * @return {Promise(response, error)} A Promise that is fulfilled with the API response or rejected with an error 346 | */ 347 | delete(ids, versionId = null) { 348 | let request, url, data; 349 | let id = ids; 350 | 351 | if (checkType(/String/, ids) || (checkType(/Array/, ids) && ids.length === 1 )) { 352 | if (versionId) { 353 | url = `${this._config.basePath}${replaceVars(MODEL_VERSION_PATH, [id, versionId])}`; 354 | } else { 355 | url = `${this._config.basePath}${replaceVars(MODEL_PATH, [id])}`; 356 | } 357 | request = wrapToken(this._config, (headers) => { 358 | return new Promise((resolve, reject) => { 359 | axios.delete(url, {headers}).then((response) => { 360 | let data = clone(response.data); 361 | data.rawData = clone(response.data); 362 | resolve(data); 363 | }, reject); 364 | }); 365 | }); 366 | } else { 367 | if (!ids && !versionId) { 368 | url = `${this._config.basePath}${MODELS_PATH}`; 369 | data = {'delete_all': true}; 370 | } else if (!versionId && ids.length > 1) { 371 | url = `${this._config.basePath}${MODELS_PATH}`; 372 | data = {ids}; 373 | } else { 374 | throw ERRORS.INVALID_DELETE_ARGS; 375 | } 376 | request = wrapToken(this._config, (headers) => { 377 | return new Promise((resolve, reject) => { 378 | axios({ 379 | method: 'delete', 380 | url, 381 | data, 382 | headers 383 | }).then((response) => { 384 | let data = clone(response.data); 385 | data.rawData = clone(response.data); 386 | resolve(data); 387 | }, reject); 388 | }); 389 | }); 390 | } 391 | 392 | return request; 393 | } 394 | 395 | /** 396 | * Search for models by name or type 397 | * @param {String} name The model name 398 | * @param {String} type This can be "concept", "color", "embed", "detect", "cluster", etc. 399 | * @return {Promise(models, error)} A Promise that is fulfilled with an instance of Models or rejected with an error 400 | */ 401 | search(name, type = null) { 402 | let url = `${this._config.basePath}${MODEL_SEARCH_PATH}`; 403 | return wrapToken(this._config, (headers) => { 404 | let params = { 405 | 'model_query': { 406 | name, 407 | type 408 | } 409 | }; 410 | return new Promise((resolve, reject) => { 411 | axios.post(url, params, {headers}).then((response) => { 412 | if (isSuccess(response)) { 413 | resolve(new Models(this._config, response.data.models)); 414 | } else { 415 | reject(response); 416 | } 417 | }, reject); 418 | }); 419 | }); 420 | } 421 | } 422 | 423 | module.exports = Models; 424 | --------------------------------------------------------------------------------