Skip to content

Commit

Permalink
ESM related fixes (#897 by @haZya)
Browse files Browse the repository at this point in the history
  • Loading branch information
haZya authored Oct 11, 2024
1 parent ba33553 commit 2f60c4e
Show file tree
Hide file tree
Showing 17 changed files with 519 additions and 190 deletions.
1 change: 0 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ bundle.js
.all-contributorsrc
.editorconfig
.gitignore
bundle-model.mjs
jest.config.js
tsconfig.cjs.json
tsconfig.esm.json
Expand Down
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,25 +113,24 @@ You can also use same parameter and load the model from your website/server, as
Model MobileNetV2 - [224x224](https://github.com/infinitered/nsfwjs/blob/master/models/mobilenet_v2/)

```js
const model = nsfwjs.load("/path/to/model/directory/");
const model = nsfwjs.load("/path/to/mobilenet_v2/");
```

If you're using a model that needs an image of dimension other than 224x224, you can pass the size in the options parameter.

Model MobileNetV2Mid - [299x299](https://github.com/infinitered/nsfwjs/tree/master/models/mobilenet_v2_mid)
Model MobileNetV2Mid - [Graph](https://github.com/infinitered/nsfwjs/tree/master/models/mobilenet_v2_mid)

```js
const model = nsfwjs.load("/path/to/model/", { size: 299 });
/* You may need to load this model with graph type */
const model = nsfwjs.load("/path/to/model/", { type: 'graph' });
const model = nsfwjs.load("/path/to/mobilenet_v2_mid/", { type: 'graph' });
```

If you're using a graph model, you cannot use the infer method, and you'll need to tell model load that you're dealing with a graph model in options.

Model InceptionV3 - [Graph](https://github.com/infinitered/nsfwjs/tree/master/models/inception_v3)
Model InceptionV3 - [299x299](https://github.com/infinitered/nsfwjs/tree/master/models/inception_v3)

```js
const model = nsfwjs.load("/path/to/different/model/", { type: "graph" });
const model = nsfwjs.load("/path/to/inception_v3/", { size: 299 });
```

### Caching
Expand Down Expand Up @@ -226,13 +225,13 @@ Here is how to install the default model on a website:

### Tensorflow.js in the browser

The demo that powers https://nsfwjs.com/ is available in the `examples/nsfw_demo` folder.
The demo that powers https://nsfwjs.com/ is available in the [`examples/nsfw_demo`](https://github.com/infinitered/nsfwjs/tree/master/examples/nsfw_demo) folder.

To run the demo, run `yarn prep` which will copy the latest code into the demo. After that's done, you can `cd` into the demo folder and run with `yarn start`.

### Browserify

A browserified version using nothing but promises and script tags is available in the `minimal_demo` folder.
A browserified version using nothing but promises and script tags is available in the [`minimal_demo`](https://github.com/infinitered/nsfwjs/tree/master/examples/minimal_demo) folder.

```js
<script src="/path/to/model/directory/model.min.js"></script>
Expand Down
6 changes: 3 additions & 3 deletions examples/minimal_demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
<!-- <script src="https://unpkg.com/[email protected]" type="text/javascript"></script> -->
<!-- For testing: Load from local bundle `yarn bundle` -->
<script
src="../../dist/models/mobilenet_v2_mid/model.min.js"
src="../../dist/cjs/models/mobilenet_v2_mid/model.min.js"
type="text/javascript"
></script>
<script
src="../../dist/models/mobilenet_v2_mid/group1-shard1of2.min.js"
src="../../dist/cjs/models/mobilenet_v2_mid/group1-shard1of2.min.js"
type="text/javascript"
></script>
<script
src="../../dist/models/mobilenet_v2_mid/group1-shard2of2.min.js"
src="../../dist/cjs/models/mobilenet_v2_mid/group1-shard2of2.min.js"
type="text/javascript"
></script>
<script src="../../dist/browser/nsfwjs.min.js" type="text/javascript"></script>
Expand Down
16 changes: 10 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "nsfwjs",
"version": "4.1.0",
"unpkg": "dist/nsfwjs.min.js",
"unpkg": "dist/browser/nsfwjs.min.js",
"description": "Detect NSFW content client-side",
"type": "commonjs",
"module": "dist/esm/index.js",
"main": "dist/cjs/index.js",
"exports": {
Expand All @@ -11,9 +12,9 @@
},
"scripts": {
"lint": "tslint -p . -t verbose",
"build:esm": "tsc -p ./tsconfig.esm.json",
"build:esm": "tsc -p ./tsconfig.esm.json && node ./scripts/add-extensions.mjs",
"build:cjs": "tsc -p ./tsconfig.cjs.json",
"build": "yarn build:esm && yarn build:cjs",
"build": "yarn build:esm && yarn build:cjs && node ./scripts/add-nested-package-json.mjs",
"bundle:mobilenet_v2": "node ./scripts/bundle-model.mjs mobilenet_v2 1",
"bundle:mobilenet_v2_mid": "node ./scripts/bundle-model.mjs mobilenet_v2_mid 2",
"bundle:inception_v3": "node ./scripts/bundle-model.mjs inception_v3 6",
Expand All @@ -36,9 +37,9 @@
"type": "git",
"url": "git+https://github.com/infinitered/nsfwjs.git"
},
"dependencies": {},
"peerDependencies": {
"@tensorflow/tfjs": "^4.0.0"
"@tensorflow/tfjs": "^4.0.0",
"buffer": "^6.0.3"
},
"devDependencies": {
"@tensorflow/tfjs": "^4.0.0",
Expand All @@ -47,7 +48,9 @@
"babel-core": "^6.26.3",
"babel-plugin-transform-runtime": "~6.23.0",
"browserify": "^17.0.0",
"buffer": "^6.0.3",
"doctoc": "^2.2.0",
"esbuild": "^0.24.0",
"jest": "^28.1.3",
"jpeg-js": "^0.4.4",
"np": "^10.0.0",
Expand All @@ -73,5 +76,6 @@
"bugs": {
"url": "https://github.com/infinitered/nsfwjs/issues"
},
"homepage": "https://nsfwjs.com"
"homepage": "https://nsfwjs.com",
"dependencies": {}
}
40 changes: 40 additions & 0 deletions scripts/add-extensions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { readdirSync, readFileSync, statSync, writeFileSync } from "fs";
import { join } from "path";

const directory = "./dist/esm"; // Target directory
const extension = ".js";

function addExtensions(dir) {
const files = readdirSync(dir);

files.forEach((file) => {
const fullPath = join(dir, file);

if (statSync(fullPath).isDirectory()) {
addExtensions(fullPath);
} else if (fullPath.endsWith(".js")) {
let content = readFileSync(fullPath, "utf8");

// Add .js extension to static imports (from './path')
content = content.replace(/(from\s+['"]\.\/[^'"]+)/g, (match) => {
if (!match.endsWith(extension)) {
return `${match}${extension}`;
}
return match;
});

// Add .js extension to dynamic imports (import('./path'))
content = content.replace(/(import\(['"]\.\/[^'"]+)/g, (match) => {
if (!match.endsWith(extension)) {
return `${match}${extension}`;
}
return match;
});

writeFileSync(fullPath, content);
}
});
}

// Call the function on the target directory
addExtensions(directory);
13 changes: 13 additions & 0 deletions scripts/add-nested-package-json.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { writeFileSync } from "fs";
import { join } from "path";

function addPackageJson(dir, type) {
const packageJsonContent = JSON.stringify({ type }, null, 2);
const filePath = join(dir, "package.json");
writeFileSync(filePath, packageJsonContent);
console.log(`Created package.json in ${dir}`);
}

// Add package.json for ESM and CJS builds
addPackageJson("./dist/esm", "module");
addPackageJson("./dist/cjs", "commonjs");
110 changes: 61 additions & 49 deletions scripts/bundle-model.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,74 +6,86 @@ import {
unlinkSync,
writeFileSync,
} from "fs";
import { promisify } from "util";

// The first argument is the model name, the second argument is the number of shards
const execPromise = promisify(exec);
const model = process.argv[2];
const numShards = +process.argv[3];
const filenames = ["model"];
const filenames = [
"model",
...Array.from(
{ length: numShards },
(_, i) => `group1-shard${i + 1}of${numShards}`
),
];

for (let i = 1; i <= numShards; i++) {
filenames.push(`group1-shard${i}of${numShards}`);
}
const BASE_MODEL_PATH = "./models/";
const BASE_DIST_PATH = "./dist/";
const JSON_EXT = ".json";
const MIN_JS_EXT = ".min.js";

const binToJson = (sourcePath, outputPath) => {
// Read the binary file and convert it to Base64
const binFile = readFileSync(`${sourcePath}`);
const base64String = binFile.toString("base64");

// Write the Base64 string to a .json file
writeFileSync(`${outputPath}.json`, JSON.stringify(base64String));
};

filenames.forEach((filename) => {
const sourcePath = `./models/${model}/${filename}`;
const outputDir = `./dist/models/${model}`;
const runCommand = async (command) => {
try {
const { stdout, stderr } = await execPromise(command);
if (stderr) console.error(`stderr: ${stderr}`);
if (stdout) console.log(`stdout: ${stdout}`);
} catch (error) {
console.error(`Error: ${error.message}`);
}
};

const generateUmd = async (outputPath, filename) => {
const identifier = filename.replace(/-/g, "_");
await runCommand(
`browserify ${outputPath}.json -s ${identifier} -o ${outputPath}.js`
);
await runCommand(
`terser ${outputPath}.js -c -m -o ${outputPath}${MIN_JS_EXT}`
);
unlinkSync(`${outputPath}.js`);
unlinkSync(`${outputPath}.json`);
};

const generateEsm = async (outputPath) => {
await runCommand(
`esbuild ${outputPath}.json --bundle --format=esm --minify --outfile=${outputPath}${MIN_JS_EXT} --log-level=error`
);
unlinkSync(`${outputPath}.json`);
};

const processBundle = async (filename, type) => {
const sourcePath = `${BASE_MODEL_PATH}${model}/${filename}`;
const outputDir = `${BASE_DIST_PATH}${type}/models/${model}`;
const outputPath = `${outputDir}/${filename}`;

// Create the output directory if it doesn't exist
mkdirSync(outputDir, { recursive: true });

if (filename === "model") {
// Copy the model.json file
copyFileSync(`${sourcePath}.json`, `${outputPath}.json`);
copyFileSync(`${sourcePath}${JSON_EXT}`, `${outputPath}${JSON_EXT}`);
} else {
binToJson(sourcePath, outputPath);
}

// Browserify the .json file
const identifier = filename.replace(/-/g, "_");
exec(
`browserify ${outputPath}.json -s ${identifier} -o ${outputPath}.js`,
(error, stdout, stderr) => {
if (error) {
console.log(`Error: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
console.log(`stdout: ${stdout}`);

// Minify the .js file with Terser
exec(
`terser ${outputPath}.js -c -m -o ${outputPath}.min.js`,
(error, stdout, stderr) => {
if (error) {
console.log(`Error: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
console.log(`stdout: ${stdout}`);
if (type === "cjs") {
await generateUmd(outputPath, filename);
} else if (type === "esm") {
await generateEsm(outputPath);
}
};

// Delete the original .js and .json files
unlinkSync(`${outputPath}.js`);
unlinkSync(`${outputPath}.json`);
}
);
}
const bundleAll = async () => {
await Promise.all(
filenames.map(async (filename) => {
await processBundle(filename, "cjs");
await processBundle(filename, "esm");
})
);
});
};

bundleAll().catch((err) => console.error(err));
21 changes: 3 additions & 18 deletions scripts/post-test.mjs
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
import { readdirSync, unlinkSync } from "fs";
import { join } from "path";
import { rmSync } from "fs";

const dir = "./models";
const dir = "./src/models";

// Get the names of all subfolders in the dir
const models = readdirSync(dir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);

models.forEach((model) => {
const modelDir = join(dir, model);

readdirSync(modelDir, { withFileTypes: true })
.filter((dirent) => dirent.isFile() && dirent.name.endsWith(".min.js"))
.forEach((dirent) => {
const file = join(modelDir, dirent.name);
unlinkSync(file);
});
});
rmSync(dir, { recursive: true, force: true });
8 changes: 5 additions & 3 deletions scripts/pre-test.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { copyFileSync, readdirSync } from "fs";
import { copyFileSync, mkdirSync, readdirSync } from "fs";
import { join } from "path";

const srcDir = "./dist/models";
const destDir = "./models";
const srcDir = "./dist/cjs/models";
const destDir = "./src/models";

// Get the names of all subfolders in the srcDir
const models = readdirSync(srcDir, { withFileTypes: true })
Expand All @@ -13,6 +13,8 @@ models.forEach((model) => {
const modelSrcDir = join(srcDir, model);
const modelDestDir = join(destDir, model);

mkdirSync(modelDestDir, { recursive: true });

readdirSync(modelSrcDir, { withFileTypes: true })
.filter((dirent) => dirent.isFile() && dirent.name.endsWith(".min.js"))
.forEach((dirent) => {
Expand Down
Loading

0 comments on commit 2f60c4e

Please sign in to comment.