Repository: NsfwSpy/NsfwSpy.NET Branch: main Commit: adce13ada009 Files: 59 Total size: 97.6 KB Directory structure: gitextract_qx2j5oyu/ ├── .gitignore ├── LICENSE ├── NsfwSpy/ │ ├── INsfwSpy.cs │ ├── NsfwSpy.cs │ ├── NsfwSpy.csproj │ └── Resources/ │ ├── ClassificationFailedException.cs │ ├── EClassificationType.cs │ ├── ModelInput.cs │ ├── ModelOutput.cs │ ├── NsfwSpyFramesResult.cs │ ├── NsfwSpyResult.cs │ ├── NsfwSpyValue.cs │ └── VideoOptions.cs ├── NsfwSpy.App/ │ ├── Controllers/ │ │ └── NsfwSpyController.cs │ ├── NsfwSpy.App.csproj │ ├── Pages/ │ │ ├── Error.cshtml │ │ ├── Error.cshtml.cs │ │ └── _ViewImports.cshtml │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── README.md │ ├── appsettings.json │ └── client-app/ │ ├── .gitignore │ ├── package.json │ ├── public/ │ │ ├── index.html │ │ ├── manifest.json │ │ └── robots.txt │ ├── src/ │ │ ├── App.scss │ │ ├── App.tsx │ │ ├── aspnetcore-https.ts │ │ ├── aspnetcore-react.ts │ │ ├── components/ │ │ │ └── Logo/ │ │ │ ├── Logo.scss │ │ │ └── Logo.tsx │ │ ├── functions/ │ │ │ ├── client.ts │ │ │ ├── getContentType.ts │ │ │ ├── selectFiles.ts │ │ │ └── sortBy.ts │ │ ├── index.scss │ │ ├── index.tsx │ │ ├── models/ │ │ │ ├── ImageFile.ts │ │ │ ├── MediaInfo.ts │ │ │ ├── NsfwSpyFramesResult.ts │ │ │ └── NsfwSpyResult.ts │ │ ├── react-app-env.d.ts │ │ └── reportWebVitals.ts │ └── tsconfig.json ├── NsfwSpy.PerformanceTesting/ │ ├── NsfwSpy.PerformanceTesting.csproj │ ├── PerformanceResult.cs │ └── Program.cs ├── NsfwSpy.Test/ │ ├── Assets/ │ │ ├── bikini.mkv │ │ └── bikini.webm │ ├── NsfwSpy.Test.csproj │ └── UnitTests.cs ├── NsfwSpy.Train/ │ ├── ImageData.cs │ ├── NsfwSpy.Train.csproj │ ├── Program.cs │ └── tfjs-convert-command.txt ├── NsfwSpy.sln └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ /NsfwSpy.Train/Models /NsfwSpy.Train/Workspace ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 d00ML0rDz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: NsfwSpy/INsfwSpy.cs ================================================ using System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; namespace NsfwSpyNS { public interface INsfwSpy { NsfwSpyFramesResult ClassifyGif(byte[] gifImage, VideoOptions videoOptions = null); NsfwSpyFramesResult ClassifyGif(string filePath, VideoOptions videoOptions = null); NsfwSpyFramesResult ClassifyGif(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null); Task ClassifyGifAsync(string filePath, VideoOptions videoOptions = null); Task ClassifyGifAsync(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null); NsfwSpyResult ClassifyImage(byte[] imageData); NsfwSpyResult ClassifyImage(string filePath); NsfwSpyResult ClassifyImage(Uri uri, WebClient webClient = null); Task ClassifyImageAsync(string filePath); Task ClassifyImageAsync(Uri uri, WebClient webClient = null); List ClassifyImages(IEnumerable filesPaths, Action actionAfterEachClassify = null); NsfwSpyFramesResult ClassifyVideo(byte[] video, VideoOptions videoOptions = null); NsfwSpyFramesResult ClassifyVideo(string filePath, VideoOptions videoOptions = null); NsfwSpyFramesResult ClassifyVideo(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null); Task ClassifyVideoAsync(string filePath, VideoOptions videoOptions = null); Task ClassifyVideoAsync(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null); } } ================================================ FILE: NsfwSpy/NsfwSpy.cs ================================================ using HeyRed.Mime; using ImageMagick; using Microsoft.ML; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; namespace NsfwSpyNS { /// /// The NsfwSpy classifier class used to analyse images for explicit content. /// public class NsfwSpy : INsfwSpy { private static ITransformer _model; public NsfwSpy() { if (_model == null) { var modelPath = Path.Combine(AppContext.BaseDirectory, "NsfwSpyModel.zip"); var mlContext = new MLContext(); _model = mlContext.Model.Load(modelPath, out var modelInputSchema); } } /// /// Classify an image from a byte array. /// /// The image content read as a byte array. /// A NsfwSpyResult that indicates the predicted value and scores for the 5 categories of classification. public NsfwSpyResult ClassifyImage(byte[] imageData) { var fileType = MimeGuesser.GuessFileType(imageData); if (fileType.Extension == "webp") { using (MagickImage image = new MagickImage(imageData)) { imageData = image.ToByteArray(MagickFormat.Png); } } var modelInput = new ModelInput(imageData); var mlContext = new MLContext(); var predictionEngine = mlContext.Model.CreatePredictionEngine(_model); var modelOutput = predictionEngine.Predict(modelInput); return new NsfwSpyResult(modelOutput); } /// /// Classify an image from a file path. /// /// Path to the image to be classified. /// A NsfwSpyResult that indicates the predicted value and scores for the 5 categories of classification. public NsfwSpyResult ClassifyImage(string filePath) { var fileBytes = File.ReadAllBytes(filePath); var result = ClassifyImage(fileBytes); return result; } /// /// Classify an image from a web url. /// /// Web address of the image to be classified. /// A custom WebClient to download the image with. /// A NsfwSpyResult that indicates the predicted value and scores for the 5 categories of classification. public NsfwSpyResult ClassifyImage(Uri uri, WebClient webClient = null) { if (webClient == null) webClient = new WebClient(); var fileBytes = webClient.DownloadData(uri); var result = ClassifyImage(fileBytes); return result; } /// /// Classify an image from a file path asynchronously. /// /// Path to the image to be classified. /// A NsfwSpyResult that indicates the predicted value and scores for the 5 categories of classification. public async Task ClassifyImageAsync(string filePath) { var fileBytes = await File.ReadAllBytesAsync(filePath); var result = ClassifyImage(fileBytes); return result; } /// /// Classify an image from a web url asynchronously. /// /// Web address of the image to be classified. /// A custom WebClient to download the image with. /// A NsfwSpyResult that indicates the predicted value and scores for the 5 categories of classification. public async Task ClassifyImageAsync(Uri uri, WebClient webClient = null) { if (webClient == null) webClient = new WebClient(); var fileBytes = await webClient.DownloadDataTaskAsync(uri); var result = ClassifyImage(fileBytes); return result; } /// /// Classify multiple images from a list of file paths. /// /// Collection of file paths to be classified. /// Action to be invoked after each file is classified. /// A list of results with their associated file paths. public List ClassifyImages(IEnumerable filesPaths, Action actionAfterEachClassify = null) { var results = new ConcurrentBag(); var sync = new object(); Parallel.ForEach(filesPaths, filePath => { var result = ClassifyImage(filePath); var value = new NsfwSpyValue(filePath, result); results.Add(value); lock (sync) { if (actionAfterEachClassify != null) actionAfterEachClassify.Invoke(filePath, result); } }); return results.ToList(); } /// /// Classify a .gif file from a byte array. /// /// The Gif content read as a byte array. /// VideoOptions to customise how the frames of the file are classified. /// A NsfwSpyFramesResult with results for each frame classified. public NsfwSpyFramesResult ClassifyGif(byte[] gifImage, VideoOptions videoOptions = null) { if (videoOptions == null) videoOptions = new VideoOptions(); if (videoOptions.ClassifyEveryNthFrame < 1) throw new Exception("VideoOptions.ClassifyEveryNthFrame must not be less than 1."); var results = new ConcurrentDictionary(); using (var collection = new MagickImageCollection(gifImage)) { collection.Coalesce(); var frameCount = collection.Count; Parallel.For(0, frameCount, (i, state) => { if (i % videoOptions.ClassifyEveryNthFrame != 0) return; if (state.ShouldExitCurrentIteration) return; var frame = collection[i]; var frameData = frame.ToByteArray(); var result = ClassifyImage(frameData); results.GetOrAdd(i, result); // Stop classifying frames if Nsfw frame is found if (result.IsNsfw && videoOptions.EarlyStopOnNsfw) state.Break(); }); } var resultDictionary = results.OrderBy(r => r.Key).ToDictionary(r => r.Key, r => r.Value); var gifResult = new NsfwSpyFramesResult(resultDictionary); return gifResult; } /// /// Classify a .gif file from a path. /// /// Path to the .gif to be classified. /// VideoOptions to customise how the frames of the file are classified. /// A NsfwSpyFramesResult with results for each frame classified. public NsfwSpyFramesResult ClassifyGif(string filePath, VideoOptions videoOptions = null) { var gifImage = File.ReadAllBytes(filePath); var results = ClassifyGif(gifImage, videoOptions); return results; } /// /// Classify a .gif file from a web url. /// /// Web address of the Gif to be classified. /// A custom WebClient to download the Gif with. /// VideoOptions to customise how the frames of the file are classified. /// A NsfwSpyFramesResult with results for each frame classified. public NsfwSpyFramesResult ClassifyGif(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null) { if (webClient == null) webClient = new WebClient(); var gifImage = webClient.DownloadData(uri); var results = ClassifyGif(gifImage, videoOptions); return results; } /// /// Classify a .gif file from a path asynchronously. /// /// Path to the .gif to be classified. /// VideoOptions to customise how the frames of the file are classified. /// A NsfwSpyFramesResult with results for each frame classified. public async Task ClassifyGifAsync(string filePath, VideoOptions videoOptions = null) { var gifImage = await File.ReadAllBytesAsync(filePath); var results = ClassifyGif(gifImage, videoOptions); return results; } /// /// Classify a .gif file from a web url asynchronously. /// /// Web address of the Gif to be classified. /// A custom WebClient to download the Gif with. /// VideoOptions to customise how the frames of the file are classified. /// A NsfwSpyFramesResult with results for each frame classified. public async Task ClassifyGifAsync(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null) { if (webClient == null) webClient = new WebClient(); var gifImage = await webClient.DownloadDataTaskAsync(uri); var results = ClassifyGif(gifImage, videoOptions); return results; } /// /// Classify a video file from a byte array. /// /// The video content read as a byte array. /// VideoOptions to customise how the frames of the file are classified. /// A NsfwSpyFramesResult with results for each frame classified. public NsfwSpyFramesResult ClassifyVideo(byte[] video, VideoOptions videoOptions = null) { if (videoOptions == null) videoOptions = new VideoOptions(); if (videoOptions.ClassifyEveryNthFrame < 1) throw new Exception("VideoOptions.ClassifyEveryNthFrame must not be less than 1."); var results = new ConcurrentDictionary(); using (var collection = new MagickImageCollection(video, MagickFormat.Mp4)) { collection.Coalesce(); var frameCount = collection.Count; Parallel.For(0, frameCount, (i, state) => { if (i % videoOptions.ClassifyEveryNthFrame != 0) return; if (state.ShouldExitCurrentIteration) return; var frame = collection[i]; frame.Format = MagickFormat.Jpg; var result = ClassifyImage(frame.ToByteArray()); results.GetOrAdd(i, result); // Stop classifying frames if Nsfw frame is found if (result.IsNsfw && videoOptions.EarlyStopOnNsfw) state.Break(); }); } var resultDictionary = results.OrderBy(r => r.Key).ToDictionary(r => r.Key, r => r.Value); var gifResult = new NsfwSpyFramesResult(resultDictionary); return gifResult; } /// /// Classify a .gif file from a path. /// /// Path to the video to be classified. /// VideoOptions to customise how the frames of the file are classified. /// A NsfwSpyFramesResult with results for each frame classified. public NsfwSpyFramesResult ClassifyVideo(string filePath, VideoOptions videoOptions = null) { var video = File.ReadAllBytes(filePath); var results = ClassifyVideo(video, videoOptions); return results; } /// /// Classify a .gif file from a web url. /// /// Web address of the video to be classified. /// A custom WebClient to download the video with. /// VideoOptions to customise how the frames of the file are classified. /// A NsfwSpyFramesResult with results for each frame classified. public NsfwSpyFramesResult ClassifyVideo(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null) { if (webClient == null) webClient = new WebClient(); var video = webClient.DownloadData(uri); var results = ClassifyVideo(video, videoOptions); return results; } /// /// Classify a .gif file from a path asynchronously. /// /// Path to the video to be classified. /// VideoOptions to customise how the frames of the file are classified. /// A NsfwSpyFramesResult with results for each frame classified. public async Task ClassifyVideoAsync(string filePath, VideoOptions videoOptions = null) { var video = await File.ReadAllBytesAsync(filePath); var results = ClassifyVideo(video, videoOptions); return results; } /// /// Classify a .gif file from a web url asynchronously. /// /// Web address of the video to be classified. /// A custom WebClient to download the video with. /// VideoOptions to customise how the frames of the file are classified. /// A NsfwSpyFramesResult with results for each frame classified. public async Task ClassifyVideoAsync(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null) { if (webClient == null) webClient = new WebClient(); var video = await webClient.DownloadDataTaskAsync(uri); var results = ClassifyVideo(video, videoOptions); return results; } } } ================================================ FILE: NsfwSpy/NsfwSpy.csproj ================================================  netcoreapp2.0 NsfwSpy NsfwSpy nsfw nude nudity porn pornography explicit image detect detection classification classifier machinglearning ml.net NsfwSpy is an image and video classifier used to identify explicit/pornographic content using machine learning. https://github.com/NsfwSpy/NsfwSpy.NET https://github.com/NsfwSpy/NsfwSpy.NET NsfwSpy-Icon.png false LICENSE 3.5.0 True True True true Always true ================================================ FILE: NsfwSpy/Resources/ClassificationFailedException.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace NsfwSpyNS { public class ClassificationFailedException : Exception { public ClassificationFailedException(string message) : base(message) { } } } ================================================ FILE: NsfwSpy/Resources/EClassificationType.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NsfwSpyNS { internal enum EClassificationType { Hentai = 0, Neutral = 1, Pornography = 2, Sexy = 3 } } ================================================ FILE: NsfwSpy/Resources/ModelInput.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NsfwSpyNS { class ModelInput { public ModelInput(byte[] image) { Image = image; } public byte[] Image { get; set; } } } ================================================ FILE: NsfwSpy/Resources/ModelOutput.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NsfwSpyNS { class ModelOutput { public string PredictedLabel { get; set; } public float[] Score { get; set; } } } ================================================ FILE: NsfwSpy/Resources/NsfwSpyFramesResult.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace NsfwSpyNS { /// /// The result from classifying a Gif file. /// public class NsfwSpyFramesResult { /// /// The NsfwSpyResults for each of the frames classified with the key being the frame index. /// public Dictionary Frames { get; set; } /// /// The amount of frames classified. /// public int FrameCount => Frames.Count; /// /// True if any of the frames have been classified as NSFW. /// public bool IsNsfw => Frames.Any(f => f.Value.IsNsfw); public NsfwSpyFramesResult(Dictionary frames) { Frames = frames; } } } ================================================ FILE: NsfwSpy/Resources/NsfwSpyResult.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NsfwSpyNS { /// /// The result from classifying an image. /// public class NsfwSpyResult { /// /// The hentai probability score between 0 and 1. /// public float Hentai { get; } /// /// The neutral probability score between 0 and 1. /// public float Neutral { get; } /// /// The pornography probability score between 0 and 1. /// public float Pornography { get; } /// /// The sexy probability score between 0 and 1. /// public float Sexy { get; } /// /// The most likely predicted value. /// public string PredictedLabel { get; } /// /// Whether the image is likely to be explicit. True if the sum of pornography, hentai and sexy is equal to or above 0.5. /// public bool IsNsfw => Neutral < 0.5; public NsfwSpyResult() { } internal NsfwSpyResult(ModelOutput modelOutput) { Hentai = modelOutput.Score[(int)EClassificationType.Hentai]; Neutral = modelOutput.Score[(int)EClassificationType.Neutral]; Pornography = modelOutput.Score[(int)EClassificationType.Pornography]; Sexy = modelOutput.Score[(int)EClassificationType.Sexy]; PredictedLabel = modelOutput.PredictedLabel; if (Hentai + Neutral + Pornography + Sexy == 0) { throw new ClassificationFailedException("Classification of the file failed. Make sure the file is a valid image format (jpg, png, gif etc) and has been loaded correctly."); } } /// /// Get the 5 classification types as a dictionary ordered by their score. /// /// Dictionary of the prediction scores. public Dictionary ToDictionary() { var dictionary = new Dictionary { { "Hentai", Hentai }, { "Neutral", Neutral }, { "Pornography", Pornography }, { "Sexy", Sexy } }; return dictionary.OrderByDescending(p => p.Value).ToDictionary(x => x.Key, x => x.Value); ; } } } ================================================ FILE: NsfwSpy/Resources/NsfwSpyValue.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NsfwSpyNS { public class NsfwSpyValue { public string FilePath { get; } public NsfwSpyResult Result { get; } public NsfwSpyValue(string filePath, NsfwSpyResult result) { FilePath = filePath; Result = result; } } } ================================================ FILE: NsfwSpy/Resources/VideoOptions.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace NsfwSpyNS { /// /// Customise how the frames of a video file are classified. /// public class VideoOptions { /// /// Stop classifying frames if a NSFW frame is found. /// public bool EarlyStopOnNsfw { get; set; } = false; /// /// Improve performance by only classifying one in every Nth frames e.g. 1 = classify all frames, 2 = classify 1 in 2 frames. /// public int ClassifyEveryNthFrame { get; set; } = 1; } } ================================================ FILE: NsfwSpy.App/Controllers/NsfwSpyController.cs ================================================ using HeyRed.Mime; using Microsoft.AspNetCore.Mvc; using System.Web; namespace NsfwSpyNS.App.Controllers { [ApiController] [Route("[controller]")] public class NsfwSpyController : ControllerBase { private INsfwSpy _nsfwSpy; public NsfwSpyController(INsfwSpy nsfwSpy) { _nsfwSpy = nsfwSpy; } [HttpGet("url/{url}")] public async Task GetUrlMediaAsync(string url) { url = HttpUtility.UrlDecode(url); var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36"); var byteArray = await httpClient.GetByteArrayAsync(url); var mimeType = MimeGuesser.GuessMimeType(byteArray); return new FileContentResult(byteArray, mimeType); } [HttpPost("image")] public ActionResult ClassifyImage(IFormFile file) { var fileBytes = ConvertFormFileToByteArray(file); var result = _nsfwSpy.ClassifyImage(fileBytes); return Ok(result); } [HttpPost("gif")] public ActionResult ClassifyGif(IFormFile file) { var fileBytes = ConvertFormFileToByteArray(file); var videoOptions = new VideoOptions { EarlyStopOnNsfw = false }; var result = _nsfwSpy.ClassifyGif(fileBytes, videoOptions); return Ok(result); } [HttpPost("video")] public ActionResult ClassifyVideo(IFormFile file) { var fileBytes = ConvertFormFileToByteArray(file); var videoOptions = new VideoOptions { EarlyStopOnNsfw = false }; var result = _nsfwSpy.ClassifyVideo(fileBytes, videoOptions); return Ok(result); } private byte[] ConvertFormFileToByteArray(IFormFile file) { using (var ms = new MemoryStream()) { file.CopyTo(ms); var fileBytes = ms.ToArray(); return fileBytes; } } } public class MediaInfo { public IFormFile File { get; set; } public string MimeType { get; set; } } } ================================================ FILE: NsfwSpy.App/NsfwSpy.App.csproj ================================================ net6.0 enable true Latest false client-app\ $(DefaultItemExcludes);$(SpaRoot)node_modules\** https://localhost:3000 npm start enable all runtime; build; native; contentfiles; analyzers; buildtransitive wwwroot\%(RecursiveDir)%(FileName)%(Extension) PreserveNewest true ================================================ FILE: NsfwSpy.App/Pages/Error.cshtml ================================================ @page @model ErrorModel @{ ViewData["Title"] = "Error"; }

Error.

An error occurred while processing your request.

@if (Model.ShowRequestId) {

Request ID: @Model.RequestId

}

Development Mode

Swapping to the Development environment displays detailed information about the error that occurred.

The Development environment shouldn't be enabled for deployed applications. It can result in displaying sensitive information from exceptions to end users. For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app.

================================================ FILE: NsfwSpy.App/Pages/Error.cshtml.cs ================================================ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Diagnostics; namespace NsfwSpyNS.App.Pages { [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public class ErrorModel : PageModel { private readonly ILogger _logger; public ErrorModel(ILogger logger) { _logger = logger; } public string? RequestId { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); public void OnGet() { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; } } } ================================================ FILE: NsfwSpy.App/Pages/_ViewImports.cshtml ================================================ @using NsfwSpyNS.App @namespace NsfwSpyNS.App.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers ================================================ FILE: NsfwSpy.App/Program.cs ================================================ using NsfwSpyNS; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.AddSingleton(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.MapControllerRoute( name: "default", pattern: "{controller}/{action=Index}/{id?}"); app.MapFallbackToFile("index.html"); ; app.Run(); ================================================ FILE: NsfwSpy.App/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:57025", "sslPort": 44385 } }, "profiles": { "NsfwSpy.App": { "commandName": "Project", "launchBrowser": true, "applicationUrl": "https://localhost:7260;http://localhost:5260", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" } } } } ================================================ FILE: NsfwSpy.App/README.md ================================================ # Getting Started Looking for a quick way to try out NsfwSpy? Use our dedicated, self-hosted web app to classify images, gifs and videos with ease. 1. Check you have [NodeJS](https://nodejs.org/) installed by using the following command in the terminal: ``` node -v ``` 2. Clone the git repository to your local machine. 3. Open the NsfwSpy.sln solution in Visual Studio or your preferred IDE. 4. Run the NsfwSpy.App project using IIS Express: NsfwSpy App config 5. Upload your images and videos or paste a link to have them classified. NsfwSpy App ================================================ FILE: NsfwSpy.App/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" } ================================================ FILE: NsfwSpy.App/client-app/.gitignore ================================================ .DS_STORE node_modules scripts/flow/*/.flowconfig .flowconfig *~ *.pyc .grunt _SpecRunner.html __benchmarks__ build/ remote-repo/ coverage/ .module-cache fixtures/dom/public/react-dom.js fixtures/dom/public/react.js test/the-files-to-test.generated.js *.log* chrome-user-data *.sublime-project *.sublime-workspace .idea *.iml .vscode *.swp *.swo packages/react-devtools-core/dist packages/react-devtools-extensions/chrome/build packages/react-devtools-extensions/chrome/*.crx packages/react-devtools-extensions/chrome/*.pem packages/react-devtools-extensions/firefox/build packages/react-devtools-extensions/firefox/*.xpi packages/react-devtools-extensions/firefox/*.pem packages/react-devtools-extensions/shared/build packages/react-devtools-extensions/.tempUserDataDir packages/react-devtools-inline/dist packages/react-devtools-shell/dist packages/react-devtools-timeline/dist ================================================ FILE: NsfwSpy.App/client-app/package.json ================================================ { "name": "client-app", "version": "0.1.0", "private": true, "proxy": "https://localhost:44385", "type": "module", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/jest": "^26.0.15", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "node-sass": "^8.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-scripts": "^5.0.1", "recharts": "^2.2.0", "ts-node": "^10.4.0", "typescript": "^4.1.2", "web-vitals": "^1.0.1" }, "scripts": { "prestart": "node --loader ts-node/esm ./src/aspnetcore-https.ts && node --loader ts-node/esm ./src/aspnetcore-react.ts", "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "devDependencies": { "@types/node": "^16.11.36" } } ================================================ FILE: NsfwSpy.App/client-app/public/index.html ================================================ NsfwSpy | Pornographic .NET image classifier
================================================ FILE: NsfwSpy.App/client-app/public/manifest.json ================================================ { "short_name": "NsfwSpy App", "name": "NsfwSpy App", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "./assets/images/NsfwSpy-Icon.png", "type": "image/png", "sizes": "256x256" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: NsfwSpy.App/client-app/public/robots.txt ================================================ # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: NsfwSpy.App/client-app/src/App.scss ================================================ .app { color: #fff; font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } header { align-items: center; background-color: #000000; display: flex; height: 80px; justify-content: center; } main { background-color: #1b1b1b; height: calc(100vh - 80px); display: flex; flex: 1 1 0; flex-direction: column; align-items: center; justify-content: center; .image-section { flex-direction: column; } section { display: flex; flex: 1; flex-direction: column; align-items: center; max-width: 1000px; padding: 16px 0; width: calc(100% - 48px); } } .image-canvas { align-items: center; border: dashed #fff 6px; border-radius: 32px; cursor: pointer; display: flex; flex-direction: column; flex: 1 1 auto; font-size: 18px; justify-content: center; margin: 16px; padding: 16px; height: 30vh; .icons { display: flex; font-size: 56px; margin: 16px 0; div { margin: 16px 16px 0; } } .subtitle { font-size: 16px; } .image-preview, .video-preview { height: 100%; object-fit: contain; width: 100%; } } .result-value { display: flex; font-size: 18px; margin: 8px 0; text-transform: capitalize; width: 300px; &.hentai { color: #c25452; } &.neutral { color: #fff; } &.pornography { color: #ffa31a; } &.sexy { color: #fdfd66; } span { flex: 50%; } } input[type=text] { background: #292929; border: 1px solid #111; border-radius: 4px; box-shadow: inset 0 1px 2px #111; box-sizing: border-box; color: #fff; display: inline-block; font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; margin: 8px 0; outline: none; padding: 12px 20px; width: 60%; } ================================================ FILE: NsfwSpy.App/client-app/src/App.tsx ================================================ import React, { useEffect, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faImage, faVideo } from '@fortawesome/free-solid-svg-icons' import { Logo } from './components/Logo/Logo'; import { selectFiles } from './functions/selectFiles'; import { ImageFile } from './models/ImageFile'; import { getMediaInfo, uploadGif, uploadImage, uploadVideo } from './functions/client'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import './App.scss'; import { NsfwSpyFramesResult } from './models/NsfwSpyFramesResult'; import { NsfwSpyResult } from './models/NsfwSpyResult'; import { sortNsfwResult } from './functions/sortBy'; import { getContentType } from './functions/getContentType'; import { MediaInfo } from './models/MediaInfo'; export const App: React.FC = () => { const [url, setUrl] = useState(); const [image, setImage] = useState(); const [video, setVideo] = useState(); const [imageResults, setImageResults] = useState(); const [videoResults, setVideoResults] = useState(); const [processing, setProcessing] = useState(false); useEffect(() => { if (!url) return; const handleUrl = async () => { const result = await getMediaInfo(url); const data = await result.blob() handleFile(data); } handleUrl(); }, [url]) const selectFile = () => { selectFiles({ accept: 'image/*;video/*', multiple: false }).then(async files => { if (files) { handleFile(files[0]) } }); } const handleFile = async (file: Blob) => { const imageFile: ImageFile = { file: file, url: URL.createObjectURL(file) }; setImage(undefined); setVideo(undefined); setImageResults(undefined); setVideoResults(undefined); const fileType = imageFile.file.type; setProcessing(true); if (fileType === "image/gif") { setImage(imageFile); const result = await uploadGif(imageFile.file); const data: NsfwSpyFramesResult = await result.json(); setVideoResults(data); } else if (fileType.startsWith("image/")) { setImage(imageFile); const result = await uploadImage(imageFile.file); const data: NsfwSpyResult = await result.json(); setImageResults(data); } else if (fileType.startsWith("video/")) { setVideo(imageFile); const result = await uploadVideo(imageFile.file); const data: NsfwSpyFramesResult = await result.json(); setVideoResults(data); } setProcessing(false); } let sortedImageResults: [string, any][] | undefined = undefined; if (imageResults) { sortedImageResults = sortNsfwResult(imageResults); } return (
{!image && !video && <>
Select an image, Gif or video.
Or paste a link below...
} {image && } {video &&
setUrl(e.target.value)} />
{processing &&
Processing...
} {videoResults && <> {videoResults.frameCount} Frames Analysed } {sortedImageResults &&
{sortedImageResults.map((result) =>
{result[0]} {result[1]}
)}
}
); } export default App; ================================================ FILE: NsfwSpy.App/client-app/src/aspnetcore-https.ts ================================================ import * as fs from 'fs' import * as path from 'path' import * as child_process from 'child_process' const spawn = child_process.spawn; const baseFolder = process.env.APPDATA !== undefined && process.env.APPDATA !== '' ? `${process.env.APPDATA}/ASP.NET/https` : `${process.env.HOME}/.aspnet/https`; const certArg = process.argv.map(arg => arg.match('/--name=(?.+)/i')).filter(Boolean)[0]; const certName = certArg ? certArg?.groups?.value : process.env.npm_package_name; if (!certName) { console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<> explicitly'); process.exit(-1); } const moduleCertFilePath = path.join(baseFolder, `${certName}.pem`); const moduleKeyFilePath = path.join(baseFolder, `${certName}.key`); if (!fs.existsSync(moduleCertFilePath) || !fs.existsSync(moduleKeyFilePath)) { spawn('dotnet', [ 'dev-certs', 'https', '--export-path', moduleCertFilePath, '--format', 'Pem', '--no-password', ], { stdio: 'inherit', }) .on('exit', (code: any) => process.exit(code)); } ================================================ FILE: NsfwSpy.App/client-app/src/aspnetcore-react.ts ================================================ import * as fs from 'fs' import * as path from 'path' const baseFolder = process.env.APPDATA !== undefined && process.env.APPDATA !== '' ? `${process.env.APPDATA}/ASP.NET/https` : `${process.env.HOME}/.aspnet/https`; const certArg = process.argv.map(arg => arg.match('/--name=(?.+)/i')).filter(Boolean)[0]; const certName = certArg ? certArg?.groups?.value : process?.env?.npm_package_name; if (!certName) { console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<> explicitly'); process.exit(-1); } const certFilePath = path.join(baseFolder, `${certName}.pem`); const keyFilePath = path.join(baseFolder, `${certName}.key`); if (!fs.existsSync('.env.development.local')) { fs.writeFileSync( '.env.development.local', `BROWSER=none HTTPS=true SSL_CRT_FILE=${certFilePath} SSL_KEY_FILE=${keyFilePath}`, ); } else { let lines = fs.readFileSync('.env.development.local') .toString() .split('\n'); let hasCert, hasCertKey = false; for (const line of lines) { if (/SSL_CRT_FILE=.*/i.test(line)) { hasCert = true; } if (/SSL_KEY_FILE=*/i.test(line)) { hasCertKey = true; } } if (!hasCert) { fs.appendFileSync( '.env.development.local', `\nSSL_CRT_FILE=${certFilePath}` ); } if (!hasCertKey) { fs.appendFileSync( '.env.development.local', `\nSSL_KEY_FILE=${keyFilePath}` ) } } ================================================ FILE: NsfwSpy.App/client-app/src/components/Logo/Logo.scss ================================================ .logo { font-family: 'Roboto'; font-size: 56px; margin: 16px; user-select: none; .white { color: #fff; } .orange { color: #ffa31a; } } ================================================ FILE: NsfwSpy.App/client-app/src/components/Logo/Logo.tsx ================================================ import './Logo.scss'; export const Logo = () => { return ( Nsfw Spy ) } ================================================ FILE: NsfwSpy.App/client-app/src/functions/client.ts ================================================ export const uploadGif = (file: Blob) => { return uploadFile("/nsfwspy/gif", file); } export const uploadImage = (file: Blob) => { return uploadFile("/nsfwspy/image", file); } export const uploadVideo = (file: Blob) => { return uploadFile("/nsfwspy/video", file); } export const getMediaInfo = (url: string) => { return fetch(`/nsfwspy/url/${encodeURIComponent(url)}`); } const uploadFile = (url: string, file: Blob) => { var postSettings: RequestInit = { method: 'POST', mode: 'cors' }; var data = new FormData(); data.append('file', file); postSettings.body = data; return fetch(url, postSettings); } ================================================ FILE: NsfwSpy.App/client-app/src/functions/getContentType.ts ================================================ export const getContentType = (url: string) => { return new Promise((resolve, reject) => { var req = new XMLHttpRequest(); req.open("GET", url, true); req.setRequestHeader("Range", "bytes=0"); req.onreadystatechange = () => { if (req.readyState === req.DONE) { resolve(req.getResponseHeader("Content-Type")); } }; req.send(); }); } ================================================ FILE: NsfwSpy.App/client-app/src/functions/selectFiles.ts ================================================ /** * Custom type for file input element (``). */ type InputFile = HTMLInputElement & { capture?: boolean | string; }; /** * Type of options for file input element (``) virtually * created to select files. */ export type Options = { /** * Defines accepted file types. It's a comma-separated list of file * extensions, mime-types or unique file type specifiers. * * https://developer.mozilla.org/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers * * @example ```js * "image/*,video/*,.pdf,.doc,.docx,.xls" * ``` */ accept?: string; /** * Combined with `accept` property it specifies which camera to use for * capture of image or video. It was previously a Boolean value. */ capture?: string | null; /** * Allow multiple files selection. */ multiple?: boolean; }; /** * Creates a virtual file input element (``) with options. * @param options */ const createInputFile = ({ accept = '', capture = null, multiple = false, }: Options = {}): InputFile => { const input = document.createElement('input') as InputFile; input.type = 'file'; input.accept = accept; if (capture !== null) input.capture = capture; input.multiple = multiple; return input; }; /** * Virtually creates a file input element (``), triggers it * and returns selected files. * * @example * selectFiles({ accept: 'image/*', multiple: true }).then(files => { * // ... * }); * * @param options */ export const selectFiles = (options?: Options) => new Promise((resolve) => { const input = createInputFile(options); input.addEventListener('change', () => resolve(input.files || null)); setTimeout(() => { const event = new MouseEvent('click'); input.dispatchEvent(event); }, 0); }); ================================================ FILE: NsfwSpy.App/client-app/src/functions/sortBy.ts ================================================ import { NsfwSpyResult } from '../models/NsfwSpyResult'; export const sortNsfwResult = (result: NsfwSpyResult) => { const validKeys = ['hentai', 'neutral', 'pornography', 'sexy']; const sortableArray = Object.entries(result); let sortedArray = sortableArray.sort(([, a], [, b]) => b - a); sortedArray = sortableArray.filter((i) => validKeys.includes(i[0])); return sortedArray; } ================================================ FILE: NsfwSpy.App/client-app/src/index.scss ================================================ body { margin: 0; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } ================================================ FILE: NsfwSpy.App/client-app/src/index.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import './index.scss'; import App from './App'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( , document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); ================================================ FILE: NsfwSpy.App/client-app/src/models/ImageFile.ts ================================================ export interface ImageFile { file: Blob url: string } ================================================ FILE: NsfwSpy.App/client-app/src/models/MediaInfo.ts ================================================ export interface MediaInfo { file: Blob mimeType: string } ================================================ FILE: NsfwSpy.App/client-app/src/models/NsfwSpyFramesResult.ts ================================================ import { NsfwSpyResult } from "./NsfwSpyResult" export interface NsfwSpyFramesResult { frames: { [frame: number]: NsfwSpyResult } frameCount: number isNsfw: boolean } ================================================ FILE: NsfwSpy.App/client-app/src/models/NsfwSpyResult.ts ================================================ export interface NsfwSpyResult { hentai: number neutral: number pornography: number sexy: number predictedLabel: string isNsfw: boolean } ================================================ FILE: NsfwSpy.App/client-app/src/react-app-env.d.ts ================================================ /// ================================================ FILE: NsfwSpy.App/client-app/src/reportWebVitals.ts ================================================ import { ReportHandler } from 'web-vitals'; const reportWebVitals = (onPerfEntry?: ReportHandler) => { if (onPerfEntry && onPerfEntry instanceof Function) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); getLCP(onPerfEntry); getTTFB(onPerfEntry); }); } }; export default reportWebVitals; ================================================ FILE: NsfwSpy.App/client-app/tsconfig.json ================================================ { "compilerOptions": { "target": "ES6", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "ESNext", "moduleResolution": "Node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": [ "src" ] } ================================================ FILE: NsfwSpy.PerformanceTesting/NsfwSpy.PerformanceTesting.csproj ================================================  Exe net6.0 ================================================ FILE: NsfwSpy.PerformanceTesting/PerformanceResult.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NsfwSpyNS.PerformanceTesting { class PerformanceResult { public string Key { get; set; } public List Results { get; set; } public int CorrectAsserts => Results.Count(r => r.PredictedLabel == Key); public int NsfwAsserts => Results.Count(r => r.IsNsfw); public int HentaiAsserts => Results.Count(r => r.PredictedLabel == "Hentai"); public int NeutralAsserts => Results.Count(r => r.PredictedLabel == "Neutral"); public int PornographyAsserts => Results.Count(r => r.PredictedLabel == "Pornography"); public int SexyAsserts => Results.Count(r => r.PredictedLabel == "Sexy"); public int TotalAsserts => Results.Count(); public PerformanceResult(string key) { Key = key; Results = new List(); } } } ================================================ FILE: NsfwSpy.PerformanceTesting/Program.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; namespace NsfwSpyNS.PerformanceTesting { class Program { static void Main(string[] args) { var assetsPath = @"E:\NsfwSpy\Images"; var testImagesPath = @"E:\NsfwSpy\Test"; var classificationTypes = new[] { "Hentai", "Neutral", "Pornography", "Sexy", }; var results = new List(); var nsfwSpy = new NsfwSpy(); foreach (var classificationType in classificationTypes) { var testFilesDirectory = Path.Combine(testImagesPath, classificationType); var testFiles = Directory.Exists(testFilesDirectory) ? Directory.GetFiles(testFilesDirectory).ToList() : new List(); if (!testFiles.Any()) { var directory = Path.Combine(assetsPath, classificationType); var files = Directory.GetFiles(directory).OrderBy(f => Guid.NewGuid()).ToList(); var length = files.Count > 10000 ? 10000 : files.Count; files = files.Take(length).ToList(); Console.WriteLine($"Copying {classificationType} test files..."); if (!Directory.Exists(testFilesDirectory)) Directory.CreateDirectory(testFilesDirectory); Parallel.ForEach(files, file => { var filename = Path.GetFileName(file); var dest = Path.Combine(testFilesDirectory, filename); File.Copy(file, dest); }); testFiles = Directory.GetFiles(testFilesDirectory).ToList(); } var pr = new PerformanceResult(classificationType); nsfwSpy.ClassifyImages(testFiles, (filePath, result) => { pr.Results.Add(result); Console.WriteLine($"{pr.Key} | Correct Asserts: {pr.CorrectAsserts} / {pr.TotalAsserts} ({(Convert.ToDouble(pr.CorrectAsserts) / pr.TotalAsserts) * 100}%) | IsNsfw: {pr.NsfwAsserts} / {pr.TotalAsserts} ({(Convert.ToDouble(pr.NsfwAsserts) / pr.TotalAsserts) * 100}%)"); }); results.Add(pr); } Console.WriteLine(Environment.NewLine); foreach (var pr in results) { Console.WriteLine($"{pr.Key} | Correct Asserts: {pr.CorrectAsserts} / {pr.TotalAsserts} ({(Convert.ToDouble(pr.CorrectAsserts) / pr.TotalAsserts) * 100}%) | IsNsfw: {pr.NsfwAsserts} / {pr.TotalAsserts} ({(Convert.ToDouble(pr.NsfwAsserts) / pr.TotalAsserts) * 100}%)"); } Console.WriteLine(Environment.NewLine); Console.WriteLine("Confusion Matrix\n"); Console.WriteLine("\t\t\tPredicted Label"); Console.WriteLine("Actual Label\t\tHentai\t\tNeutral\t\tPornography\tSexy"); Console.WriteLine(); foreach (var pr in results) { Console.WriteLine($"{pr.Key}\t\t{(pr.Key != "Pornography" ? "\t" : "")}{pr.HentaiAsserts}\t\t{pr.NeutralAsserts}\t\t{pr.PornographyAsserts}\t\t{pr.SexyAsserts}"); } Console.WriteLine(Environment.NewLine); Console.WriteLine("Average Confidence\n"); foreach (var pr in results) { Console.WriteLine($"{pr.Key}\t\t{(pr.Key != "Pornography" ? "\t" : "")}{pr.Results.Sum(r => r.ToDictionary()[pr.Key]) / pr.Results.Count}"); } } } } ================================================ FILE: NsfwSpy.Test/NsfwSpy.Test.csproj ================================================  net6.0 false runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest ================================================ FILE: NsfwSpy.Test/UnitTests.cs ================================================ using System; using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; using Xunit; namespace NsfwSpyNS.Test { public class UnitTests { [Theory] [InlineData("flower.jpg")] [InlineData("flower.png")] [InlineData("flower.webp")] public void ClassifyImageByteArray_ValidByteArray(string filename) { var filePath = Path.Combine(AppContext.BaseDirectory, $@"Assets/{filename}"); var imageBytes = File.ReadAllBytes(filePath); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyImage(imageBytes); Assert.Equal("Neutral", result.PredictedLabel); } [Fact] public void ClassifyImageByteArray_InvalidByteArray() { var nsfwSpy = new NsfwSpy(); Assert.Throws(() => nsfwSpy.ClassifyImage(new byte[0])); } [Theory] [InlineData("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.jpg")] [InlineData("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.png")] [InlineData("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.webp")] public void ClassifyImageUri_ValidUri(string url) { var uri = new Uri(url); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyImage(uri); Assert.Equal("Neutral", result.PredictedLabel); } [Theory] [InlineData("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.jpg")] [InlineData("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.png")] [InlineData("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.webp")] public void ClassifyImageUri_CustomWebClient(string url) { var uri = new Uri(url); var webClient = new WebClient(); webClient.Headers["User-Agent"] = "test"; var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyImage(uri, webClient); Assert.Equal("Neutral", result.PredictedLabel); } [Theory] [InlineData("flower.jpg")] [InlineData("flower.png")] [InlineData("flower.webp")] public void ClassifyImageFilePath_ValidFilePath(string filename) { var filePath = Path.Combine(AppContext.BaseDirectory, $@"Assets/{filename}"); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyImage(filePath); Assert.Equal("Neutral", result.PredictedLabel); } [Fact] public void ClassifyImageFilePath_InvalidFilePath() { var filePath = Path.Combine(AppContext.BaseDirectory, @"Assets/filedoesnotexist.jpg"); var nsfwSpy = new NsfwSpy(); Assert.Throws(() => nsfwSpy.ClassifyImage(filePath)); } [Theory] [InlineData("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.jpg")] [InlineData("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.png")] [InlineData("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.webp")] public async Task ClassifyImageUriAsync_ValidUri(string url) { var uri = new Uri(url); var nsfwSpy = new NsfwSpy(); var result = await nsfwSpy.ClassifyImageAsync(uri); Assert.Equal("Neutral", result.PredictedLabel); } [Theory] [InlineData("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.jpg")] [InlineData("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.png")] [InlineData("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.webp")] public async Task ClassifyImageUriAsync_CustomWebClient(string url) { var uri = new Uri(url); var webClient = new WebClient(); webClient.Headers["User-Agent"] = "test"; var nsfwSpy = new NsfwSpy(); var result = await nsfwSpy.ClassifyImageAsync(uri, webClient); Assert.Equal("Neutral", result.PredictedLabel); } [Theory] [InlineData("flower.jpg")] [InlineData("flower.png")] [InlineData("flower.webp")] public async Task ClassifyImageFilePathAsync_ValidFilePath(string filename) { var filePath = Path.Combine(AppContext.BaseDirectory, $@"Assets/{filename}"); var nsfwSpy = new NsfwSpy(); var result = await nsfwSpy.ClassifyImageAsync(filePath); Assert.Equal("Neutral", result.PredictedLabel); } [Fact] public void ClassifyGifByteArray_ValidByteArray() { var filePath = Path.Combine(AppContext.BaseDirectory, @"Assets/cool.gif"); var imageBytes = File.ReadAllBytes(filePath); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyGif(imageBytes); Assert.Equal(10, result.Frames.Count); Assert.False(result.IsNsfw); } [Fact] public void ClassifyGifFilePath_ValidFilePath() { var filePath = Path.Combine(AppContext.BaseDirectory, @"Assets/cool.gif"); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyGif(filePath); Assert.Equal(10, result.Frames.Count); Assert.False(result.IsNsfw); } [Fact] public void ClassifyGifFilePath_ClassifyEvery2ndFrame() { var filePath = Path.Combine(AppContext.BaseDirectory, @"Assets/cool.gif"); var videoOptions = new VideoOptions { ClassifyEveryNthFrame = 2 }; var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyGif(filePath, videoOptions); Assert.Equal(5, result.Frames.Count); Assert.False(result.IsNsfw); } [Fact] public void ClassifyGifFilePath_EndEarlyOnNsfw() { var filePath = Path.Combine(AppContext.BaseDirectory, @"Assets/bikini.gif"); var videoOptions = new VideoOptions { EarlyStopOnNsfw = true }; var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyGif(filePath, videoOptions: videoOptions); Assert.True(result.IsNsfw); Assert.True(result.Frames.Count < 181); // This Gif has 181 frames } [Fact] public void ClassifyGifUri_ValidUri() { var uri = new Uri("https://media2.giphy.com/media/62PP2yEIAZF6g/giphy.gif"); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyGif(uri); Assert.Equal(10, result.Frames.Count); Assert.False(result.IsNsfw); } [Fact] public void ClassifyGifUri_ClassifyEvery2ndFrame() { var uri = new Uri("https://media2.giphy.com/media/62PP2yEIAZF6g/giphy.gif"); var videoOptions = new VideoOptions { ClassifyEveryNthFrame = 2 }; var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyGif(uri, videoOptions: videoOptions); Assert.Equal(5, result.Frames.Count); Assert.False(result.IsNsfw); } [Fact] public void ClassifyGifUri_EndEarlyOnNsfw() { var uri = new Uri("https://c.tenor.com/5y-jOowm51MAAAAd/bikini.gif"); var videoOptions = new VideoOptions { EarlyStopOnNsfw = true }; var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyGif(uri, videoOptions: videoOptions); Assert.True(result.IsNsfw); Assert.True(result.Frames.Count < 181); // This Gif has 181 frames } [Fact] public async Task ClassifyGifFilePathAsync_ValidFilePath() { var filePath = Path.Combine(AppContext.BaseDirectory, @"Assets/cool.gif"); var nsfwSpy = new NsfwSpy(); var result = await nsfwSpy.ClassifyGifAsync(filePath); Assert.Equal(10, result.Frames.Count); Assert.False(result.IsNsfw); } [Fact] public async Task ClassifyGifUriAsync_ValidUri() { var uri = new Uri("https://media2.giphy.com/media/62PP2yEIAZF6g/giphy.gif"); var nsfwSpy = new NsfwSpy(); var result = await nsfwSpy.ClassifyGifAsync(uri); Assert.Equal(10, result.Frames.Count); Assert.False(result.IsNsfw); } [Theory] [InlineData("bikini.avi")] [InlineData("bikini.mkv")] [InlineData("bikini.mp4")] [InlineData("bikini.webm")] public void ClassifyVideoByteArray_ValidByteArray(string filename) { var filePath = Path.Combine(AppContext.BaseDirectory, $@"Assets\{filename}"); var imageBytes = File.ReadAllBytes(filePath); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyVideo(imageBytes); if (filename.EndsWith(".webm")) Assert.Equal(180, result.Frames.Count); else Assert.Equal(181, result.Frames.Count); Assert.True(result.IsNsfw); } [Theory] [InlineData("bikini.avi")] [InlineData("bikini.mkv")] [InlineData("bikini.mp4")] [InlineData("bikini.webm")] public void ClassifyVideoFilePath_ValidFilePath(string filename) { var filePath = Path.Combine(AppContext.BaseDirectory, $@"Assets\{filename}"); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyVideo(filePath); if (filename.EndsWith(".webm")) Assert.Equal(180, result.Frames.Count); else Assert.Equal(181, result.Frames.Count); Assert.True(result.IsNsfw); } [Theory] [InlineData("bikini.avi")] [InlineData("bikini.mkv")] [InlineData("bikini.mp4")] [InlineData("bikini.webm")] public void ClassifyVideoFilePath_ClassifyEvery2ndFrame(string filename) { var filePath = Path.Combine(AppContext.BaseDirectory, $@"Assets\{filename}"); var videoOptions = new VideoOptions { ClassifyEveryNthFrame = 2 }; var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyVideo(filePath, videoOptions); if (filename.EndsWith(".webm")) Assert.Equal(90, result.Frames.Count); else Assert.Equal(91, result.Frames.Count); Assert.True(result.IsNsfw); } [Theory] [InlineData("bikini.avi")] [InlineData("bikini.mkv")] [InlineData("bikini.mp4")] [InlineData("bikini.webm")] public void ClassifyVideoFilePath_EndEarlyOnNsfw(string filename) { var filePath = Path.Combine(AppContext.BaseDirectory, $@"Assets\{filename}"); var videoOptions = new VideoOptions { EarlyStopOnNsfw = true }; var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyVideo(filePath, videoOptions: videoOptions); Assert.True(result.IsNsfw); Assert.True(result.Frames.Count < 181); // This video has 181 frames } [Fact] public void ClassifyVideoUri_ValidUri() { var uri = new Uri("https://i.imgur.com/MjTH5ZS.mp4"); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyVideo(uri); Assert.Equal(120, result.Frames.Count); Assert.True(result.IsNsfw); } [Fact] public void ClassifyVideoUri_ClassifyEvery2ndFrame() { var uri = new Uri("https://i.imgur.com/MjTH5ZS.mp4"); var videoOptions = new VideoOptions { ClassifyEveryNthFrame = 2 }; var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyVideo(uri, videoOptions: videoOptions); Assert.Equal(60, result.Frames.Count); Assert.True(result.IsNsfw); } [Fact] public void ClassifyVideoUri_EndEarlyOnNsfw() { var uri = new Uri("https://i.imgur.com/MjTH5ZS.mp4"); var videoOptions = new VideoOptions { EarlyStopOnNsfw = true }; var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyVideo(uri, videoOptions: videoOptions); Assert.True(result.IsNsfw); Assert.True(result.Frames.Count < 120); // This video has 120 frames } [Theory] [InlineData("bikini.avi")] [InlineData("bikini.mkv")] [InlineData("bikini.mp4")] [InlineData("bikini.webm")] public async Task ClassifyVideoFilePathAsync_ValidFilePath(string filename) { var filePath = Path.Combine(AppContext.BaseDirectory, $@"Assets\{filename}"); var nsfwSpy = new NsfwSpy(); var result = await nsfwSpy.ClassifyVideoAsync(filePath); if (filename.EndsWith(".webm")) Assert.Equal(180, result.Frames.Count); else Assert.Equal(181, result.Frames.Count); Assert.True(result.IsNsfw); } [Fact] public async Task ClassifyVideoUriAsync_ValidUri() { var uri = new Uri("https://i.imgur.com/MjTH5ZS.mp4"); var nsfwSpy = new NsfwSpy(); var result = await nsfwSpy.ClassifyVideoAsync(uri); Assert.Equal(120, result.Frames.Count); Assert.True(result.IsNsfw); } } } ================================================ FILE: NsfwSpy.Train/ImageData.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NsfwSpyNS.Train { class ImageData { public string ImagePath { get; set; } public string Label { get; set; } } } ================================================ FILE: NsfwSpy.Train/NsfwSpy.Train.csproj ================================================  Exe net6.0 ================================================ FILE: NsfwSpy.Train/Program.cs ================================================ using Microsoft.ML; using Microsoft.ML.Trainers; using Microsoft.ML.Vision; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace NsfwSpyNS.Train { class Program { static void Main(string[] args) { var projectDirectory = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../")); var workspaceRelativePath = Path.Combine(projectDirectory, "Workspace"); var saveModelPath = Path.Combine(projectDirectory, "Models"); var assetsPath = @"E:\NsfwSpy\Images"; var mlContext = new MLContext(); mlContext.Log += (object sender, LoggingEventArgs e) => Console.WriteLine(e.Message); var images = LoadImagesFromDirectory(assetsPath); var imageData = mlContext.Data.LoadFromEnumerable(images); var shuffledData = mlContext.Data.ShuffleRows(imageData); var preprocessingPipeline = mlContext.Transforms.Conversion.MapValueToKey( inputColumnName: "Label", outputColumnName: "LabelAsKey") .Append(mlContext.Transforms.LoadRawImageBytes( outputColumnName: "Image", imageFolder: assetsPath, inputColumnName: "ImagePath")); var preProcessedData = preprocessingPipeline .Fit(shuffledData) .Transform(shuffledData); var trainSplit = mlContext.Data.TrainTestSplit(data: preProcessedData, testFraction: 0.1); var trainSet = trainSplit.TrainSet; var validationSet = trainSplit.TestSet; var classifierOptions = new ImageClassificationTrainer.Options() { FeatureColumnName = "Image", LabelColumnName = "LabelAsKey", ValidationSet = validationSet, Arch = ImageClassificationTrainer.Architecture.ResnetV250, MetricsCallback = (metrics) => Console.WriteLine(metrics), TestOnTrainSet = true, ReuseTrainSetBottleneckCachedValues = true, ReuseValidationSetBottleneckCachedValues = true, Epoch = 2500, BatchSize = 32, LearningRate = 0.01f, EarlyStoppingCriteria = new ImageClassificationTrainer.EarlyStopping { CheckIncreasing = true, MinDelta = 0.00001f, Patience = 50 }, WorkspacePath = workspaceRelativePath, LearningRateScheduler = new ExponentialLRDecay(numEpochsPerDecay: 5) }; var trainingPipeline = mlContext.MulticlassClassification.Trainers.ImageClassification(classifierOptions) .Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel")); var trainedModel = trainingPipeline.Fit(trainSet); if (!Directory.Exists(saveModelPath)) Directory.CreateDirectory(saveModelPath); saveModelPath = Path.Combine(saveModelPath, "NsfwSpyModel.zip"); mlContext.Model.Save(trainedModel, trainSet.Schema, saveModelPath); Console.WriteLine("Complete"); } public static IEnumerable LoadImagesFromDirectory(string folder) { var allowedFileExtensions = new[] { ".jpg", ".jpeg", ".png" }; var files = Directory.GetFiles(folder, "*", searchOption: SearchOption.AllDirectories); foreach (var file in files) { var extension = Path.GetExtension(file); if (!allowedFileExtensions.Contains(extension)) continue; var label = Directory.GetParent(file).Name; yield return new ImageData() { ImagePath = file, Label = label }; } } } } ================================================ FILE: NsfwSpy.Train/tfjs-convert-command.txt ================================================ tensorflowjs_converter --input_format=tf_frozen_model {input_file} {export_dir} --output_node_names=Score --skip_op_check ================================================ FILE: NsfwSpy.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.32228.430 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NsfwSpy", "NsfwSpy\NsfwSpy.csproj", "{B041BBD4-B1E3-4BCE-B06B-25146E38CE3E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NsfwSpy.Train", "NsfwSpy.Train\NsfwSpy.Train.csproj", "{C3F14E31-BDCD-4702-8370-5CD80C964621}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NsfwSpy.PerformanceTesting", "NsfwSpy.PerformanceTesting\NsfwSpy.PerformanceTesting.csproj", "{412E968C-E4D6-4CA1-9C75-8F78BE7275CD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NsfwSpy.Test", "NsfwSpy.Test\NsfwSpy.Test.csproj", "{3DE7054D-D7B5-4315-ADEF-5E4BA2D21138}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NsfwSpy.App", "NsfwSpy.App\NsfwSpy.App.csproj", "{025998A0-9590-4C70-89E5-311ADBBDCD92}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B041BBD4-B1E3-4BCE-B06B-25146E38CE3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B041BBD4-B1E3-4BCE-B06B-25146E38CE3E}.Debug|Any CPU.Build.0 = Debug|Any CPU {B041BBD4-B1E3-4BCE-B06B-25146E38CE3E}.Release|Any CPU.ActiveCfg = Release|Any CPU {B041BBD4-B1E3-4BCE-B06B-25146E38CE3E}.Release|Any CPU.Build.0 = Release|Any CPU {C3F14E31-BDCD-4702-8370-5CD80C964621}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C3F14E31-BDCD-4702-8370-5CD80C964621}.Debug|Any CPU.Build.0 = Debug|Any CPU {C3F14E31-BDCD-4702-8370-5CD80C964621}.Release|Any CPU.ActiveCfg = Release|Any CPU {C3F14E31-BDCD-4702-8370-5CD80C964621}.Release|Any CPU.Build.0 = Release|Any CPU {412E968C-E4D6-4CA1-9C75-8F78BE7275CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {412E968C-E4D6-4CA1-9C75-8F78BE7275CD}.Debug|Any CPU.Build.0 = Debug|Any CPU {412E968C-E4D6-4CA1-9C75-8F78BE7275CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {412E968C-E4D6-4CA1-9C75-8F78BE7275CD}.Release|Any CPU.Build.0 = Release|Any CPU {3DE7054D-D7B5-4315-ADEF-5E4BA2D21138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3DE7054D-D7B5-4315-ADEF-5E4BA2D21138}.Debug|Any CPU.Build.0 = Debug|Any CPU {3DE7054D-D7B5-4315-ADEF-5E4BA2D21138}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DE7054D-D7B5-4315-ADEF-5E4BA2D21138}.Release|Any CPU.Build.0 = Release|Any CPU {025998A0-9590-4C70-89E5-311ADBBDCD92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {025998A0-9590-4C70-89E5-311ADBBDCD92}.Debug|Any CPU.Build.0 = Debug|Any CPU {025998A0-9590-4C70-89E5-311ADBBDCD92}.Release|Any CPU.ActiveCfg = Release|Any CPU {025998A0-9590-4C70-89E5-311ADBBDCD92}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3F4B0CC-B514-4792-912E-F2A6052FD5E9} EndGlobalSection EndGlobal ================================================ FILE: README.md ================================================ NsfwSpy Logo # Introduction NsfwSpy is a nudity/pornography image and video classifier built for .NET Core 2.0 and later, with support for Windows, [macOS](#macos-support) and Linux, to aid in moderating user-generated content for various different application types, written in C#. The [ML.NET](https://github.com/dotnet/machinelearning) model has been trained against the ResNet V250 neural net architecture with 646,000 images (109GB), from 4 different categories: | Label | Description | Files | | ----------- | ----------- | ----- | | Pornography | Images that depict sexual acts and nudity. | 106,000 | | Sexy | Images of people in their underwear and men who are topless. | 78,000 | | Hentai | Drawings or animations of sexual acts and nudity. | 83,000 | | Neutral | Images that are not sexual in nature. | 378,000 | ### Other Projects Looking for a **JavaScript** version of NsfwSpy? We have you covered - **[NsfwSpy.js](https://github.com/d00ML0rDz/NsfwSpy.js)** 😎 # Performance NsfwSpy isn't perfect, but the accuracy should be good enough to detect approximately 96% of Nsfw images, those being images that are classed as pornography, sexy or hentai. | | Pornography | Sexy | Hentai | Neutral | | --- | --- | --- | --- | --- | | Is Nsfw (pornography + sexy + hentai >= 0.5) | 95.8% | 97.0% | 95.2% | 3.7% | | Correctly Predicted Label | 85.7% | 84.4% | 91.9% | 96.54% | # Quick Start Looking to quickly try out NsfwSpy? Check out our steps to use [NsfwSpy.App](https://github.com/d00ML0rDz/NsfwSpy/tree/main/NsfwSpy.App). This project is available as a [NuGet](https://www.nuget.org/packages/NsfwSpy/) package and can be installed with the following commands: **Package Manager** ``` Install-Package NsfwSpy ``` **.NET CLI** ``` dotnet add package NsfwSpy ``` ### Classify an Image File ```csharp var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyImage(@"C:\Users\username\Documents\flower.jpg"); ``` ### Classify a Web Image ```csharp var uri = new Uri("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.jpg"); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyImage(uri); ``` ### Classify an Image from a Byte Array ```csharp var fileBytes = File.ReadAllBytes(filePath); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyImage(fileBytes); ``` ### Classify Multiple Image Files ```csharp var files = Directory.GetFiles(@"C:\Users\username\Pictures"); var nsfwSpy = new NsfwSpy(); var results = nsfwSpy.ClassifyImages(files, (filePath, result) => { Console.WriteLine($"{filePath} - {result.PredictedLabel}"); }); ``` ### Classify a Gif File ```csharp var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyGif(@"C:\Users\username\Documents\happy.gif"); ``` ### Classify a Web Gif ```csharp var uri = new Uri("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/cool.gif"); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyGif(uri); ``` ### Classify a Video File ```csharp var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyVideo(@"C:\Users\username\Documents\happy.mp4"); ``` ### Classify a Web Video ```csharp var uri = new Uri("https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/bikini.mp4"); var nsfwSpy = new NsfwSpy(); var result = nsfwSpy.ClassifyVideo(uri); ``` ### Dependency Injection ```csharp services.AddScoped(); ``` # Classify Video Support To be able to make use of the ClassifyVideo methods, [FFmpeg](https://www.ffmpeg.org/) needs to be installed and available in the command line via the 'ffmpeg' command. ## Windows Follow [this guide](https://www.geeksforgeeks.org/how-to-install-ffmpeg-on-windows/) to download FFmpeg, extract it to your C:\ drive and add the required environment path variable. ## macOS Install FFmpeg on macOS using [Homebrew](https://brew.sh/) via the following command: ``` brew install ffmpeg ``` ## Ubuntu Install FFmpeg on Ubuntu using the following command: ``` sudo apt install ffmpeg ``` # GPU Support To get GPU support working, please follow the prerequisite steps [here](https://docs.microsoft.com/en-us/dotnet/api/microsoft.ml.vision.imageclassificationtrainer?view=ml-dotnet&fbclid=IwAR3Ng6Pe1BWDZ3hR20tchutSozmdMojxvpy3pqdwA3fZ_OEstU8C-ptSRZw#gpu-support) to install [CUDA v10.1](https://developer.nvidia.com/cuda-10.1-download-archive-update2) and [CUDNN v7.6.4 for CUDA 10.1](https://developer.nvidia.com/rdp/cudnn-archive). Later versions do not work (as I tried with CUDA v11.4). The SciSharp.TensorFlow.Redist-Windows-GPU and SciSharp.TensorFlow.Redist-Linux-GPU packages are already included as part of the NsfwSpy package. # macOS Support To get NsfwSpy working on macOS, the [SciSharp.TensorFlow.Redist v2.3.1](https://www.nuget.org/packages/SciSharp.TensorFlow.Redist/2.3.1) NuGet package also needs to be installed. This not included by default as it interfers with supporting GPUs on Windows and Linux. You can do this with either of the following commands: **Package Manager** ``` Install-Package SciSharp.TensorFlow.Redist -Version 2.3.1 ``` **.NET CLI** ``` dotnet add package SciSharp.TensorFlow.Redist --version 2.3.1 ``` Please note that Macs that use M1 chips currently [do not support TensorFlow](https://github.com/dotnet/machinelearning/blob/main/docs/project-docs/platform-limitations.md) with ML.NET and cannot make use of NsfwSpy. # Contact Us Interested to get involved in the project? Whether you fancy adding features, providing images to train NsfwSpy with or something else, feel free to contact us via email at [nsfwspy@outlook.com](mailto:nsfwspy@outlook.com) or find us on Twitter at [@nsfw_spy](https://twitter.com/nsfw_spy). # Notes Using NsfwSpy? Let us know! We're keen to hear how the technology is being used and improving the safety of applications. Got a feature request or found something not quite right? Report it [here](https://github.com/d00ML0rDz/NsfwSpy/issues) on GitHub and we'll try to help as best as possible.