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.
Files changed (37) hide show
  1. package/.prettierrc +8 -0
  2. package/.release-it.json +6 -0
  3. package/LICENSE +42 -0
  4. package/README.md +133 -0
  5. package/build/ReactNativeNsfwDetector.types.d.ts +2 -0
  6. package/build/ReactNativeNsfwDetector.types.d.ts.map +1 -0
  7. package/build/ReactNativeNsfwDetector.types.js +3 -0
  8. package/build/ReactNativeNsfwDetector.types.js.map +1 -0
  9. package/build/ReactNativeNsfwDetectorModule.d.ts +7 -0
  10. package/build/ReactNativeNsfwDetectorModule.d.ts.map +1 -0
  11. package/build/ReactNativeNsfwDetectorModule.js +3 -0
  12. package/build/ReactNativeNsfwDetectorModule.js.map +1 -0
  13. package/build/ReactNativeNsfwDetectorModule.web.d.ts +6 -0
  14. package/build/ReactNativeNsfwDetectorModule.web.d.ts.map +1 -0
  15. package/build/ReactNativeNsfwDetectorModule.web.js +6 -0
  16. package/build/ReactNativeNsfwDetectorModule.web.js.map +1 -0
  17. package/build/index.d.ts +3 -0
  18. package/build/index.d.ts.map +1 -0
  19. package/build/index.js +8 -0
  20. package/build/index.js.map +1 -0
  21. package/eslint.config.cjs +5 -0
  22. package/expo-module.config.json +6 -0
  23. package/ios/NSFW.mlmodel +0 -0
  24. package/ios/NSFW.mlmodelc/analytics/coremldata.bin +0 -0
  25. package/ios/NSFW.mlmodelc/coremldata.bin +0 -0
  26. package/ios/NSFW.mlmodelc/metadata.json +78 -0
  27. package/ios/NSFW.mlmodelc/model0/coremldata.bin +0 -0
  28. package/ios/NSFW.mlmodelc/model1/coremldata.bin +0 -0
  29. package/ios/NSFWDetector.swift +115 -0
  30. package/ios/ReactNativeNsfwDetector.podspec +31 -0
  31. package/ios/ReactNativeNsfwDetectorModule.swift +68 -0
  32. package/package.json +60 -0
  33. package/src/ReactNativeNsfwDetector.types.ts +1 -0
  34. package/src/ReactNativeNsfwDetectorModule.ts +7 -0
  35. package/src/ReactNativeNsfwDetectorModule.web.ts +6 -0
  36. package/src/index.ts +9 -0
  37. package/tsconfig.json +28 -0
package/.prettierrc ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "printWidth": 100,
3
+ "tabWidth": 2,
4
+ "singleQuote": true,
5
+ "bracketSameLine": true,
6
+ "trailingComma": "es5",
7
+ "jsxSingleQuote": false,
8
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://unpkg.com/release-it/schema/release-it.json",
3
+ "github": {
4
+ "release": true
5
+ }
6
+ }
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ReactNativeNsfwDetector.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReactNativeNsfwDetector.types.d.ts","sourceRoot":"","sources":["../src/ReactNativeNsfwDetector.types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ export {};
2
+ // Define your exported module types here.
3
+ //# sourceMappingURL=ReactNativeNsfwDetector.types.js.map
@@ -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,3 @@
1
+ import { requireNativeModule } from 'expo';
2
+ export default requireNativeModule('ReactNativeNsfwDetector');
3
+ //# sourceMappingURL=ReactNativeNsfwDetectorModule.js.map
@@ -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"]}
@@ -0,0 +1,3 @@
1
+ export declare function check(imageUri: string): Promise<number>;
2
+ export * from './ReactNativeNsfwDetector.types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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]);
@@ -0,0 +1,6 @@
1
+ {
2
+ "platforms": ["apple"],
3
+ "apple": {
4
+ "modules": ["ReactNativeNsfwDetectorModule"]
5
+ }
6
+ }
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
+ ]
@@ -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
+ }