├── 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 | 
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 | [](https://travis-ci.org/Clarifai/clarifai-javascript)
20 | [](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 |
--------------------------------------------------------------------------------