react-native-nsfw-detector 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +8 -0
- package/.release-it.json +6 -0
- package/LICENSE +42 -0
- package/README.md +133 -0
- package/build/ReactNativeNsfwDetector.types.d.ts +2 -0
- package/build/ReactNativeNsfwDetector.types.d.ts.map +1 -0
- package/build/ReactNativeNsfwDetector.types.js +3 -0
- package/build/ReactNativeNsfwDetector.types.js.map +1 -0
- package/build/ReactNativeNsfwDetectorModule.d.ts +7 -0
- package/build/ReactNativeNsfwDetectorModule.d.ts.map +1 -0
- package/build/ReactNativeNsfwDetectorModule.js +3 -0
- package/build/ReactNativeNsfwDetectorModule.js.map +1 -0
- package/build/ReactNativeNsfwDetectorModule.web.d.ts +6 -0
- package/build/ReactNativeNsfwDetectorModule.web.d.ts.map +1 -0
- package/build/ReactNativeNsfwDetectorModule.web.js +6 -0
- package/build/ReactNativeNsfwDetectorModule.web.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +8 -0
- package/build/index.js.map +1 -0
- package/eslint.config.cjs +5 -0
- package/expo-module.config.json +6 -0
- package/ios/NSFW.mlmodel +0 -0
- package/ios/NSFW.mlmodelc/analytics/coremldata.bin +0 -0
- package/ios/NSFW.mlmodelc/coremldata.bin +0 -0
- package/ios/NSFW.mlmodelc/metadata.json +78 -0
- package/ios/NSFW.mlmodelc/model0/coremldata.bin +0 -0
- package/ios/NSFW.mlmodelc/model1/coremldata.bin +0 -0
- package/ios/NSFWDetector.swift +115 -0
- package/ios/ReactNativeNsfwDetector.podspec +31 -0
- package/ios/ReactNativeNsfwDetectorModule.swift +68 -0
- package/package.json +60 -0
- package/src/ReactNativeNsfwDetector.types.ts +1 -0
- package/src/ReactNativeNsfwDetectorModule.ts +7 -0
- package/src/ReactNativeNsfwDetectorModule.web.ts +6 -0
- package/src/index.ts +9 -0
- package/tsconfig.json +28 -0
package/.prettierrc
ADDED
package/.release-it.json
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
THIRD PARTY SOFTWARE NOTICES AND INFORMATION
|
|
24
|
+
|
|
25
|
+
This project uses third party software components listed below.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
1. LOVOO GmbH Library (example component name)
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2018, LOVOO GmbH
|
|
32
|
+
All rights reserved.
|
|
33
|
+
|
|
34
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
35
|
+
|
|
36
|
+
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
37
|
+
|
|
38
|
+
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and or other materials provided with the distribution.
|
|
39
|
+
|
|
40
|
+
Neither the name of LOVOO GmbH nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
41
|
+
|
|
42
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://github.com/user-attachments/assets/7d1df9b9-bafb-4e0e-a9cd-cecd499484a6" alt="example" height="150"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h3 align="center">
|
|
6
|
+
React Native NSFW Detector
|
|
7
|
+
</h3>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
A fast on device AI image safety detector for React Native / Expo using a CoreML model<br/>
|
|
11
|
+
to detect nudity and unsafe visual content in images.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://www.npmjs.com/package/react-native-nsfw-detector">
|
|
16
|
+
<img alt="npm version" src="https://badge.fury.io/js/react-native-nsfw-detector.svg"/>
|
|
17
|
+
</a>
|
|
18
|
+
<a title="License" href="https://github.com/your-repo/react-native-nsfw-detector/blob/master/LICENSE">
|
|
19
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" />
|
|
20
|
+
</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<a href="https://x.com/icookandcode" target="_blank">
|
|
25
|
+
Need help building your React Native app? Connect with Adrian on X
|
|
26
|
+
</a>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- React Native 0.70+
|
|
32
|
+
- iOS 13+
|
|
33
|
+
- CoreML enabled environment
|
|
34
|
+
- **Xcode 10+** Because the model was trained with CreateML, you need Xcode 10 and above to compile the project
|
|
35
|
+
- Physical iOS device strongly recommended for accurate inference (Simulator results are unreliable)
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- On device inference using CoreML
|
|
40
|
+
- Detects NSFW and safe for work content
|
|
41
|
+
- Fast and lightweight
|
|
42
|
+
- No network calls required
|
|
43
|
+
- Works with React Native and Expo native modules
|
|
44
|
+
- Simple promise based API
|
|
45
|
+
|
|
46
|
+
> ⚠️ Important: Running on the iOS Simulator results in significantly reduced accuracy. For reliable results, always test on a physical device.
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
Using npm
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install react-native-nsfw-detector
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Using yarn
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
yarn add react-native-nsfw-detector
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
```jsx
|
|
65
|
+
import { check } from 'react-native-nsfw-detector';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* IMPORTANT:
|
|
69
|
+
* Simulator runs will produce significantly reduced accuracy.
|
|
70
|
+
* Always test on a physical iOS device for reliable results.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
async function checkImage(imageUri: string) {
|
|
74
|
+
const confidence = await check(imageUri);
|
|
75
|
+
|
|
76
|
+
if(confidence > 0.9) {
|
|
77
|
+
console.log("Not safe for work 🤦♂️🤦♀️")
|
|
78
|
+
} else {
|
|
79
|
+
console.log("Safe! ✅")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
See the full working example Expo app here:
|
|
85
|
+
|
|
86
|
+
- [/example](/example/) directory in this repo
|
|
87
|
+
- Run it locally to test real CoreML inference on device
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
[MIT](LICENSE)
|
|
92
|
+
|
|
93
|
+
## Author
|
|
94
|
+
|
|
95
|
+
Feel free to ask me questions on Twitter [@icookandcode](https://www.twitter.com/icookandcode)!
|
|
96
|
+
|
|
97
|
+
## Credits
|
|
98
|
+
|
|
99
|
+
Work is based on the amazing work of
|
|
100
|
+
[NSFWDetector](https://github.com/lovoo/NSFWDetector/tree/master) by the LOVOO
|
|
101
|
+
org. See copyright notices in [LICENSE](LICENSE).
|
|
102
|
+
|
|
103
|
+
Built with [create expo module](https://docs.expo.dev/modules/get-started/).
|
|
104
|
+
|
|
105
|
+
## Contributors
|
|
106
|
+
|
|
107
|
+
Submit a PR to contribute :)
|
|
108
|
+
|
|
109
|
+
## Release
|
|
110
|
+
|
|
111
|
+
We use `release-it`, to release do the following:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
yarn run release:dry
|
|
115
|
+
yarn run release
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
<div align="center">
|
|
121
|
+
|
|
122
|
+
**Ready to build a React Native app or CoreML model?**
|
|
123
|
+
|
|
124
|
+
⭐ **Star this repo** • 💬 **[Contact Adrian to Build It](https://x.com/icookandcode)**
|
|
125
|
+
|
|
126
|
+
_Built with ❤️ by [Adrian](https://x.com/icookandcode)_
|
|
127
|
+
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
**Keywords:** react-native, react, CoreML, AI, nsfw, inference, typescript,
|
|
133
|
+
react-native-nsfw-detector, swift
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativeNsfwDetector.types.d.ts","sourceRoot":"","sources":["../src/ReactNativeNsfwDetector.types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativeNsfwDetector.types.js","sourceRoot":"","sources":["../src/ReactNativeNsfwDetector.types.ts"],"names":[],"mappings":";AAAA,0CAA0C","sourcesContent":["// Define your exported module types here.\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { NativeModule } from 'expo';
|
|
2
|
+
declare class ReactNativeNsfwDetectorModule extends NativeModule<{}> {
|
|
3
|
+
check(imageUri: string): Promise<number>;
|
|
4
|
+
}
|
|
5
|
+
declare const _default: ReactNativeNsfwDetectorModule;
|
|
6
|
+
export default _default;
|
|
7
|
+
//# sourceMappingURL=ReactNativeNsfwDetectorModule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativeNsfwDetectorModule.d.ts","sourceRoot":"","sources":["../src/ReactNativeNsfwDetectorModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,OAAO,6BAA8B,SAAQ,YAAY,CAAC,EAAE,CAAC;IAClE,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CACzC;;AAED,wBAA6F"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativeNsfwDetectorModule.js","sourceRoot":"","sources":["../src/ReactNativeNsfwDetectorModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAMzD,eAAe,mBAAmB,CAAgC,yBAAyB,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\ndeclare class ReactNativeNsfwDetectorModule extends NativeModule<{}> {\n check(imageUri: string): Promise<number>;\n}\n\nexport default requireNativeModule<ReactNativeNsfwDetectorModule>('ReactNativeNsfwDetector');\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { NativeModule } from 'expo';
|
|
2
|
+
declare class ReactNativeNsfwDetectorModule extends NativeModule<{}> {
|
|
3
|
+
}
|
|
4
|
+
declare const _default: typeof ReactNativeNsfwDetectorModule;
|
|
5
|
+
export default _default;
|
|
6
|
+
//# sourceMappingURL=ReactNativeNsfwDetectorModule.web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativeNsfwDetectorModule.web.d.ts","sourceRoot":"","sources":["../src/ReactNativeNsfwDetectorModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,YAAY,EAAE,MAAM,MAAM,CAAC;AAGvD,cAAM,6BAA8B,SAAQ,YAAY,CAAC,EAAE,CAAC;CAAG;;AAE/D,wBAAiG"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { registerWebModule, NativeModule } from 'expo';
|
|
2
|
+
// ReactNativeNsfwDetectorModule is not available on the web platform.
|
|
3
|
+
class ReactNativeNsfwDetectorModule extends NativeModule {
|
|
4
|
+
}
|
|
5
|
+
export default registerWebModule(ReactNativeNsfwDetectorModule, 'ReactNativeNsfwDetectorModule');
|
|
6
|
+
//# sourceMappingURL=ReactNativeNsfwDetectorModule.web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativeNsfwDetectorModule.web.js","sourceRoot":"","sources":["../src/ReactNativeNsfwDetectorModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEvD,sEAAsE;AACtE,MAAM,6BAA8B,SAAQ,YAAgB;CAAG;AAE/D,eAAe,iBAAiB,CAAC,6BAA6B,EAAE,+BAA+B,CAAC,CAAC","sourcesContent":["import { registerWebModule, NativeModule } from 'expo';\n\n// ReactNativeNsfwDetectorModule is not available on the web platform.\nclass ReactNativeNsfwDetectorModule extends NativeModule<{}> {}\n\nexport default registerWebModule(ReactNativeNsfwDetectorModule, 'ReactNativeNsfwDetectorModule');\n"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,wBAAgB,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEvD;AAED,cAAc,iCAAiC,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Reexport the native module. On web, it will be resolved to ReactNativeNsfwDetectorModule.web.ts
|
|
2
|
+
// and on native platforms to ReactNativeNsfwDetectorModule.ts
|
|
3
|
+
import ReactNativeNsfwDetectorModule from './ReactNativeNsfwDetectorModule';
|
|
4
|
+
export function check(imageUri) {
|
|
5
|
+
return ReactNativeNsfwDetectorModule.check(imageUri);
|
|
6
|
+
}
|
|
7
|
+
export * from './ReactNativeNsfwDetector.types';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,8DAA8D;AAC9D,OAAO,6BAA6B,MAAM,iCAAiC,CAAC;AAE5E,MAAM,UAAU,KAAK,CAAC,QAAgB;IACpC,OAAO,6BAA6B,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;AACvD,CAAC;AAED,cAAc,iCAAiC,CAAC","sourcesContent":["// Reexport the native module. On web, it will be resolved to ReactNativeNsfwDetectorModule.web.ts\n// and on native platforms to ReactNativeNsfwDetectorModule.ts\nimport ReactNativeNsfwDetectorModule from './ReactNativeNsfwDetectorModule';\n\nexport function check(imageUri: string): Promise<number> {\n return ReactNativeNsfwDetectorModule.check(imageUri);\n}\n\nexport * from './ReactNativeNsfwDetector.types';\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const { defineConfig } = require('eslint/config');
|
|
2
|
+
const universe = require('eslint-config-universe/flat/native');
|
|
3
|
+
const universeWeb = require('eslint-config-universe/flat/web');
|
|
4
|
+
|
|
5
|
+
module.exports = defineConfig([{ ignores: ['build'] }, ...universe, ...universeWeb]);
|
package/ios/NSFW.mlmodel
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"shortDescription" : "NSFW is cabable of scanning images for Nudity Content.",
|
|
4
|
+
"metadataOutputVersion" : "3.0",
|
|
5
|
+
"outputSchema" : [
|
|
6
|
+
{
|
|
7
|
+
"isOptional" : "0",
|
|
8
|
+
"keyType" : "String",
|
|
9
|
+
"formattedType" : "Dictionary (String → Double)",
|
|
10
|
+
"type" : "Dictionary",
|
|
11
|
+
"name" : "classLabelProbs",
|
|
12
|
+
"shortDescription" : "Probability of each category"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"isOptional" : "0",
|
|
16
|
+
"formattedType" : "String",
|
|
17
|
+
"type" : "String",
|
|
18
|
+
"name" : "classLabel",
|
|
19
|
+
"shortDescription" : "Most likely image category"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"version" : "1.1",
|
|
23
|
+
"modelParameters" : [
|
|
24
|
+
|
|
25
|
+
],
|
|
26
|
+
"author" : "LOVOO GmbH",
|
|
27
|
+
"specificationVersion" : 1,
|
|
28
|
+
"license" : "BSD",
|
|
29
|
+
"stateSchema" : [
|
|
30
|
+
|
|
31
|
+
],
|
|
32
|
+
"isUpdatable" : "0",
|
|
33
|
+
"availability" : {
|
|
34
|
+
"macOS" : "10.13",
|
|
35
|
+
"tvOS" : "11.0",
|
|
36
|
+
"visionOS" : "1.0",
|
|
37
|
+
"watchOS" : "unavailable",
|
|
38
|
+
"iOS" : "11.0",
|
|
39
|
+
"macCatalyst" : "11.0"
|
|
40
|
+
},
|
|
41
|
+
"modelType" : {
|
|
42
|
+
"name" : "MLModelType_imageClassifier",
|
|
43
|
+
"structure" : [
|
|
44
|
+
{
|
|
45
|
+
"name" : "MLModelType_visionFeaturePrint"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name" : "MLModelType_glmClassifier"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"inputSchema" : [
|
|
53
|
+
{
|
|
54
|
+
"formattedType" : "Image (Color 299 × 299)",
|
|
55
|
+
"hasSizeFlexibility" : "1",
|
|
56
|
+
"shortDescription" : "Input image to be classified",
|
|
57
|
+
"sizeRange" : "[[299, -1], [299, -1]]",
|
|
58
|
+
"width" : "299",
|
|
59
|
+
"type" : "Image",
|
|
60
|
+
"isColor" : "1",
|
|
61
|
+
"height" : "299",
|
|
62
|
+
"sizeFlexibility" : "299... × 299...",
|
|
63
|
+
"colorspace" : "BGR",
|
|
64
|
+
"name" : "image",
|
|
65
|
+
"isOptional" : "0"
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"classLabels" : [
|
|
69
|
+
"NSFW",
|
|
70
|
+
"SFW"
|
|
71
|
+
],
|
|
72
|
+
"generatedClassName" : "NSFW",
|
|
73
|
+
"userDefinedMetadata" : {
|
|
74
|
+
|
|
75
|
+
},
|
|
76
|
+
"method" : "predict"
|
|
77
|
+
}
|
|
78
|
+
]
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreML
|
|
3
|
+
import Vision
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
@available(iOS 12.0, *)
|
|
7
|
+
public class NSFWDetector {
|
|
8
|
+
|
|
9
|
+
public static let shared = NSFWDetector()
|
|
10
|
+
|
|
11
|
+
private let model: VNCoreMLModel
|
|
12
|
+
|
|
13
|
+
public init() {
|
|
14
|
+
do {
|
|
15
|
+
let bundle = Bundle(for: Self.self)
|
|
16
|
+
|
|
17
|
+
guard let modelURL = bundle.url(forResource: "NSFW", withExtension: "mlmodelc") else {
|
|
18
|
+
fatalError("NSFW.mlmodelc not found in bundle")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let mlModel = try MLModel(contentsOf: modelURL)
|
|
22
|
+
self.model = try VNCoreMLModel(for: mlModel)
|
|
23
|
+
|
|
24
|
+
} catch {
|
|
25
|
+
fatalError("Failed to load NSFW model: \(error)")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// MARK: - Result type
|
|
30
|
+
|
|
31
|
+
public enum DetectionResult {
|
|
32
|
+
case error(Error)
|
|
33
|
+
case success(nsfwConfidence: Float)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// MARK: - Public API
|
|
37
|
+
|
|
38
|
+
public func check(image: UIImage, completion: @escaping (DetectionResult) -> Void) {
|
|
39
|
+
|
|
40
|
+
guard let cgImage = image.cgImage else {
|
|
41
|
+
completion(.error(NSError(
|
|
42
|
+
domain: "NSFWDetector",
|
|
43
|
+
code: 0,
|
|
44
|
+
userInfo: [NSLocalizedDescriptionKey: "Missing CGImage"]
|
|
45
|
+
)))
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
|
50
|
+
run(handler: handler, completion: completion)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public func check(cvPixelbuffer: CVPixelBuffer, completion: @escaping (DetectionResult) -> Void) {
|
|
54
|
+
|
|
55
|
+
let handler = VNImageRequestHandler(cvPixelBuffer: cvPixelbuffer, options: [:])
|
|
56
|
+
run(handler: handler, completion: completion)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// MARK: - Core execution
|
|
60
|
+
|
|
61
|
+
private func run(handler: VNImageRequestHandler,
|
|
62
|
+
completion: @escaping (DetectionResult) -> Void) {
|
|
63
|
+
|
|
64
|
+
let request = VNCoreMLRequest(model: self.model) { request, error in
|
|
65
|
+
|
|
66
|
+
if let error {
|
|
67
|
+
completion(.error(error))
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
guard let results = request.results as? [VNClassificationObservation] else {
|
|
72
|
+
completion(.error(NSError(
|
|
73
|
+
domain: "NSFWDetector",
|
|
74
|
+
code: 0,
|
|
75
|
+
userInfo: [NSLocalizedDescriptionKey: "No classification results"]
|
|
76
|
+
)))
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Debug logs (keep for now)
|
|
81
|
+
print("------ NSFW DETECTION ------")
|
|
82
|
+
for r in results {
|
|
83
|
+
print("🔎 \(r.identifier): \(r.confidence)")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
guard let nsfw = results.first(where: { $0.identifier == "NSFW" }),
|
|
87
|
+
let sfw = results.first(where: { $0.identifier == "SFW" }) else {
|
|
88
|
+
completion(.error(NSError(
|
|
89
|
+
domain: "NSFWDetector",
|
|
90
|
+
code: 0,
|
|
91
|
+
userInfo: [NSLocalizedDescriptionKey: "Missing SFW/NSFW labels"]
|
|
92
|
+
)))
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
print("⭐ SFW:", sfw.confidence)
|
|
97
|
+
print("⭐ NSFW:", nsfw.confidence)
|
|
98
|
+
|
|
99
|
+
// Return NSFW score (primary signal)
|
|
100
|
+
completion(.success(nsfwConfidence: nsfw.confidence))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
request.imageCropAndScaleOption = .centerCrop
|
|
104
|
+
|
|
105
|
+
#if targetEnvironment(simulator)
|
|
106
|
+
request.usesCPUOnly = true
|
|
107
|
+
#endif
|
|
108
|
+
|
|
109
|
+
do {
|
|
110
|
+
try handler.perform([request])
|
|
111
|
+
} catch {
|
|
112
|
+
completion(.error(error))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Pod::Spec.new do |s|
|
|
2
|
+
s.name = 'ReactNativeNsfwDetector'
|
|
3
|
+
s.version = '1.0.0'
|
|
4
|
+
s.summary = 'A sample project summary'
|
|
5
|
+
s.description = 'A sample project description'
|
|
6
|
+
s.author = ''
|
|
7
|
+
s.homepage = 'https://docs.expo.dev/modules/'
|
|
8
|
+
s.platforms = {
|
|
9
|
+
:ios => '16.4',
|
|
10
|
+
:tvos => '16.4'
|
|
11
|
+
}
|
|
12
|
+
s.source = { git: '' }
|
|
13
|
+
s.static_framework = true
|
|
14
|
+
|
|
15
|
+
s.dependency 'ExpoModulesCore'
|
|
16
|
+
|
|
17
|
+
# s.prepare_command = <<-CMD
|
|
18
|
+
# if [ ! -f NSFW.mlmodel ]; then
|
|
19
|
+
# curl -sL "https://github.com/lovoo/NSFWDetector/raw/master/NSFWDetector/Classes/NSFW.mlmodel" -o "NSFW.mlmodel"
|
|
20
|
+
# fi
|
|
21
|
+
# CMD
|
|
22
|
+
|
|
23
|
+
# Swift/Objective-C compatibility
|
|
24
|
+
s.pod_target_xcconfig = {
|
|
25
|
+
'DEFINES_MODULE' => 'YES',
|
|
26
|
+
'COREML_CODEGEN_LANGUAGE' => 'Swift',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
s.source_files = "*.{h,m,mm,swift,hpp,cpp}"
|
|
30
|
+
s.resources = ["NSFW.mlmodelc"]
|
|
31
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
public class ReactNativeNsfwDetectorModule: Module {
|
|
5
|
+
public func definition() -> ModuleDefinition {
|
|
6
|
+
Name("ReactNativeNsfwDetector")
|
|
7
|
+
|
|
8
|
+
AsyncFunction("check") { (imageUri: String, promise: Promise) in
|
|
9
|
+
guard #available(iOS 12.0, *) else {
|
|
10
|
+
promise.reject(
|
|
11
|
+
"ERR_UNAVAILABLE",
|
|
12
|
+
"NSFW detection requires iOS 12.0 or later"
|
|
13
|
+
)
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
guard let image = Self.loadImage(from: imageUri) else {
|
|
18
|
+
promise.reject(
|
|
19
|
+
"ERR_IMAGE_LOAD",
|
|
20
|
+
"Could not load or decode image from URI: \(imageUri)"
|
|
21
|
+
)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
guard let cgImage = image.cgImage else {
|
|
26
|
+
promise.reject(
|
|
27
|
+
"ERR_IMAGE_INVALID",
|
|
28
|
+
"Image could not be converted to CGImage (Vision requires CGImage-backed images)"
|
|
29
|
+
)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Normalize orientation for Vision consistency
|
|
34
|
+
let normalizedImage = UIImage(cgImage: cgImage, scale: image.scale, orientation: .up)
|
|
35
|
+
|
|
36
|
+
NSFWDetector.shared.check(image: normalizedImage) { result in
|
|
37
|
+
switch result {
|
|
38
|
+
case .success(let nsfwConfidence):
|
|
39
|
+
promise.resolve(Double(nsfwConfidence))
|
|
40
|
+
|
|
41
|
+
case .error(let error):
|
|
42
|
+
promise.reject("ERR_DETECTION", error.localizedDescription)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private static func loadImage(from uri: String) -> UIImage? {
|
|
49
|
+
|
|
50
|
+
// Handle file:// URIs properly
|
|
51
|
+
if let url = URL(string: uri), url.isFileURL {
|
|
52
|
+
guard FileManager.default.fileExists(atPath: url.path),
|
|
53
|
+
let data = try? Data(contentsOf: url),
|
|
54
|
+
let image = UIImage(data: data) else {
|
|
55
|
+
return nil
|
|
56
|
+
}
|
|
57
|
+
return image
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fallback for raw paths
|
|
61
|
+
guard let data = try? Data(contentsOf: URL(fileURLWithPath: uri)),
|
|
62
|
+
let image = UIImage(data: data) else {
|
|
63
|
+
return nil
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return image
|
|
67
|
+
}
|
|
68
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-nsfw-detector",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "React Native module for detecting NSFW images using an on device AI model.",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "node internal/module_scripts/build.js",
|
|
9
|
+
"clean": "node internal/module_scripts/clean.js",
|
|
10
|
+
"lint": "eslint src/",
|
|
11
|
+
"test": "node internal/module_scripts/test.js",
|
|
12
|
+
"prepare": "node internal/module_scripts/prepare.js",
|
|
13
|
+
"open:ios": "node internal/module_scripts/open-ios.js",
|
|
14
|
+
"open:android": "node internal/module_scripts/open-android.js",
|
|
15
|
+
"release": "release-it",
|
|
16
|
+
"release:dry": "release-it --dry-run"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react-native",
|
|
20
|
+
"expo",
|
|
21
|
+
"react-native-nsfw-detector",
|
|
22
|
+
"ReactNativeNsfwDetector",
|
|
23
|
+
"CoreML",
|
|
24
|
+
"Swift",
|
|
25
|
+
"NSFW"
|
|
26
|
+
],
|
|
27
|
+
"repository": "https://github.com/watadarkstar/react-native-nsfw-detector",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/watadarkstar/react-native-nsfw-detector/issues"
|
|
30
|
+
},
|
|
31
|
+
"author": "Adrian Carolli <adrian.caarolli@gmail.com> (https://github.com/watadarkstar)",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"homepage": "https://github.com/watadarkstar/react-native-nsfw-detector#readme",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@babel/core": "^7.26.0",
|
|
36
|
+
"@types/jest": "^29.2.1",
|
|
37
|
+
"@types/react": "~19.2.2",
|
|
38
|
+
"babel-preset-expo": "~56.0.14",
|
|
39
|
+
"eslint": "~9.39.4",
|
|
40
|
+
"eslint-config-universe": "^15.0.3",
|
|
41
|
+
"expo": "^56.0.11",
|
|
42
|
+
"jest": "^29.7.0",
|
|
43
|
+
"jest-expo": "~56.0.4",
|
|
44
|
+
"prettier": "^3.0.0",
|
|
45
|
+
"react-native": "0.85.3",
|
|
46
|
+
"release-it": "^20.2.0",
|
|
47
|
+
"typescript": "^5.9.2"
|
|
48
|
+
},
|
|
49
|
+
"jest": {
|
|
50
|
+
"preset": "jest-expo",
|
|
51
|
+
"roots": [
|
|
52
|
+
"<rootDir>/src"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"expo": "*",
|
|
57
|
+
"react": "*",
|
|
58
|
+
"react-native": "*"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Define your exported module types here.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { NativeModule, requireNativeModule } from 'expo';
|
|
2
|
+
|
|
3
|
+
declare class ReactNativeNsfwDetectorModule extends NativeModule<{}> {
|
|
4
|
+
check(imageUri: string): Promise<number>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default requireNativeModule<ReactNativeNsfwDetectorModule>('ReactNativeNsfwDetector');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { registerWebModule, NativeModule } from 'expo';
|
|
2
|
+
|
|
3
|
+
// ReactNativeNsfwDetectorModule is not available on the web platform.
|
|
4
|
+
class ReactNativeNsfwDetectorModule extends NativeModule<{}> {}
|
|
5
|
+
|
|
6
|
+
export default registerWebModule(ReactNativeNsfwDetectorModule, 'ReactNativeNsfwDetectorModule');
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Reexport the native module. On web, it will be resolved to ReactNativeNsfwDetectorModule.web.ts
|
|
2
|
+
// and on native platforms to ReactNativeNsfwDetectorModule.ts
|
|
3
|
+
import ReactNativeNsfwDetectorModule from './ReactNativeNsfwDetectorModule';
|
|
4
|
+
|
|
5
|
+
export function check(imageUri: string): Promise<number> {
|
|
6
|
+
return ReactNativeNsfwDetectorModule.check(imageUri);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export * from './ReactNativeNsfwDetector.types';
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["dom", "DOM.Iterable", "esnext"],
|
|
4
|
+
"types": ["jest"],
|
|
5
|
+
"typeRoots": ["./ts-declarations", "./node_modules/@types"],
|
|
6
|
+
"jsx": "react-native",
|
|
7
|
+
"target": "esnext",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"module": "esnext",
|
|
10
|
+
"moduleDetection": "force",
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"inlineSources": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
20
|
+
"noImplicitReturns": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": false,
|
|
23
|
+
"rootDir": "./src",
|
|
24
|
+
"outDir": "./build"
|
|
25
|
+
},
|
|
26
|
+
"include": ["./src"],
|
|
27
|
+
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
|
|
28
|
+
}
|