nsfw_detect 2.1.1
nsfw_detect: ^2.1.1 copied to clipboard
Privacy-friendly NSFW detection for Flutter apps. Analyze images, videos, picked media, photo libraries, and camera frames on-device.
2.1.1 - 2026-05-05 #
- Refresh pub.dev presentation with a clearer privacy-first README, shorter quickstarts, use cases, setup guidance, FAQ, and neutral visual-asset suggestions.
- Add versioned
doc/guides for getting started, permissions, media prechecks, picker workflows, library scanning, camera scanning, configuration, models, privacy, limitations, and troubleshooting. - Expand Dartdoc coverage for the main detector APIs, scan and camera configuration, result types, sessions, controller lifecycle, and core widgets.
- Improve package metadata with repository, issue tracker, documentation, platform declarations, and search-friendly topics.
- Add GitHub issue and pull request templates for bug reports, feature requests, documentation issues, and contribution review.
- Exclude local agent/editor files from the published archive via
.pubignore. - Fix the example README package name and include the iOS example lockfile entry for
app_settings.
2.1.0 — 2026-05-05 #
v2.1.0 ships a live-camera detection pipeline alongside the existing photo-library scan, plus a reusable permissions UI widget and the missing
example/android/shell.
New — Live camera scan pipeline #
NsfwDetector.startCameraScan(CameraConfiguration?)/stopCameraScan()— single-session live detection. Returns aCameraScanSessionwhoseresultsis a broadcastStream<CameraFrameResult>. Concurrent sessions throw aStateError.CameraConfiguration—modelId,confidenceThreshold,mode(classification or detection),fps(1–30, default 2),resolution(low/medium/high),detectionConfidenceThreshold,iouThreshold,iosComputeUnits,androidDelegate. Same model surface as the photo-library API.CameraFrameResult— per-frame value type withframeTimestamp, sortedlabels, optionaldetections(NudeNet boxes), tolerantfromMapparsing.topCategory/topConfidence/isNsfwmirrorScanResult.CameraPermissionDeniedException,CameraErrorException— surfaced as stream errors onCameraScanSession.results(not as null per-frame results).
iOS native — AVCaptureSession + CoreML #
ios/Classes/camera/CameraSessionTask.swift,CameraFrameProcessor.swift,CameraConfiguration.swift,permissions/CameraPermission.swift.- AVCaptureSession running at configurable FPS, frames thrown into the existing
MLEngine.classify(pixelBuffer:)andMLDetectorEngine.detect(pixelBuffer:)paths — no duplicate inference code. Detection mode reuses NudeNet with NMS. - Camera-permission flow via
AVCaptureDevice.authorizationStatus/requestAccess(for: .video). - Lifecycle:
start,stop,restartare graceful — no buffer leaks, CVBufferPool reuse. - Events multiplexed onto the existing
nsfw_detect_ios/scan_eventsEventChannelwithtype∈ {cameraFrameResult,cameraPermissionDenied,cameraError} — no new channels.
Android native — CameraX + TFLite #
android/.../camera/CameraSessionTask.kt,CameraFrameAnalyzer.kt,FpsThrottle.kt,ImageProxyConverter.kt,CameraSessionConfig.kt,CameraPermission.kt,PluginLifecycleOwner.kt.- CameraX
ImageAnalysisat the configured FPS,ImageProxy → Bitmap → TFLitereusing the existingTFLiteEngineand detector path.ImageProxy.close()infinallyacross all five analyzer branches. DetectionAggregatorextracted fromScanSessionTask.runDetectionScanso the same body-part aggregation runs identically for library and camera frames (snapshot-compared — wire shape byte-identical).- CameraX 1.3.4 (the last 1.3.x line) keeps
minSdkVersion 21and stockcompileOptions.
Flutter — NsfwCameraView #
- New widget: native preview via
UiKitView(iOS) /AndroidView(Android) + HUD stack on top.- iOS PlatformView factory
nsfw_detect_ios/camera_previewattachesAVCaptureVideoPreviewLayer(resizeAspectFill). - Android PlatformView wraps a
PreviewView(FILL_CENTER); pulls inandroidx.camera:camera-view:1.3.4.
- iOS PlatformView factory
- New
NsfwCameraHudwidget — confidence bar + category label + NSFW badge. ReusesNsfwResultBadgevia a privateScanResultadapter (no duplicated badge code). - Detection mode draws bounding boxes via the existing
NsfwDetectionOverlay(it was alreadySize-agnostic). - Optional blur on NSFW —
BackdropFilter+ImageFilter.blurwrapped inAnimatedSwitcherso the blur fades in/out instead of strobing on borderline frames. ConfigurableblurSigmaandenableBlurOnNsfw. - Lifecycle bound to
initState/dispose. NsfwGalleryThemeextended additively with four camera-only fields:cameraBlurSigma,cameraBlurTintOpacity,cameraConfidenceBarHeight,cameraHudBackgroundOpacity. ExistingNsfwGalleryTheme(...)callers keep working unchanged.
New — NsfwPermissionsView #
- Reusable widget that surfaces every permission the plugin needs (photo library + camera) with status, an explainer, and a Request / Open Settings action button. Auto-refreshes when the app returns from system Settings (
WidgetsBindingObserver). - Plugin stays dependency-free — the "Open Settings" deep-link is delegated to the host app via the
onOpenSettingscallback. Example app pulls inapp_settings: ^5.1.1to wire it. - Wired into the example app's
SettingsScreenaboveNsfwSettingsPanel. - Themed via
NsfwThemesemantic tokens (success/accent/danger),Semantics-wrapped for screen readers. - New types in
lib/src/api/permissions/permission_kind.dart:PermissionKind(photoLibrary,camera),PermissionStatus(authorized,limited,denied,permanentlyDenied,restricted,notDetermined),PhotoLibraryPermissionStatusMappingextension.
Platform-interface additions #
NsfwPlatformInterface.startCameraScan(CameraConfiguration)/stopCameraScan()— abstract; every platform must implement. Wired inNsfwMethodChannel.NsfwPlatformInterface.checkCameraPermission()/requestCameraPermission()— non-abstract withUnimplementedErrordefaults so test mocks need only stub what they exercise.NsfwMethodChannelre-mapsMissingPluginException → UnimplementedError;NsfwDetector.checkCameraPermission()/requestCameraPermission()catch and degrade gracefully toPermissionStatus.notDeterminedon platforms without the native handler yet wired.
Fixes #
- Concurrent-task crash in
ImageAnalyzer(iOS).pixelBufferPoolwas alazy var; concurrent first-frame Tasks racing on the lazy initialisation produced a partially-written pointer, manifesting asEXC_BAD_ACCESSat+0x18insideCVPixelBufferPool::createPixelBufferon cooperative-queue threads. Eager-init ininit()instead — pool depends only oninputSize, no behaviour or perf change. scanAsset returns resulttest regression. Pre-existing failure onmainfrom before v2.1.0: the mock fixture mixedsafe=0.95withnudity=0.03, butScanResult.topCategoryhas been NSFW-priority-sorted since v2.0 (commit882ba2a). Reduced the fixture to a single unambiguous label and added an explicit test that locks in the priority-sort contract.
Example app #
example/ios/Runner/Info.plist— addsNSCameraUsageDescription. iOS killed the app onAVCaptureDevice.requestAccesswithout it.example/android/app/src/main/AndroidManifest.xml— addsandroid.permission.CAMERA+ two optional<uses-feature>entries (camera+camera.autofocuswithrequired="false") so the Play Store doesn't filter the listing on cameraless devices.example/lib/screens/settings_screen.dartembedsNsfwPermissionsView; depends onapp_settings: ^5.1.1.
Tests #
- 86 → 116 plugin unit + widget tests (
+30). Camera HUD widget tests (Phase 4),NsfwPermissionsViewtests covering all sixPermissionStatusvariants + lifecycle resume + UnimplementedError fallback, and the explicit NSFW-priority sort test.
Migration #
// New: live camera scan
final session = await NsfwDetector.instance.startCameraScan(
const CameraConfiguration(fps: 2, mode: ScanMode.detection),
);
session.results.listen((CameraFrameResult r) {
if (r.isNsfw) print('NSFW @ ${r.frameTimestamp}: ${r.topCategory}');
});
// Or drop in the widget — it manages the session itself:
NsfwCameraView(
config: const CameraConfiguration(fps: 2),
enableBlurOnNsfw: true,
showHudOverlay: true,
);
// New: permissions widget
NsfwPermissionsView(
theme: appNsfwTheme,
onOpenSettings: AppSettings.openAppSettings, // host-app dep
);
iOS callers must add to Info.plist:
<key>NSCameraUsageDescription</key>
<string>Live NSFW detection on the camera feed.</string>
Android callers must add to AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA"/>
2.0.1 #
- Doc cleanup. No code changes from 2.0.0.
2.0.0 #
Breaking changes #
PickedMedia.mediaTypeis nowMediaTypeenum (wasString).pickedMedia.mediaType == 'image'→pickedMedia.mediaType == MediaType.image. Affects callers ofNsfwDetector.instance.pickMedia(...).
Architecture — NsfwGalleryView is no longer a god-widget #
NsfwScanControllerextracts gallery state into aChangeNotifier. Permission, scan session, item list, results map, progress and lifecycle methods (startScan,stopScan,requestPermission,dispose) live on the controller. Consumers can hold their own controller and bind multiple views to it, or rely onNsfwGalleryViewto create+dispose one internally (default, BC).NsfwGalleryView.controller— new optional parameter. Whennull, the widget builds its own controller exactly like before. Otherwise the consumer owns the lifecycle.NsfwPlatformInterfaceslimmer for mocks. Optional methods (preloadModel,downloadModel,deleteModel,setModelUrl,setLogging,clearScanCache,resetScan,pickMedia,scanFilePath,scanImageBytes) ship default implementations — no-op orUnimplementedError. Test mocks now stub only the six lifecycle methods that actually matter.
New API #
NsfwScanController— see above. Exported frompackage:nsfw_detect/nsfw_detect.dart.MediaPickerTypelives inlib/src/api/media_picker_type.dart(was inlined). Re-exported.
Internals & tests #
- Test count
78 → 87(+9): controller lifecycle, controller-vs-view interaction,PickedMedia.mediaTypeenum parsing,MediaPickerTypestandalone import. NsfwGalleryViewshrunk from ~520 lines (logic + view) to a thin scaffold. Body is grid/skeleton/empty/permission render — driven by the controller.
Migration #
// PickedMedia.mediaType
- if (pm.mediaType == 'image') { ... }
+ if (pm.mediaType == MediaType.image) { ... }
// Optional: hold your own controller
final controller = NsfwScanController(initialConfig: ScanConfiguration());
NsfwGalleryView(controller: controller, ...);
// remember to controller.dispose() in your State.dispose()
1.3.0 #
Object detection — body-part bounding boxes #
- NudeNet detector (iOS + Android). New detection mode runs a YOLOv8-based 18-class body-part detector and emits bounding boxes per asset (e.g.
FEMALE_BREAST_EXPOSED,FEMALE_GENITALIA_COVERED, …). Hosted as a downloadable model —~46 MB.mlmodelc.zip(CoreML, with NMS pipeline producingVNRecognizedObjectObservation) /.tflite.zip(TFLite raw YOLO; Kotlin runs class-aware NMS at IoU 0.45).inputSize640 for accuracy. MLDetectorEngineprotocol (iOS + Android). First-class peer toMLEngineso the registry can hold both classifier and detector kinds.kind(for: id)distinguishes the two;detectorEngine(for:)/engine(for:)route to the correct factory.- Detection-mode scan pipeline. When
ScanConfiguration.mode == "detection"(or the chosenmodelIdis registered as a detector),ScanSessionTaskroutes pixel buffers throughdetect(...)and aggregates per-category max confidence intoresult.labelsso the existingtopCategory/isNsfwsemantics keep working — extra raw boxes are stashed onresult.detections. - NSFW priority sort. Detection aggregation now sorts categories by NSFW priority (
explicitNudity → nudity → suggestive → safe → unknown) before confidence. A high-confidenceFACE_FEMALEno longer outranks a moderate-confidenceFEMALE_BREAST_EXPOSED— any*_EXPOSEDhit at or above the detection threshold flips the result to NSFW. NsfwDetectionOverlaywidget. Drop-in overlay that renders detection boxes + labels + confidence on top of any image tile. Categories colour-coded via the existing theme tokens.ScanCacheschema v2. Newdetections_jsoncolumn persists raw bounding boxes per cached entry; cache hits replay both labels and detections. Migrations are wrapped in transactions; downgrade drops + recreates.
Models #
models-v1GitHub Release consolidates all on-device artefacts as a single source of truth (Falconsai, AdamCodd, OpenNSFW2, NudeNetDetector). Defaults in both registries point there.- Falconsai + AdamCodd Android. TFLite parity. ViT normalisation
(2x − 1)and softmax baked into the graph so Kotlin passes raw[0, 1]floats and reads[0, 1]probabilities directly — symmetric to iOS viaclassifier_config. INT8 weight-only post-quantisation gets the artefacts to ~75 MB without measurable accuracy loss. - Reproducible conversion. New
tools/convert_models.py(CoreML),tools/convert_tflite.py(TFLite, runs in a separate venv to avoid the LLVM CLI clash betweentensorflowandjax), andtools/convert_nudenet.py(ultralytics YOLO export → both formats). Each script is idempotent and gates download-then-convert from the upstream HuggingFace / GitHub source.
Fixes #
PHPhotosError 3303fallback (iOS). WhenPHCachingImageManagerrefuses an asset (limited library, iCloud-only without network, cache edge cases), the analyzer retries viaPHImageManager.default()+requestImageDataAndOrientation. Recovers most assets that previously surfaced as opaque "pixelBuffer unavailable".- Detector preload (iOS).
preloadModeland the implicit preload insidestartScanboth went through the classifier-onlyengine(for:)factory map and 404'd every detector model. Routes now branch onkind(for: id). - Softmax for ViT classifiers (iOS). Falconsai / AdamCodd CoreML models emit raw logits via
classifier_config. Vision passes those through asconfidence, producing values likenudity=357%, safe=−240%.CoreMLEnginenow applies a numerically-stable softmax client-side, so confidences land back in[0, 1]and the gallery's default0..1filter shows the NSFW hits. - Surfaced video skip reasons. "Skipped" status now carries the underlying cause (zero-duration AVAsset, empty sample times, etc.) in
result.errorMessageinstead of disappearing silently. - Surfaced pixel-buffer errors. Error from the per-asset image fetch is captured per-asset and propagated to
result.errorMessageinstead of being swallowed bytry?.
Performance — large library scans #
- Incremental scans (iOS + Android). Per-asset cache keyed by
(localId, modelId, modificationDate)persisted in SQLite. A second sync of an unchanged 200k-asset library skips the ML pipeline entirely — sub-second filter pass instead of minutes of inference. - Cached results are replayed by default.
Stream<ScanResult>stays complete; cached items are flagged via the newScanResult.fromCache: boolso consumers can distinguish them. - Event-sink throttling (iOS). Per-asset
resultevents are coalesced into batchedresultschannel messages (50 items / 100 ms), andprogressevents are throttled to ≤ 1 per 100 ms. Reduces IPC roundtrips by ~50–100× for large libraries. - Sliding-window prefetch (iOS).
PHCachingImageManagernow releases older prefetch windows viastopCachingImages(for:), keeping memory bounded regardless of library size (previously linear). - Real cache hits in the
ImageAnalyzerpipeline (iOS). Prefetched assets are now read through the samePHCachingImageManagerwith identical options — the prefetch was previously a no-op. - Throttled checkpoint writes (iOS). Per-asset
UserDefaultswrites are coalesced (every 25 assets + at scan boundaries). Cuts disk I/O in the hot path proportionally. - Batched scan-cache writes (iOS + Android).
ScanCache.record()now buffers in memory and flushes in groups of 50 inside a singleBEGIN…COMMITtransaction (iOS uses one prepared statement reused viabind/step/reset; Android usesSQLiteStatement+beginTransaction). 200k assets go from ~200k mini-transactions to ~4k. Reads callflush()first to keep lookups consistent. inSampleSizedownsampling on bitmap decode (Android).BitmapFactory.decodeStream(...)previously did a full-resolution decode — a 12 MP photo allocated ~50 MB before being scaled to 224×224. Now a two-pass decode (inJustDecodeBounds+ power-of-twoinSampleSize) drops peak per-frame allocation by ~250×. Pendant to the iOSImageIOthumbnail path.- Pooled
CVPixelBuffer+ cachedCGColorSpace(iOS).ImageAnalyzernow renders frames into buffers from aCVPixelBufferPoolinstead ofCVPixelBufferCreateper frame, and the device RGB color space is allocated once statically. ~400k allocations eliminated on a 200k-asset scan.
Performance — inference acceleration #
-
Configurable Core ML compute units (iOS). New
ScanConfiguration.iosComputeUnitsexposesMLModelConfiguration.computeUnits. Defaults to.all;cpuAndNeuralEngineorcpuOnlycan be faster on older devices without a dedicated ANE (no GPU roundtrip). -
TFLite GPU / NNAPI delegate (Android). New
ScanConfiguration.androidDelegateenables thelitert-gpuor NNAPI delegate. Opt-in (default CPU) because GPU/NNAPI delegates are flaky on some device families. Falls back to CPU silently if the delegate cannot be loaded. -
Reused
VNCoreMLRequest(iOS).CoreMLEngine.classify(pixelBuffer:)no longer allocates a Vision request per call. One request is built atload()time and reused under a serializing lock — only relevant when the batch path falls back, but cleaner and cheaper. -
OSAllocatedUnfairLockfor the scan counter (iOS 16+). Hot-path counter swapsNSLockforOSAllocatedUnfairLock<Int>. 5–10× cheaper under contention.
New API #
ScanConfiguration.skipAlreadyScanned: bool(defaulttrue) — skip assets matching a cached fingerprint.ScanConfiguration.forceRescan: bool(defaultfalse) — bypass the cache for this run; cache is overwritten on completion.ScanConfiguration.replayCachedResults: bool(defaulttrue) — emit cached results on hit; disable for "delta only" mode.ScanConfiguration.iosComputeUnits: IosComputeUnits(default.all) — selects the Core ML compute-unit preference on iOS.ScanConfiguration.androidDelegate: AndroidDelegate?(defaultnull= CPU) — opt into the TFLitegpuornnapidelegate on Android.NsfwDetector.instance.pickMedia({type, multiple, maxItems})— opens the native picker and returns the selected items asList<PickedMedia>without classifying. Pair withscanAssetfor on-demand classification.NsfwDetector.instance.clearScanCache({String? modelId})— drop cached entries for one model or all.ScanResult.fromCache: bool—truewhen the result was replayed from the persistent cache.
Internals #
- Schema migrations for the scan cache via
PRAGMA user_version(iOS) /SQLiteOpenHelper.onUpgrade(Android), wrapped in transactions;onDowngradedrops + recreates rather than failing. - New
EventBatcherandCheckpointWriterhelpers inScanSessionTask.swift. MLEngineprotocol gainssetPreferredComputeUnits(_:)/loadedComputeUnits;ModelRegistry.engine(for:computeUnits:)recreates the cached engine if the preference changes.
Demo app #
- New "Skip Already Scanned" and "Force Rescan" toggles in the settings screen.
- New "Clear Scan Cache" maintenance action.
1.2.8 #
Fixes #
- Fixed
ModelIds.adamcoddruntime classification mapping on iOS (AdamCodd output classes now map correctly to plugin categories). - Fixed model-size handling for AdamCodd (ViT-384) in video frame sampling and file/bytes scan preprocessing.
1.2.7 #
1.2.6 #
Performance #
- Improved native runtime scheduling stability during long scans.
- Reduced unnecessary cancellation side effects in background task orchestration.
1.2.5 #
Fixes #
- Fixed example app compile issues by removing obsolete detection-only UI code from the detail screen.
- Added a visible maintenance action in example settings to trigger
NsfwDetector.resetScan().
1.2.4 #
Performance #
- iOS method-channel heavy operations now run with utility-priority tasks to avoid accidental UI-thread contention during preload, model download, scanFile, scanBytes, and scanSingleAsset.
- Android background dispatch switched from per-asset raw thread spawning to a bounded native worker pool with backpressure, reducing thread churn and frame-drop risk under large scans.
1.2.3 #
New features #
- Added
NsfwDetector.resetScan()to reset native scan runtime state. - Implemented native handling on iOS and Android (
resetScanmethod-channel call).
1.2.1 #
Maintenance #
- Updated package metadata/topics for pub.dev discovery (
nsfw-detection,nudity-detection). - Version sync/release housekeeping (
pubspec.yaml+ iOS podspec). - Minor runtime/performance cleanup: reduced logging overhead in native processing paths.
- No API changes.
1.2.0 #
New features #
NsfwDetector.pickAndScan({int maxItems})— opens the native iOSPHPickerViewController(or Android photo picker on API 33+) and scans the selected photos/videos. Returns aScanSessionthat streams results and progress exactly likestartScan. No photo library permission required — the user picks items directly.NsfwDetector.scanFile(String filePath)— classifies a single image/video from a file path. Useful for scanning files from the app sandbox, document picker, etc.NsfwDetector.scanBytes(Uint8List bytes)— classifies a single image supplied as raw bytes (Uint8List). Useful when the image is already in memory (camera capture, network download, clipboard, etc.).- Both
scanFileandscanBytesaccept an optionalmodelIdandconfidenceThresholdand return aScanResultdirectly (no session required).
1.1.5 #
Breaking changes #
- Removed
BodyPartDetection,BoundingBox,NsfwSeverity,DetectionSummary— these types were never backed by a running model and always returned empty/null results. - Removed
ScanResult.detections,hasDetections,isDetectorResult,detectionSummary.
1.1.4 #
Docs #
- Removed body part detection section from README and CHANGELOG — feature was never implemented.
1.1.3 #
Fixes #
CoreMLEngine.classifyBatch: removed invalidoverridekeyword (protocol conformance, not subclass override)PixelBufferFeatureProvider: changed fromstructtofinal class NSObject—MLFeatureProvideris an ObjC protocol requiring a classMLModel.predictions(from:): added requiredoptions: MLPredictionOptions()parameter (iOS 16+ SDK)performBackgroundGalleryScan: replaced internalModelIds.falconsaidefault with string literal (public API constraint)
1.1.2 #
Improvements #
- Auto model download in
startScan—startScannow automatically downloads and compiles the model if it is not yet on disk. CallingdownloadModel+preloadModelfrom Dart beforestartScanis no longer needed (they still work for manual preloading). All three steps run on native background threads;startScanreturns to Dart immediately. NsfwDetectIosPlugin.performBackgroundGalleryScan— new public static method for running a gallery scan from aBGProcessingTaskor similar context where no Flutter engine is present. Events are silently discarded; the completion callback is called when the scan finishes.
1.1.1 #
Fixes #
- Fixed
podspecversion (was still1.0.0) and removed placeholder author/homepage fields. - Removed placeholder
repositoryURL frompubspec.yaml.
1.1.0 #
Performance #
- CoreML batch inference — images are now submitted to the model in batches via
MLModel.predictions(from:), reducing ANE/GPU setup overhead from N times to once per batch. Typical throughput improvement: 1.5–3× faster on large photo libraries. Videos and Live Photos are unaffected; their pipeline is unchanged. - Batch size is derived from the existing
concurrencyparameter — no Dart API change needed. - Automatic fallback: if batch prediction fails twice in a row, the engine transparently reverts to the previous per-image Vision path for the remainder of the session.
- Added
ScanConfiguration.disableBatchPrediction(defaultfalse) — set totrueto force the serial Vision path, e.g. for diagnosing device-specific behaviour.
1.0.0 #
Initial release.
Core API #
NsfwDetector.instancesingleton for all plugin operationsstartScan(ScanConfiguration)→ScanSessionwith streaming results, progress, and completionScanSession.results—Stream<ScanResult>emitting results as each asset is classifiedScanSession.progress—Stream<ScanProgress>with scanned/total counts and fractionScanSession.done—Future<ScanSummary>resolving on completion or cancellationScanSession.cancel()— graceful abort at any pointscanAsset(localIdentifier)— single-asset synchronous-style scanrequestPermission()/checkPermission()— photo library permission handlingScanConfiguration— immutable config: threshold, concurrency, video settings, model selection
Classification #
- Five categories:
safe,suggestive,nudity,explicitNudity,unknown ScanResult.isNsfw,topCategory,topConfidence,confidenceFor(category)ScanResult.labels— all categories sorted by confidence
Models #
- OpenNSFW2 (CoreML, bundled, ~11 MB) — no download needed
- Falconsai NSFW and AdamCodd NSFW — downloadable via
downloadModel() availableModels(),preloadModel(),downloadModel(),deleteModel(),setModelUrl()
Widgets #
NsfwGalleryView— drop-in gallery with live scan, result badges, and custom tile/thumbnail buildersNsfwResultBadge— 4 badge styles:compact,detailed,iconOnly,minimalNsfwScanProgressBar— 3 display styles:linear,compact,textOnlyNsfwGalleryTheme— full color + style customization withcopyWithNsfwScanControls— start/stop control barNsfwMediaTile— composable tile widget for custom layouts
Platform support #
- iOS 16+ — CoreML + Vision + Apple Neural Engine acceleration
- Android API 24+ — TensorFlow Lite inference, tiered permission handling (API 21 → 33+)
Video scanning #
- Uniform temporal frame sampling
- Hard-threshold fast-exit (score > 0.9 → immediate flag)
- Center-weighted aggregation to reduce title-card false positives
- Configurable
maxVideoFramesandvideoFrameInterval