[
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n/NsfwSpy.Train/Models\n/NsfwSpy.Train/Workspace\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 d00ML0rDz\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "NsfwSpy/INsfwSpy.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Threading.Tasks;\n\nnamespace NsfwSpyNS\n{\n    public interface INsfwSpy\n    {\n        NsfwSpyFramesResult ClassifyGif(byte[] gifImage, VideoOptions videoOptions = null);\n        NsfwSpyFramesResult ClassifyGif(string filePath, VideoOptions videoOptions = null);\n        NsfwSpyFramesResult ClassifyGif(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null);\n        Task<NsfwSpyFramesResult> ClassifyGifAsync(string filePath, VideoOptions videoOptions = null);\n        Task<NsfwSpyFramesResult> ClassifyGifAsync(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null);\n        NsfwSpyResult ClassifyImage(byte[] imageData);\n        NsfwSpyResult ClassifyImage(string filePath);\n        NsfwSpyResult ClassifyImage(Uri uri, WebClient webClient = null);\n        Task<NsfwSpyResult> ClassifyImageAsync(string filePath);\n        Task<NsfwSpyResult> ClassifyImageAsync(Uri uri, WebClient webClient = null);\n        List<NsfwSpyValue> ClassifyImages(IEnumerable<string> filesPaths, Action<string, NsfwSpyResult> actionAfterEachClassify = null);\n        NsfwSpyFramesResult ClassifyVideo(byte[] video, VideoOptions videoOptions = null);\n        NsfwSpyFramesResult ClassifyVideo(string filePath, VideoOptions videoOptions = null);\n        NsfwSpyFramesResult ClassifyVideo(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null);\n        Task<NsfwSpyFramesResult> ClassifyVideoAsync(string filePath, VideoOptions videoOptions = null);\n        Task<NsfwSpyFramesResult> ClassifyVideoAsync(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null);\n    }\n}"
  },
  {
    "path": "NsfwSpy/NsfwSpy.cs",
    "content": "﻿using HeyRed.Mime;\nusing ImageMagick;\nusing Microsoft.ML;\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Threading.Tasks;\n\nnamespace NsfwSpyNS\n{\n    /// <summary>\n    /// The NsfwSpy classifier class used to analyse images for explicit content.\n    /// </summary>\n    public class NsfwSpy : INsfwSpy\n    {\n        private static ITransformer _model;\n\n        public NsfwSpy()\n        {\n            if (_model == null)\n            {\n                var modelPath = Path.Combine(AppContext.BaseDirectory, \"NsfwSpyModel.zip\");\n                var mlContext = new MLContext();\n                _model = mlContext.Model.Load(modelPath, out var modelInputSchema);\n            }\n        }\n\n        /// <summary>\n        /// Classify an image from a byte array.\n        /// </summary>\n        /// <param name=\"imageData\">The image content read as a byte array.</param>\n        /// <returns>A NsfwSpyResult that indicates the predicted value and scores for the 5 categories of classification.</returns>\n        public NsfwSpyResult ClassifyImage(byte[] imageData)\n        {\n            var fileType = MimeGuesser.GuessFileType(imageData);\n            if (fileType.Extension == \"webp\")\n            {\n                using (MagickImage image = new MagickImage(imageData))\n                {\n                    imageData = image.ToByteArray(MagickFormat.Png);\n                }\n            }\n\n            var modelInput = new ModelInput(imageData);\n            var mlContext = new MLContext();\n            var predictionEngine = mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(_model);\n            var modelOutput = predictionEngine.Predict(modelInput);\n            return new NsfwSpyResult(modelOutput);\n        }\n\n        /// <summary>\n        /// Classify an image from a file path.\n        /// </summary>\n        /// <param name=\"filePath\">Path to the image to be classified.</param>\n        /// <returns>A NsfwSpyResult that indicates the predicted value and scores for the 5 categories of classification.</returns>\n        public NsfwSpyResult ClassifyImage(string filePath)\n        {\n            var fileBytes = File.ReadAllBytes(filePath);\n            var result = ClassifyImage(fileBytes);\n            return result;\n        }\n\n        /// <summary>\n        /// Classify an image from a web url.\n        /// </summary>\n        /// <param name=\"uri\">Web address of the image to be classified.</param>\n        /// <param name=\"webClient\">A custom WebClient to download the image with.</param>\n        /// <returns>A NsfwSpyResult that indicates the predicted value and scores for the 5 categories of classification.</returns>\n        public NsfwSpyResult ClassifyImage(Uri uri, WebClient webClient = null)\n        {\n            if (webClient == null) webClient = new WebClient();\n\n            var fileBytes = webClient.DownloadData(uri);\n            var result = ClassifyImage(fileBytes);\n            return result;\n        }\n\n        /// <summary>\n        /// Classify an image from a file path asynchronously.\n        /// </summary>\n        /// <param name=\"filePath\">Path to the image to be classified.</param>\n        /// <returns>A NsfwSpyResult that indicates the predicted value and scores for the 5 categories of classification.</returns>\n        public async Task<NsfwSpyResult> ClassifyImageAsync(string filePath)\n        {\n            var fileBytes = await File.ReadAllBytesAsync(filePath);\n            var result = ClassifyImage(fileBytes);\n            return result;\n        }\n\n        /// <summary>\n        /// Classify an image from a web url asynchronously.\n        /// </summary>\n        /// <param name=\"uri\">Web address of the image to be classified.</param>\n        /// <param name=\"webClient\">A custom WebClient to download the image with.</param>\n        /// <returns>A NsfwSpyResult that indicates the predicted value and scores for the 5 categories of classification.</returns>\n        public async Task<NsfwSpyResult> ClassifyImageAsync(Uri uri, WebClient webClient = null)\n        {\n            if (webClient == null) webClient = new WebClient();\n\n            var fileBytes = await webClient.DownloadDataTaskAsync(uri);\n            var result = ClassifyImage(fileBytes);\n            return result;\n        }\n\n        /// <summary>\n        /// Classify multiple images from a list of file paths.\n        /// </summary>\n        /// <param name=\"filesPaths\">Collection of file paths to be classified.</param>\n        /// <param name=\"actionAfterEachClassify\">Action to be invoked after each file is classified.</param>\n        /// <returns>A list of results with their associated file paths.</returns>\n        public List<NsfwSpyValue> ClassifyImages(IEnumerable<string> filesPaths, Action<string, NsfwSpyResult> actionAfterEachClassify = null)\n        {\n            var results = new ConcurrentBag<NsfwSpyValue>();\n            var sync = new object();\n\n            Parallel.ForEach(filesPaths, filePath =>\n            {\n                var result = ClassifyImage(filePath);\n                var value = new NsfwSpyValue(filePath, result);\n                results.Add(value);\n\n                lock (sync)\n                {\n                    if (actionAfterEachClassify != null)\n                        actionAfterEachClassify.Invoke(filePath, result);\n                }\n            });\n\n            return results.ToList();\n        }\n\n        /// <summary>\n        /// Classify a .gif file from a byte array.\n        /// </summary>\n        /// <param name=\"gifImage\">The Gif content read as a byte array.</param>\n        /// <param name=\"videoOptions\">VideoOptions to customise how the frames of the file are classified.</param>\n        /// <returns>A NsfwSpyFramesResult with results for each frame classified.</returns>\n        public NsfwSpyFramesResult ClassifyGif(byte[] gifImage, VideoOptions videoOptions = null)\n        {\n            if (videoOptions == null)\n                videoOptions = new VideoOptions();\n\n            if (videoOptions.ClassifyEveryNthFrame < 1)\n                throw new Exception(\"VideoOptions.ClassifyEveryNthFrame must not be less than 1.\");\n\n            var results = new ConcurrentDictionary<int, NsfwSpyResult>();\n\n            using (var collection = new MagickImageCollection(gifImage))\n            {\n                collection.Coalesce();\n                var frameCount = collection.Count;\n\n                Parallel.For(0, frameCount, (i, state) =>\n                {\n                    if (i % videoOptions.ClassifyEveryNthFrame != 0)\n                        return;\n\n                    if (state.ShouldExitCurrentIteration)\n                        return;\n\n                    var frame = collection[i];\n                    var frameData = frame.ToByteArray();\n                    var result = ClassifyImage(frameData);\n                    results.GetOrAdd(i, result);\n\n                    // Stop classifying frames if Nsfw frame is found\n                    if (result.IsNsfw && videoOptions.EarlyStopOnNsfw)\n                        state.Break();\n                });\n            }\n\n            var resultDictionary = results.OrderBy(r => r.Key).ToDictionary(r => r.Key, r => r.Value);\n            var gifResult = new NsfwSpyFramesResult(resultDictionary);\n            return gifResult;\n        }\n\n        /// <summary>\n        /// Classify a .gif file from a path.\n        /// </summary>\n        /// <param name=\"filePath\">Path to the .gif to be classified.</param>\n        /// <param name=\"videoOptions\">VideoOptions to customise how the frames of the file are classified.</param>\n        /// <returns>A NsfwSpyFramesResult with results for each frame classified.</returns>\n        public NsfwSpyFramesResult ClassifyGif(string filePath, VideoOptions videoOptions = null)\n        {\n            var gifImage = File.ReadAllBytes(filePath);\n            var results = ClassifyGif(gifImage, videoOptions);\n            return results;\n        }\n\n        /// <summary>\n        /// Classify a .gif file  from a web url.\n        /// </summary>\n        /// <param name=\"uri\">Web address of the Gif to be classified.</param>\n        /// <param name=\"webClient\">A custom WebClient to download the Gif with.</param>\n        /// <param name=\"videoOptions\">VideoOptions to customise how the frames of the file are classified.</param>\n        /// <returns>A NsfwSpyFramesResult with results for each frame classified.</returns>\n        public NsfwSpyFramesResult ClassifyGif(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null)\n        {\n            if (webClient == null) webClient = new WebClient();\n\n            var gifImage = webClient.DownloadData(uri);\n            var results = ClassifyGif(gifImage, videoOptions);\n            return results;\n        }\n\n        /// <summary>\n        /// Classify a .gif file from a path asynchronously.\n        /// </summary>\n        /// <param name=\"filePath\">Path to the .gif to be classified.</param>\n        /// <param name=\"videoOptions\">VideoOptions to customise how the frames of the file are classified.</param>\n        /// <returns>A NsfwSpyFramesResult with results for each frame classified.</returns>\n        public async Task<NsfwSpyFramesResult> ClassifyGifAsync(string filePath, VideoOptions videoOptions = null)\n        {\n            var gifImage = await File.ReadAllBytesAsync(filePath);\n            var results = ClassifyGif(gifImage, videoOptions);\n            return results;\n        }\n\n        /// <summary>\n        /// Classify a .gif file  from a web url asynchronously.\n        /// </summary>\n        /// <param name=\"uri\">Web address of the Gif to be classified.</param>\n        /// <param name=\"webClient\">A custom WebClient to download the Gif with.</param>\n        /// <param name=\"videoOptions\">VideoOptions to customise how the frames of the file are classified.</param>\n        /// <returns>A NsfwSpyFramesResult with results for each frame classified.</returns>\n        public async Task<NsfwSpyFramesResult> ClassifyGifAsync(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null)\n        {\n            if (webClient == null) webClient = new WebClient();\n\n            var gifImage = await webClient.DownloadDataTaskAsync(uri);\n            var results = ClassifyGif(gifImage, videoOptions);\n            return results;\n        }\n\n        /// <summary>\n        /// Classify a video file from a byte array.\n        /// </summary>\n        /// <param name=\"video\">The video content read as a byte array.</param>\n        /// <param name=\"videoOptions\">VideoOptions to customise how the frames of the file are classified.</param>\n        /// <returns>A NsfwSpyFramesResult with results for each frame classified.</returns>\n        public NsfwSpyFramesResult ClassifyVideo(byte[] video, VideoOptions videoOptions = null)\n        {\n            if (videoOptions == null)\n                videoOptions = new VideoOptions();\n\n            if (videoOptions.ClassifyEveryNthFrame < 1)\n                throw new Exception(\"VideoOptions.ClassifyEveryNthFrame must not be less than 1.\");\n\n            var results = new ConcurrentDictionary<int, NsfwSpyResult>();\n\n            using (var collection = new MagickImageCollection(video, MagickFormat.Mp4))\n            {\n                collection.Coalesce();\n                var frameCount = collection.Count;\n\n                Parallel.For(0, frameCount, (i, state) =>\n                {\n                    if (i % videoOptions.ClassifyEveryNthFrame != 0)\n                        return;\n\n                    if (state.ShouldExitCurrentIteration)\n                        return;\n\n                    var frame = collection[i];\n                    frame.Format = MagickFormat.Jpg;\n\n                    var result = ClassifyImage(frame.ToByteArray());\n                    results.GetOrAdd(i, result);\n\n                    // Stop classifying frames if Nsfw frame is found\n                    if (result.IsNsfw && videoOptions.EarlyStopOnNsfw)\n                        state.Break();\n                });\n            }\n\n            var resultDictionary = results.OrderBy(r => r.Key).ToDictionary(r => r.Key, r => r.Value);\n            var gifResult = new NsfwSpyFramesResult(resultDictionary);\n            return gifResult;\n        }\n\n        /// <summary>\n        /// Classify a .gif file from a path.\n        /// </summary>\n        /// <param name=\"filePath\">Path to the video to be classified.</param>\n        /// <param name=\"videoOptions\">VideoOptions to customise how the frames of the file are classified.</param>\n        /// <returns>A NsfwSpyFramesResult with results for each frame classified.</returns>\n        public NsfwSpyFramesResult ClassifyVideo(string filePath, VideoOptions videoOptions = null)\n        {\n            var video = File.ReadAllBytes(filePath);\n            var results = ClassifyVideo(video, videoOptions);\n            return results;\n        }\n\n        /// <summary>\n        /// Classify a .gif file  from a web url.\n        /// </summary>\n        /// <param name=\"uri\">Web address of the video to be classified.</param>\n        /// <param name=\"webClient\">A custom WebClient to download the video with.</param>\n        /// <param name=\"videoOptions\">VideoOptions to customise how the frames of the file are classified.</param>\n        /// <returns>A NsfwSpyFramesResult with results for each frame classified.</returns>\n        public NsfwSpyFramesResult ClassifyVideo(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null)\n        {\n            if (webClient == null) webClient = new WebClient();\n\n            var video = webClient.DownloadData(uri);\n            var results = ClassifyVideo(video, videoOptions);\n            return results;\n        }\n\n        /// <summary>\n        /// Classify a .gif file from a path asynchronously.\n        /// </summary>\n        /// <param name=\"filePath\">Path to the video to be classified.</param>\n        /// <param name=\"videoOptions\">VideoOptions to customise how the frames of the file are classified.</param>\n        /// <returns>A NsfwSpyFramesResult with results for each frame classified.</returns>\n        public async Task<NsfwSpyFramesResult> ClassifyVideoAsync(string filePath, VideoOptions videoOptions = null)\n        {\n            var video = await File.ReadAllBytesAsync(filePath);\n            var results = ClassifyVideo(video, videoOptions);\n            return results;\n        }\n\n        /// <summary>\n        /// Classify a .gif file  from a web url asynchronously.\n        /// </summary>\n        /// <param name=\"uri\">Web address of the video to be classified.</param>\n        /// <param name=\"webClient\">A custom WebClient to download the video with.</param>\n        /// <param name=\"videoOptions\">VideoOptions to customise how the frames of the file are classified.</param>\n        /// <returns>A NsfwSpyFramesResult with results for each frame classified.</returns>\n        public async Task<NsfwSpyFramesResult> ClassifyVideoAsync(Uri uri, WebClient webClient = null, VideoOptions videoOptions = null)\n        {\n            if (webClient == null) webClient = new WebClient();\n\n            var video = await webClient.DownloadDataTaskAsync(uri);\n            var results = ClassifyVideo(video, videoOptions);\n            return results;\n        }\n    }\n}\n"
  },
  {
    "path": "NsfwSpy/NsfwSpy.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>netcoreapp2.0</TargetFramework>\n    <Authors>NsfwSpy</Authors>\n    <Company>NsfwSpy</Company>\n    <PackageTags>nsfw nude nudity porn pornography explicit image detect detection classification classifier machinglearning ml.net</PackageTags>\n    <Description>NsfwSpy is an image and video classifier used to identify explicit/pornographic content using machine learning.</Description>\n    <RepositoryUrl>https://github.com/NsfwSpy/NsfwSpy.NET</RepositoryUrl>\n    <PackageProjectUrl>https://github.com/NsfwSpy/NsfwSpy.NET</PackageProjectUrl>\n    <PackageIcon>NsfwSpy-Icon.png</PackageIcon>\n    <PackageIconUrl />\n    <PackageLicenseExpression></PackageLicenseExpression>\n    <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>\n    <PackageLicenseFile>LICENSE</PackageLicenseFile>\n    <Version>3.5.0</Version>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Magick.NET-Q16-AnyCPU\" Version=\"11.1.2\" />\n    <PackageReference Include=\"Microsoft.ML\" Version=\"1.6.0\" />\n    <PackageReference Include=\"Microsoft.ML.Vision\" Version=\"1.6.0\" />\n    <PackageReference Include=\"Mime\" Version=\"3.4.0\" />\n    <PackageReference Include=\"SciSharp.TensorFlow.Redist-Linux-GPU\" Version=\"2.3.1\" />\n    <PackageReference Include=\"SciSharp.TensorFlow.Redist-Windows-GPU\" Version=\"2.3.1\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Include=\"..\\LICENSE\">\n      <Pack>True</Pack>\n      <PackagePath></PackagePath>\n    </None>\n    <None Include=\"NsfwSpy-Icon.png\">\n      <Pack>True</Pack>\n      <PackagePath></PackagePath>\n    </None>\n    <None Include=\"NsfwSpy-Icon.png\">\n      <Pack>True</Pack>\n      <PackagePath></PackagePath>\n    </None>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"NsfwSpyModel.zip\">\n      <Pack>true</Pack>\n      <CopyToOutputDirectory>Always</CopyToOutputDirectory>\n      <PackageCopyToOutput>true</PackageCopyToOutput>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "NsfwSpy/Resources/ClassificationFailedException.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Text;\n\nnamespace NsfwSpyNS\n{\n    public class ClassificationFailedException : Exception\n    {\n        public ClassificationFailedException(string message)\n            : base(message)\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "NsfwSpy/Resources/EClassificationType.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace NsfwSpyNS\n{\n    internal enum EClassificationType\n    {\n        Hentai = 0,\n        Neutral = 1,\n        Pornography = 2,\n        Sexy = 3\n    }\n}\n"
  },
  {
    "path": "NsfwSpy/Resources/ModelInput.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace NsfwSpyNS\n{\n    class ModelInput\n    {\n        public ModelInput(byte[] image)\n        {\n            Image = image;\n        }\n\n        public byte[] Image { get; set; }\n    }\n}\n"
  },
  {
    "path": "NsfwSpy/Resources/ModelOutput.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace NsfwSpyNS\n{\n    class ModelOutput\n    {\n        public string PredictedLabel { get; set; }\n        public float[] Score { get; set; }\n    }\n}\n"
  },
  {
    "path": "NsfwSpy/Resources/NsfwSpyFramesResult.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\n\nnamespace NsfwSpyNS\n{\n    /// <summary>\n    /// The result from classifying a Gif file.\n    /// </summary>\n    public class NsfwSpyFramesResult\n    {\n        /// <summary>\n        /// The NsfwSpyResults for each of the frames classified with the key being the frame index.\n        /// </summary>\n        public Dictionary<int, NsfwSpyResult> Frames { get; set; }\n\n        /// <summary>\n        /// The amount of frames classified.\n        /// </summary>\n        public int FrameCount => Frames.Count;\n\n        /// <summary>\n        /// True if any of the frames have been classified as NSFW.\n        /// </summary>\n        public bool IsNsfw => Frames.Any(f => f.Value.IsNsfw);\n\n        public NsfwSpyFramesResult(Dictionary<int, NsfwSpyResult> frames)\n        {\n            Frames = frames;\n        }\n    }\n}\n"
  },
  {
    "path": "NsfwSpy/Resources/NsfwSpyResult.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace NsfwSpyNS\n{\n    /// <summary>\n    /// The result from classifying an image.\n    /// </summary>\n    public class NsfwSpyResult\n    {\n        /// <summary>\n        /// The hentai probability score between 0 and 1.\n        /// </summary>\n        public float Hentai { get; }\n\n        /// <summary>\n        /// The neutral probability score between 0 and 1.\n        /// </summary>\n        public float Neutral { get; }\n\n        /// <summary>\n        /// The pornography probability score between 0 and 1.\n        /// </summary>\n        public float Pornography { get; }\n\n        /// <summary>\n        /// The sexy probability score between 0 and 1.\n        /// </summary>\n        public float Sexy { get; }\n\n        /// <summary>\n        /// The most likely predicted value.\n        /// </summary>\n        public string PredictedLabel { get; }\n\n        /// <summary>\n        /// Whether the image is likely to be explicit. True if the sum of pornography, hentai and sexy is equal to or above 0.5.\n        /// </summary>\n        public bool IsNsfw => Neutral < 0.5;\n\n        public NsfwSpyResult()\n        {\n\n        }\n\n        internal NsfwSpyResult(ModelOutput modelOutput)\n        {\n            Hentai = modelOutput.Score[(int)EClassificationType.Hentai];\n            Neutral = modelOutput.Score[(int)EClassificationType.Neutral];\n            Pornography = modelOutput.Score[(int)EClassificationType.Pornography];\n            Sexy = modelOutput.Score[(int)EClassificationType.Sexy];\n            PredictedLabel = modelOutput.PredictedLabel;\n\n            if (Hentai + Neutral + Pornography + Sexy == 0)\n            {\n                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.\");\n            }\n        }\n\n        /// <summary>\n        /// Get the 5 classification types as a dictionary ordered by their score.\n        /// </summary>\n        /// <returns>Dictionary of the prediction scores.</returns>\n        public Dictionary<string, float> ToDictionary()\n        {\n            var dictionary = new Dictionary<string, float>\n            {\n                { \"Hentai\", Hentai },\n                { \"Neutral\", Neutral },\n                { \"Pornography\", Pornography },\n                { \"Sexy\", Sexy }\n            };\n\n            return dictionary.OrderByDescending(p => p.Value).ToDictionary(x => x.Key, x => x.Value); ;\n        }\n    }\n}\n"
  },
  {
    "path": "NsfwSpy/Resources/NsfwSpyValue.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace NsfwSpyNS\n{\n    public class NsfwSpyValue\n    {\n        public string FilePath { get; }\n        public NsfwSpyResult Result { get; }\n\n        public NsfwSpyValue(string filePath, NsfwSpyResult result)\n        {\n            FilePath = filePath;\n            Result = result;\n        }\n    }\n}\n"
  },
  {
    "path": "NsfwSpy/Resources/VideoOptions.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Text;\n\nnamespace NsfwSpyNS\n{\n    /// <summary>\n    /// Customise how the frames of a video file are classified.\n    /// </summary>\n    public class VideoOptions\n    {\n        /// <summary>\n        /// Stop classifying frames if a NSFW frame is found.\n        /// </summary>\n        public bool EarlyStopOnNsfw { get; set; } = false;\n\n        /// <summary>\n        /// Improve performance by only classifying one in every Nth frames e.g. 1 = classify all frames, 2 = classify 1 in 2 frames.\n        /// </summary>\n        public int ClassifyEveryNthFrame { get; set; } = 1;\n    }\n}\n"
  },
  {
    "path": "NsfwSpy.App/Controllers/NsfwSpyController.cs",
    "content": "﻿using HeyRed.Mime;\nusing Microsoft.AspNetCore.Mvc;\nusing System.Web;\n\nnamespace NsfwSpyNS.App.Controllers\n{\n    [ApiController]\n    [Route(\"[controller]\")]\n    public class NsfwSpyController : ControllerBase\n    {\n        private INsfwSpy _nsfwSpy;\n\n        public NsfwSpyController(INsfwSpy nsfwSpy)\n        {\n            _nsfwSpy = nsfwSpy;\n        }\n\n        [HttpGet(\"url/{url}\")]\n        public async Task<FileContentResult> GetUrlMediaAsync(string url)\n        {\n            url = HttpUtility.UrlDecode(url);\n            var httpClient = new HttpClient();\n            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\");\n            var byteArray = await httpClient.GetByteArrayAsync(url);\n            var mimeType = MimeGuesser.GuessMimeType(byteArray);\n            return new FileContentResult(byteArray, mimeType);\n        }\n\n        [HttpPost(\"image\")]\n        public ActionResult<NsfwSpyResult> ClassifyImage(IFormFile file)\n        {\n            var fileBytes = ConvertFormFileToByteArray(file);\n            var result = _nsfwSpy.ClassifyImage(fileBytes);\n            return Ok(result);\n        }\n\n        [HttpPost(\"gif\")]\n        public ActionResult<NsfwSpyFramesResult> ClassifyGif(IFormFile file)\n        {\n            var fileBytes = ConvertFormFileToByteArray(file);\n            var videoOptions = new VideoOptions\n            {\n                EarlyStopOnNsfw = false\n            };\n            var result = _nsfwSpy.ClassifyGif(fileBytes, videoOptions);\n            return Ok(result);\n        }\n\n        [HttpPost(\"video\")]\n        public ActionResult<NsfwSpyFramesResult> ClassifyVideo(IFormFile file)\n        {\n            var fileBytes = ConvertFormFileToByteArray(file);\n            var videoOptions = new VideoOptions\n            {\n                EarlyStopOnNsfw = false\n            };\n            var result = _nsfwSpy.ClassifyVideo(fileBytes, videoOptions);\n            return Ok(result);\n        }\n\n        private byte[] ConvertFormFileToByteArray(IFormFile file)\n        {\n            using (var ms = new MemoryStream())\n            {\n                file.CopyTo(ms);\n                var fileBytes = ms.ToArray();\n                return fileBytes;\n            }\n        }\n    }\n\n    public class MediaInfo\n    {\n        public IFormFile File { get; set; }\n        public string MimeType { get; set; }\n    }\n}"
  },
  {
    "path": "NsfwSpy.App/NsfwSpy.App.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFramework>net6.0</TargetFramework>\n    <Nullable>enable</Nullable>\n    <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>\n    <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>\n    <IsPackable>false</IsPackable>\n    <SpaRoot>client-app\\</SpaRoot>\n    <DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\\**</DefaultItemExcludes>\n    <SpaProxyServerUrl>https://localhost:3000</SpaProxyServerUrl>\n    <SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>\n    <ImplicitUsings>enable</ImplicitUsings>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.AspNetCore.SpaProxy\" Version=\"6.0.5\" />\n    <PackageReference Include=\"Microsoft.TypeScript.MSBuild\" Version=\"4.7.2\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"Mime\" Version=\"3.4.0\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <!-- Don't publish the SPA source files, but do show them in the project files list -->\n    <Content Remove=\"$(SpaRoot)**\" />\n    <None Remove=\"$(SpaRoot)**\" />\n    <None Include=\"$(SpaRoot)**\" Exclude=\"$(SpaRoot)node_modules\\**\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Remove=\"client-app\\src\\aspnetcore-https.ts\" />\n    <None Remove=\"client-app\\src\\aspnetcore-react.ts\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <TypeScriptCompile Remove=\"client-app\\src\\functions\\getContentType.ts\" />\n    <TypeScriptCompile Remove=\"client-app\\src\\functions\\sortBy.ts\" />\n    <TypeScriptCompile Remove=\"client-app\\src\\models\\MediaInfo.ts\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\NsfwSpy\\NsfwSpy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <TypeScriptCompile Include=\"client-app\\src\\aspnetcore-https.ts\" />\n    <TypeScriptCompile Include=\"client-app\\src\\aspnetcore-react.ts\" />\n  </ItemGroup>\n\n  <Target Name=\"DebugEnsureNodeEnv\" BeforeTargets=\"Build\" Condition=\" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') \">\n    <!-- Ensure Node.js is installed -->\n    <Exec Command=\"node --version\" ContinueOnError=\"true\">\n      <Output TaskParameter=\"ExitCode\" PropertyName=\"ErrorCode\" />\n    </Exec>\n    <Error Condition=\"'$(ErrorCode)' != '0'\" Text=\"Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE.\" />\n    <Message Importance=\"high\" Text=\"Restoring dependencies using 'npm'. This may take several minutes...\" />\n    <Exec WorkingDirectory=\"$(SpaRoot)\" Command=\"npm install\" />\n  </Target>\n\n  <Target Name=\"PublishRunWebpack\" AfterTargets=\"ComputeFilesToPublish\">\n    <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->\n    <Exec WorkingDirectory=\"$(SpaRoot)\" Command=\"npm install\" />\n    <Exec WorkingDirectory=\"$(SpaRoot)\" Command=\"npm run build\" />\n\n    <!-- Include the newly-built files in the publish output -->\n    <ItemGroup>\n      <DistFiles Include=\"$(SpaRoot)build\\**\" />\n      <ResolvedFileToPublish Include=\"@(DistFiles->'%(FullPath)')\" Exclude=\"@(ResolvedFileToPublish)\">\n        <RelativePath>wwwroot\\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>\n        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>\n        <ExcludeFromSingleFile>true</ExcludeFromSingleFile>\n      </ResolvedFileToPublish>\n    </ItemGroup>\n  </Target>\n</Project>\n"
  },
  {
    "path": "NsfwSpy.App/Pages/Error.cshtml",
    "content": "﻿@page\n@model ErrorModel\n@{\n    ViewData[\"Title\"] = \"Error\";\n}\n\n<h1 class=\"text-danger\">Error.</h1>\n<h2 class=\"text-danger\">An error occurred while processing your request.</h2>\n\n@if (Model.ShowRequestId)\n{\n    <p>\n        <strong>Request ID:</strong> <code>@Model.RequestId</code>\n    </p>\n}\n\n<h3>Development Mode</h3>\n<p>\n    Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.\n</p>\n<p>\n    <strong>The Development environment shouldn't be enabled for deployed applications.</strong>\n    It can result in displaying sensitive information from exceptions to end users.\n    For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>\n    and restarting the app.\n</p>\n"
  },
  {
    "path": "NsfwSpy.App/Pages/Error.cshtml.cs",
    "content": "using Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.RazorPages;\nusing System.Diagnostics;\n\nnamespace NsfwSpyNS.App.Pages\n{\n    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]\n    public class ErrorModel : PageModel\n    {\n        private readonly ILogger<ErrorModel> _logger;\n\n        public ErrorModel(ILogger<ErrorModel> logger)\n        {\n            _logger = logger;\n        }\n\n        public string? RequestId { get; set; }\n\n        public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);\n\n        public void OnGet()\n        {\n            RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;\n        }\n    }\n}"
  },
  {
    "path": "NsfwSpy.App/Pages/_ViewImports.cshtml",
    "content": "﻿@using NsfwSpyNS.App\n@namespace NsfwSpyNS.App.Pages\n@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers\n"
  },
  {
    "path": "NsfwSpy.App/Program.cs",
    "content": "using NsfwSpyNS;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// Add services to the container.\n\nbuilder.Services.AddControllersWithViews();\nbuilder.Services.AddSingleton<INsfwSpy, NsfwSpy>();\n\nvar app = builder.Build();\n\n// Configure the HTTP request pipeline.\nif (!app.Environment.IsDevelopment())\n{\n    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.\n    app.UseHsts();\n}\n\napp.UseHttpsRedirection();\napp.UseStaticFiles();\napp.UseRouting();\n\n\napp.MapControllerRoute(\n    name: \"default\",\n    pattern: \"{controller}/{action=Index}/{id?}\");\n\napp.MapFallbackToFile(\"index.html\"); ;\n\napp.Run();\n"
  },
  {
    "path": "NsfwSpy.App/Properties/launchSettings.json",
    "content": "﻿{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:57025\",\n      \"sslPort\": 44385\n    }\n  },\n  \"profiles\": {\n    \"NsfwSpy.App\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"https://localhost:7260;http://localhost:5260\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES\": \"Microsoft.AspNetCore.SpaProxy\"\n      }\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES\": \"Microsoft.AspNetCore.SpaProxy\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "NsfwSpy.App/README.md",
    "content": "# Getting Started\nLooking for a quick way to try out NsfwSpy? Use our dedicated, self-hosted web app to classify images, gifs and videos with ease.\n\n1. Check you have [NodeJS](https://nodejs.org/) installed by using the following command in the terminal:\n  ```\n  node -v\n  ```\n2. Clone the git repository to your local machine.\n3. Open the NsfwSpy.sln solution in Visual Studio or your preferred IDE.\n4. Run the NsfwSpy.App project using IIS Express:\n<img src=\"https://raw.githubusercontent.com/NsfwSpy/NsfwSpy.NET/main/_art/NsfwSpy.App step 3.jpg\" alt=\"NsfwSpy App config\" />\n5. Upload your images and videos or paste a link to have them classified. \n\n<img src=\"https://raw.githubusercontent.com/NsfwSpy/NsfwSpy.NET/main/_art/NsfwSpy.App.gif\" alt=\"NsfwSpy App\" width=\"400\" />\n"
  },
  {
    "path": "NsfwSpy.App/appsettings.json",
    "content": "{\n  \"Logging\": {\n      \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    },\n\"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "NsfwSpy.App/client-app/.gitignore",
    "content": "﻿.DS_STORE\nnode_modules\nscripts/flow/*/.flowconfig\n.flowconfig\n*~\n*.pyc\n.grunt\n_SpecRunner.html\n__benchmarks__\nbuild/\nremote-repo/\ncoverage/\n.module-cache\nfixtures/dom/public/react-dom.js\nfixtures/dom/public/react.js\ntest/the-files-to-test.generated.js\n*.log*\nchrome-user-data\n*.sublime-project\n*.sublime-workspace\n.idea\n*.iml\n.vscode\n*.swp\n*.swo\n\npackages/react-devtools-core/dist\npackages/react-devtools-extensions/chrome/build\npackages/react-devtools-extensions/chrome/*.crx\npackages/react-devtools-extensions/chrome/*.pem\npackages/react-devtools-extensions/firefox/build\npackages/react-devtools-extensions/firefox/*.xpi\npackages/react-devtools-extensions/firefox/*.pem\npackages/react-devtools-extensions/shared/build\npackages/react-devtools-extensions/.tempUserDataDir\npackages/react-devtools-inline/dist\npackages/react-devtools-shell/dist\npackages/react-devtools-timeline/dist"
  },
  {
    "path": "NsfwSpy.App/client-app/package.json",
    "content": "{\n  \"name\": \"client-app\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"proxy\": \"https://localhost:44385\",\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@fortawesome/fontawesome-svg-core\": \"^6.1.1\",\n    \"@fortawesome/free-solid-svg-icons\": \"^6.1.1\",\n    \"@fortawesome/react-fontawesome\": \"^0.1.18\",\n    \"@testing-library/jest-dom\": \"^5.11.4\",\n    \"@testing-library/react\": \"^11.1.0\",\n    \"@testing-library/user-event\": \"^12.1.10\",\n    \"@types/jest\": \"^26.0.15\",\n    \"@types/react\": \"^17.0.0\",\n    \"@types/react-dom\": \"^17.0.0\",\n    \"node-sass\": \"^8.0.0\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"react-scripts\": \"^5.0.1\",\n    \"recharts\": \"^2.2.0\",\n    \"ts-node\": \"^10.4.0\",\n    \"typescript\": \"^4.1.2\",\n    \"web-vitals\": \"^1.0.1\"\n  },\n  \"scripts\": {\n    \"prestart\": \"node --loader ts-node/esm ./src/aspnetcore-https.ts && node --loader ts-node/esm ./src/aspnetcore-react.ts\",\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\"\n  },\n  \"eslintConfig\": {\n    \"extends\": [\n      \"react-app\",\n      \"react-app/jest\"\n    ]\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^16.11.36\"\n  }\n}\n"
  },
  {
    "path": "NsfwSpy.App/client-app/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\" />\n  <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n  <meta name=\"theme-color\" content=\"#000000\" />\n  <meta name=\"description\" content=\"NsfwSpy | Pornographic .NET image classifier\" />\n  <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n  <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n  <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n  <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n  <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n  <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n  <link href=\"https://fonts.googleapis.com/css2?family=Roboto:wght@900&display=swap\" rel=\"stylesheet\">\n  <title>NsfwSpy | Pornographic .NET image classifier</title>\n</head>\n\n<body>\n  <noscript>You need to enable JavaScript to run this app.</noscript>\n  <div id=\"root\"></div>\n  <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n</body>\n\n</html>"
  },
  {
    "path": "NsfwSpy.App/client-app/public/manifest.json",
    "content": "{\n  \"short_name\": \"NsfwSpy App\",\n  \"name\": \"NsfwSpy App\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"./assets/images/NsfwSpy-Icon.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"256x256\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "NsfwSpy.App/client-app/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "NsfwSpy.App/client-app/src/App.scss",
    "content": ".app {\n    color: #fff;\n    font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n}\n\nheader {\n    align-items: center;\n    background-color: #000000;\n    display: flex;\n    height: 80px;\n    justify-content: center;\n}\n\nmain {\n    background-color: #1b1b1b;\n    height: calc(100vh - 80px);\n    display: flex;\n    flex: 1 1 0;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n\n    .image-section {\n        flex-direction: column;\n    }\n    \n    section {\n        display: flex;\n        flex: 1;\n        flex-direction: column;\n        align-items: center;\n        max-width: 1000px;\n        padding: 16px 0;\n        width: calc(100% - 48px);\n    }\n}\n\n.image-canvas {\n    align-items: center;\n    border: dashed #fff 6px;\n    border-radius: 32px;\n    cursor: pointer;\n    display: flex;\n    flex-direction: column;\n    flex: 1 1 auto;\n    font-size: 18px;\n    justify-content: center;\n    margin: 16px;\n    padding: 16px;\n    height: 30vh;\n\n    .icons {\n        display: flex;\n        font-size: 56px;\n        margin: 16px 0;\n\n        div {\n            margin: 16px 16px 0;\n        }\n    }\n\n    .subtitle {\n        font-size: 16px;\n    }\n\n    .image-preview,\n    .video-preview {\n        height: 100%;\n        object-fit: contain;\n        width: 100%;\n    }\n}\n\n.result-value {\n    display: flex;\n    font-size: 18px;\n    margin: 8px 0;\n    text-transform: capitalize;\n    width: 300px;\n\n    &.hentai {\n        color: #c25452;\n    }\n\n    &.neutral {\n        color: #fff;\n    }\n\n    &.pornography {\n        color: #ffa31a;\n    }\n\n    &.sexy {\n        color: #fdfd66;\n    }\n\n    span {\n        flex: 50%;\n    }\n}\n\ninput[type=text] {\n    background: #292929;\n    border: 1px solid #111;\n    border-radius: 4px;\n    box-shadow: inset 0 1px 2px #111;\n    box-sizing: border-box;\n    color: #fff;\n    display: inline-block;\n    font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n    margin: 8px 0;\n    outline: none;\n    padding: 12px 20px;\n    width: 60%;\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/App.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faImage, faVideo } from '@fortawesome/free-solid-svg-icons'\nimport { Logo } from './components/Logo/Logo';\nimport { selectFiles } from './functions/selectFiles';\nimport { ImageFile } from './models/ImageFile';\nimport { getMediaInfo, uploadGif, uploadImage, uploadVideo } from './functions/client';\nimport { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';\nimport './App.scss';\nimport { NsfwSpyFramesResult } from './models/NsfwSpyFramesResult';\nimport { NsfwSpyResult } from './models/NsfwSpyResult';\nimport { sortNsfwResult } from './functions/sortBy';\nimport { getContentType } from './functions/getContentType';\nimport { MediaInfo } from './models/MediaInfo';\n\nexport const App: React.FC = () => {\n    const [url, setUrl] = useState<string>();\n    const [image, setImage] = useState<ImageFile>();\n    const [video, setVideo] = useState<ImageFile>();\n    const [imageResults, setImageResults] = useState<NsfwSpyResult>();\n    const [videoResults, setVideoResults] = useState<NsfwSpyFramesResult>();\n    const [processing, setProcessing] = useState<boolean>(false);\n\n    useEffect(() => {\n        if (!url) return;\n\n        const handleUrl = async () => {\n            const result = await getMediaInfo(url);\n            const data = await result.blob()\n            handleFile(data);\n        }\n\n        handleUrl();\n    }, [url])\n\n    const selectFile = () => {\n        selectFiles({ accept: 'image/*;video/*', multiple: false }).then(async files => {\n            if (files) {\n                handleFile(files[0])\n            }\n        });\n    }\n\n    const handleFile = async (file: Blob) => {\n        const imageFile: ImageFile = {\n            file: file,\n            url: URL.createObjectURL(file)\n        };\n\n        setImage(undefined);\n        setVideo(undefined);\n        setImageResults(undefined);\n        setVideoResults(undefined);\n\n        const fileType = imageFile.file.type;\n\n        setProcessing(true);\n        if (fileType === \"image/gif\") {\n            setImage(imageFile);\n            const result = await uploadGif(imageFile.file);\n            const data: NsfwSpyFramesResult = await result.json();\n            setVideoResults(data);\n        } else if (fileType.startsWith(\"image/\")) {\n            setImage(imageFile);\n            const result = await uploadImage(imageFile.file);\n            const data: NsfwSpyResult = await result.json();\n            setImageResults(data);\n        } else if (fileType.startsWith(\"video/\")) {\n            setVideo(imageFile);\n            const result = await uploadVideo(imageFile.file);\n            const data: NsfwSpyFramesResult = await result.json();\n            setVideoResults(data);\n        }\n        setProcessing(false);\n    }\n\n    let sortedImageResults: [string, any][] | undefined = undefined;\n    if (imageResults) {\n        sortedImageResults = sortNsfwResult(imageResults);\n    }\n\n    return (\n        <div className=\"app\">\n            <header>\n                <Logo />\n            </header>\n            <main>\n                <section className=\"image-section\">\n                    <div className=\"image-canvas\" onClick={selectFile}>\n                        {!image && !video &&\n                            <>\n                                <div>\n                                    Select an image, Gif or video.\n                                </div>\n                                <div className=\"icons\">\n                                    <div><FontAwesomeIcon icon={faImage} /></div>\n                                    <div><FontAwesomeIcon icon={faVideo} /></div>\n                                </div>\n                                <div className=\"subtitle\">\n                                    Or paste a link below...\n                                </div>\n                            </>}\n                        {image &&\n                            <img src={image.url} className=\"image-preview\" />}\n                        {video &&\n                            <video\n                                src={video.url}\n                                autoPlay\n                                loop\n                                muted\n                                className=\"video-preview\" />}\n                    </div>\n                    <input\n                        type=\"text\"\n                        placeholder=\"https://i3.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg\"\n                        onChange={(e) => setUrl(e.target.value)} />\n                </section>\n                <section className=\"results-section\">\n                    {processing &&\n                        <div>\n                            Processing...\n                        </div>}\n                    {videoResults &&\n                        <>\n                            <ResponsiveContainer height=\"100%\" width=\"100%\">\n                                <LineChart\n                                    width={500}\n                                    height={300}\n                                    data={Object.values(videoResults.frames)}\n                                    margin={{\n                                        top: 5,\n                                        right: 30,\n                                        left: 20,\n                                        bottom: 5,\n                                    }}>\n                                    <XAxis dataKey=\"name\" />\n                                    <YAxis />\n                                    <Tooltip contentStyle={{ background: \"#292929\" }} />\n                                    <Legend />\n                                    <Line type=\"monotone\" dataKey=\"hentai\" stroke=\"#c25452\" strokeWidth={3} dot={false} />\n                                    <Line type=\"monotone\" dataKey=\"neutral\" stroke=\"#ffffff\" strokeWidth={3} dot={false} />\n                                    <Line type=\"monotone\" dataKey=\"pornography\" stroke=\"#ffa31a\" strokeWidth={3} dot={false} />\n                                    <Line type=\"monotone\" dataKey=\"sexy\" stroke=\"#fdfd66\" strokeWidth={3} dot={false} />\n                                </LineChart>\n                            </ResponsiveContainer>\n                            <span>{videoResults.frameCount} Frames Analysed</span>\n                        </>}\n                    {sortedImageResults &&\n                        <div>\n                            {sortedImageResults.map((result) =>\n                                <div className={`result-value ${result[0]}`}>\n                                    <span>{result[0]}</span>\n                                    <span>{result[1]}</span>\n                                </div>\n                            )}\n                        </div>}\n                </section>\n            </main >\n        </div >\n    );\n}\n\nexport default App;\n"
  },
  {
    "path": "NsfwSpy.App/client-app/src/aspnetcore-https.ts",
    "content": "﻿import * as fs from 'fs'\nimport * as path from 'path'\nimport * as child_process from 'child_process'\n\nconst spawn = child_process.spawn;\n\nconst baseFolder = process.env.APPDATA !== undefined && process.env.APPDATA !== ''\n    ? `${process.env.APPDATA}/ASP.NET/https`\n    : `${process.env.HOME}/.aspnet/https`;\n\nconst certArg = process.argv.map(arg => arg.match('/--name=(?<value>.+)/i')).filter(Boolean)[0];\nconst certName = certArg ? certArg?.groups?.value : process.env.npm_package_name;\n\nif (!certName) {\n    console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<<app>> explicitly');\n    process.exit(-1);\n}\n\nconst moduleCertFilePath = path.join(baseFolder, `${certName}.pem`);\nconst moduleKeyFilePath = path.join(baseFolder, `${certName}.key`);\n\nif (!fs.existsSync(moduleCertFilePath) || !fs.existsSync(moduleKeyFilePath)) {\n    spawn('dotnet', [\n        'dev-certs',\n        'https',\n        '--export-path',\n        moduleCertFilePath,\n        '--format',\n        'Pem',\n        '--no-password',\n    ], { stdio: 'inherit', })\n        .on('exit', (code: any) => process.exit(code));\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/aspnetcore-react.ts",
    "content": "﻿import * as fs from 'fs'\nimport * as path from 'path'\n\nconst baseFolder = process.env.APPDATA !== undefined && process.env.APPDATA !== ''\n    ? `${process.env.APPDATA}/ASP.NET/https`\n    : `${process.env.HOME}/.aspnet/https`;\n\nconst certArg = process.argv.map(arg => arg.match('/--name=(?<value>.+)/i')).filter(Boolean)[0];\nconst certName = certArg ? certArg?.groups?.value : process?.env?.npm_package_name;\n\nif (!certName) {\n    console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<<app>> explicitly');\n    process.exit(-1);\n}\n\nconst certFilePath = path.join(baseFolder, `${certName}.pem`);\nconst keyFilePath = path.join(baseFolder, `${certName}.key`);\n\nif (!fs.existsSync('.env.development.local')) {\n    fs.writeFileSync(\n        '.env.development.local',\n        `BROWSER=none\n        HTTPS=true\n        SSL_CRT_FILE=${certFilePath}\n        SSL_KEY_FILE=${keyFilePath}`,\n    );\n} else {\n    let lines = fs.readFileSync('.env.development.local')\n        .toString()\n        .split('\\n');\n\n    let hasCert, hasCertKey = false;\n    for (const line of lines) {\n        if (/SSL_CRT_FILE=.*/i.test(line)) {\n            hasCert = true;\n        }\n        if (/SSL_KEY_FILE=*/i.test(line)) {\n            hasCertKey = true;\n        }\n    }\n\n    if (!hasCert) {\n        fs.appendFileSync(\n            '.env.development.local',\n            `\\nSSL_CRT_FILE=${certFilePath}`\n        );\n    }\n    if (!hasCertKey) {\n        fs.appendFileSync(\n            '.env.development.local',\n            `\\nSSL_KEY_FILE=${keyFilePath}`\n        )\n    }\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/components/Logo/Logo.scss",
    "content": ".logo {\n    font-family: 'Roboto';\n    font-size: 56px;\n    margin: 16px;\n    user-select: none;\n\n    .white {\n        color: #fff;\n    }\n\n    .orange {\n        color: #ffa31a;\n    }\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/components/Logo/Logo.tsx",
    "content": "import './Logo.scss';\n\nexport const Logo = () => {\n    return (\n        <span className=\"logo\">\n            <span className=\"white\">Nsfw</span>\n            <span className=\"orange\">Spy</span>\n        </span>\n    )\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/functions/client.ts",
    "content": "export const uploadGif = (file: Blob) => {\n    return uploadFile(\"/nsfwspy/gif\", file);\n}\n\nexport const uploadImage = (file: Blob) => {\n    return uploadFile(\"/nsfwspy/image\", file);\n}\n\nexport const uploadVideo = (file: Blob) => {\n    return uploadFile(\"/nsfwspy/video\", file);\n}\n\nexport const getMediaInfo = (url: string) => {\n    return fetch(`/nsfwspy/url/${encodeURIComponent(url)}`);\n}\n\nconst uploadFile = (url: string, file: Blob) => {\n    var postSettings: RequestInit = {\n        method: 'POST',\n        mode: 'cors'\n    };\n    var data = new FormData();\n    data.append('file', file);\n    postSettings.body = data;\n\n    return fetch(url, postSettings);\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/functions/getContentType.ts",
    "content": "﻿export const getContentType = (url: string) => {\n    return new Promise((resolve, reject) => {\n        var req = new XMLHttpRequest();\n        req.open(\"GET\", url, true);\n        req.setRequestHeader(\"Range\", \"bytes=0\");\n        req.onreadystatechange = () => {\n            if (req.readyState === req.DONE) {\n                resolve(req.getResponseHeader(\"Content-Type\"));\n            }\n        };\n        req.send();\n    });\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/functions/selectFiles.ts",
    "content": "/**\n * Custom type for file input element (`<input type=\"file\" />`).\n */\ntype InputFile = HTMLInputElement & {\n    capture?: boolean | string;\n};\n\n/**\n * Type of options for file input element (`<input type=\"file\" />`) virtually\n * created to select files.\n */\nexport type Options = {\n    /**\n     * Defines accepted file types. It's a comma-separated list of file\n     * extensions, mime-types or unique file type specifiers.\n     *\n     * https://developer.mozilla.org/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers\n     *\n     * @example ```js\n     * \"image/*,video/*,.pdf,.doc,.docx,.xls\"\n     * ```\n     */\n    accept?: string;\n\n    /**\n     * Combined with `accept` property it specifies which camera to use for\n     * capture of image or video. It was previously a Boolean value.\n     */\n    capture?: string | null;\n\n    /**\n     * Allow multiple files selection.\n     */\n    multiple?: boolean;\n};\n\n/**\n * Creates a virtual file input element (`<input type=\"file\" />`) with options.\n * @param options\n */\nconst createInputFile = ({\n    accept = '',\n    capture = null,\n    multiple = false,\n}: Options = {}): InputFile => {\n    const input = document.createElement('input') as InputFile;\n\n    input.type = 'file';\n    input.accept = accept;\n    if (capture !== null)\n        input.capture = capture;\n    input.multiple = multiple;\n\n    return input;\n};\n\n/**\n * Virtually creates a file input element (`<input type=\"file\" />`), triggers it\n * and returns selected files.\n *\n * @example\n * selectFiles({ accept: 'image/*', multiple: true }).then(files => {\n *   // ...\n * });\n *\n * @param options\n */\nexport const selectFiles = (options?: Options) =>\n    new Promise<null | FileList>((resolve) => {\n        const input = createInputFile(options);\n\n        input.addEventListener('change', () => resolve(input.files || null));\n\n        setTimeout(() => {\n            const event = new MouseEvent('click');\n            input.dispatchEvent(event);\n        }, 0);\n    });\n"
  },
  {
    "path": "NsfwSpy.App/client-app/src/functions/sortBy.ts",
    "content": "﻿import { NsfwSpyResult } from '../models/NsfwSpyResult';\n\nexport const sortNsfwResult = (result: NsfwSpyResult) => {\n    const validKeys = ['hentai', 'neutral', 'pornography', 'sexy'];\n    const sortableArray = Object.entries(result);\n    let sortedArray = sortableArray.sort(([, a], [, b]) => b - a);\n    sortedArray = sortableArray.filter((i) => validKeys.includes(i[0]));\n    return sortedArray;\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/index.scss",
    "content": "body {\n  margin: 0;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport './index.scss';\nimport App from './App';\nimport reportWebVitals from './reportWebVitals';\n\nReactDOM.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n  document.getElementById('root')\n);\n\n// If you want to start measuring performance in your app, pass a function\n// to log results (for example: reportWebVitals(console.log))\n// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals\nreportWebVitals();\n"
  },
  {
    "path": "NsfwSpy.App/client-app/src/models/ImageFile.ts",
    "content": "export interface ImageFile {\n    file: Blob\n    url: string\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/models/MediaInfo.ts",
    "content": "export interface MediaInfo {\n    file: Blob\n    mimeType: string\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/models/NsfwSpyFramesResult.ts",
    "content": "import { NsfwSpyResult } from \"./NsfwSpyResult\"\n\nexport interface NsfwSpyFramesResult {\n    frames: { [frame: number]: NsfwSpyResult }\n    frameCount: number\n    isNsfw: boolean\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/models/NsfwSpyResult.ts",
    "content": "export interface NsfwSpyResult {\n    hentai: number\n    neutral: number\n    pornography: number\n    sexy: number\n    predictedLabel: string\n    isNsfw: boolean\n}"
  },
  {
    "path": "NsfwSpy.App/client-app/src/react-app-env.d.ts",
    "content": "/// <reference types=\"react-scripts\" />\n"
  },
  {
    "path": "NsfwSpy.App/client-app/src/reportWebVitals.ts",
    "content": "import { ReportHandler } from 'web-vitals';\n\nconst reportWebVitals = (onPerfEntry?: ReportHandler) => {\n  if (onPerfEntry && onPerfEntry instanceof Function) {\n    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {\n      getCLS(onPerfEntry);\n      getFID(onPerfEntry);\n      getFCP(onPerfEntry);\n      getLCP(onPerfEntry);\n      getTTFB(onPerfEntry);\n    });\n  }\n};\n\nexport default reportWebVitals;\n"
  },
  {
    "path": "NsfwSpy.App/client-app/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES6\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\n    \"src\"\n  ]\n}\n"
  },
  {
    "path": "NsfwSpy.PerformanceTesting/NsfwSpy.PerformanceTesting.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net6.0</TargetFramework>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\NsfwSpy\\NsfwSpy.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "NsfwSpy.PerformanceTesting/PerformanceResult.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace NsfwSpyNS.PerformanceTesting\n{\n    class PerformanceResult\n    {\n        public string Key { get; set; }\n        public List<NsfwSpyResult> Results { get; set; }\n        public int CorrectAsserts => Results.Count(r => r.PredictedLabel == Key);\n        public int NsfwAsserts => Results.Count(r => r.IsNsfw);\n        public int HentaiAsserts => Results.Count(r => r.PredictedLabel == \"Hentai\");\n        public int NeutralAsserts => Results.Count(r => r.PredictedLabel == \"Neutral\");\n        public int PornographyAsserts => Results.Count(r => r.PredictedLabel == \"Pornography\");\n        public int SexyAsserts => Results.Count(r => r.PredictedLabel == \"Sexy\");\n        public int TotalAsserts => Results.Count();\n\n        public PerformanceResult(string key)\n        {\n            Key = key;\n            Results = new List<NsfwSpyResult>();\n        }\n    }\n}\n"
  },
  {
    "path": "NsfwSpy.PerformanceTesting/Program.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\n\nnamespace NsfwSpyNS.PerformanceTesting\n{\n    class Program\n    {\n        static void Main(string[] args)\n        {\n            var assetsPath = @\"E:\\NsfwSpy\\Images\";\n            var testImagesPath = @\"E:\\NsfwSpy\\Test\";\n\n            var classificationTypes = new[]\n            {\n                \"Hentai\",\n                \"Neutral\",\n                \"Pornography\",\n                \"Sexy\",\n            };\n\n            var results = new List<PerformanceResult>();\n            var nsfwSpy = new NsfwSpy();\n\n            foreach (var classificationType in classificationTypes)\n            {\n                var testFilesDirectory = Path.Combine(testImagesPath, classificationType);\n                var testFiles = Directory.Exists(testFilesDirectory) ? Directory.GetFiles(testFilesDirectory).ToList() : new List<string>();\n\n                if (!testFiles.Any())\n                {\n                    var directory = Path.Combine(assetsPath, classificationType);\n                    var files = Directory.GetFiles(directory).OrderBy(f => Guid.NewGuid()).ToList();\n\n                    var length = files.Count > 10000 ? 10000 : files.Count;\n                    files = files.Take(length).ToList();\n\n                    Console.WriteLine($\"Copying {classificationType} test files...\");\n\n                    if (!Directory.Exists(testFilesDirectory))\n                        Directory.CreateDirectory(testFilesDirectory);\n\n                    Parallel.ForEach(files, file => {\n                        var filename = Path.GetFileName(file);\n                        var dest = Path.Combine(testFilesDirectory, filename);\n                        File.Copy(file, dest);\n                    });\n\n                    testFiles = Directory.GetFiles(testFilesDirectory).ToList();\n                }\n\n                var pr = new PerformanceResult(classificationType);\n\n                nsfwSpy.ClassifyImages(testFiles, (filePath, result) =>\n                {\n                    pr.Results.Add(result);\n                    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}%)\");\n                });\n\n                results.Add(pr);\n            }\n\n            Console.WriteLine(Environment.NewLine);\n            foreach (var pr in results)\n            {\n                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}%)\");\n            }\n\n            Console.WriteLine(Environment.NewLine);\n            Console.WriteLine(\"Confusion Matrix\\n\");\n\n            Console.WriteLine(\"\\t\\t\\tPredicted Label\");\n            Console.WriteLine(\"Actual Label\\t\\tHentai\\t\\tNeutral\\t\\tPornography\\tSexy\");\n            Console.WriteLine();\n            foreach (var pr in results)\n            {\n                Console.WriteLine($\"{pr.Key}\\t\\t{(pr.Key != \"Pornography\" ? \"\\t\" : \"\")}{pr.HentaiAsserts}\\t\\t{pr.NeutralAsserts}\\t\\t{pr.PornographyAsserts}\\t\\t{pr.SexyAsserts}\");\n            }\n\n            Console.WriteLine(Environment.NewLine);\n            Console.WriteLine(\"Average Confidence\\n\");\n\n            foreach (var pr in results)\n            {\n                Console.WriteLine($\"{pr.Key}\\t\\t{(pr.Key != \"Pornography\" ? \"\\t\" : \"\")}{pr.Results.Sum(r => r.ToDictionary()[pr.Key]) / pr.Results.Count}\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "NsfwSpy.Test/NsfwSpy.Test.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net6.0</TargetFramework>\n\n    <IsPackable>false</IsPackable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.2.0\" />\n    <PackageReference Include=\"xunit\" Version=\"2.4.1\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"2.4.5\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\" Version=\"3.1.2\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\NsfwSpy\\NsfwSpy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Assets\\\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"Assets\\bikini.avi\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"Assets\\bikini.gif\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"Assets\\bikini.mkv\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"Assets\\bikini.mp4\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"Assets\\bikini.webm\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"Assets\\cool.gif\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"Assets\\flower.jpg\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"Assets\\flower.png\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"Assets\\flower.webp\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "NsfwSpy.Test/UnitTests.cs",
    "content": "using System;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace NsfwSpyNS.Test\n{\n    public class UnitTests\n    {\n        [Theory]\n        [InlineData(\"flower.jpg\")]\n        [InlineData(\"flower.png\")]\n        [InlineData(\"flower.webp\")]\n        public void ClassifyImageByteArray_ValidByteArray(string filename)\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, $@\"Assets/{filename}\");\n            var imageBytes = File.ReadAllBytes(filePath);\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyImage(imageBytes);\n\n            Assert.Equal(\"Neutral\", result.PredictedLabel);\n        }\n\n        [Fact]\n        public void ClassifyImageByteArray_InvalidByteArray()\n        {\n            var nsfwSpy = new NsfwSpy();\n            Assert.Throws<ClassificationFailedException>(() => nsfwSpy.ClassifyImage(new byte[0]));\n        }\n\n        [Theory]\n        [InlineData(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.jpg\")]\n        [InlineData(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.png\")]\n        [InlineData(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.webp\")]\n        public void ClassifyImageUri_ValidUri(string url)\n        {\n            var uri = new Uri(url);\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyImage(uri);\n\n            Assert.Equal(\"Neutral\", result.PredictedLabel);\n        }\n\n        [Theory]\n        [InlineData(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.jpg\")]\n        [InlineData(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.png\")]\n        [InlineData(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.webp\")]\n        public void ClassifyImageUri_CustomWebClient(string url)\n        {\n            var uri = new Uri(url);\n            var webClient = new WebClient();\n            webClient.Headers[\"User-Agent\"] = \"test\";\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyImage(uri, webClient);\n\n            Assert.Equal(\"Neutral\", result.PredictedLabel);\n        }\n\n        [Theory]\n        [InlineData(\"flower.jpg\")]\n        [InlineData(\"flower.png\")]\n        [InlineData(\"flower.webp\")]\n        public void ClassifyImageFilePath_ValidFilePath(string filename)\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, $@\"Assets/{filename}\");\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyImage(filePath);\n\n            Assert.Equal(\"Neutral\", result.PredictedLabel);\n        }\n\n        [Fact]\n        public void ClassifyImageFilePath_InvalidFilePath()\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, @\"Assets/filedoesnotexist.jpg\");\n\n            var nsfwSpy = new NsfwSpy();\n            Assert.Throws<FileNotFoundException>(() => nsfwSpy.ClassifyImage(filePath));\n        }\n\n        [Theory]\n        [InlineData(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.jpg\")]\n        [InlineData(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.png\")]\n        [InlineData(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.webp\")]\n        public async Task ClassifyImageUriAsync_ValidUri(string url)\n        {\n            var uri = new Uri(url);\n\n            var nsfwSpy = new NsfwSpy();\n            var result = await nsfwSpy.ClassifyImageAsync(uri);\n\n            Assert.Equal(\"Neutral\", result.PredictedLabel);\n        }\n\n        [Theory]\n        [InlineData(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.jpg\")]\n        [InlineData(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.png\")]\n        [InlineData(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.webp\")]\n        public async Task ClassifyImageUriAsync_CustomWebClient(string url)\n        {\n            var uri = new Uri(url);\n            var webClient = new WebClient();\n            webClient.Headers[\"User-Agent\"] = \"test\";\n\n            var nsfwSpy = new NsfwSpy();\n            var result = await nsfwSpy.ClassifyImageAsync(uri, webClient);\n\n            Assert.Equal(\"Neutral\", result.PredictedLabel);\n        }\n\n        [Theory]\n        [InlineData(\"flower.jpg\")]\n        [InlineData(\"flower.png\")]\n        [InlineData(\"flower.webp\")]\n        public async Task ClassifyImageFilePathAsync_ValidFilePath(string filename)\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, $@\"Assets/{filename}\");\n\n            var nsfwSpy = new NsfwSpy();\n            var result = await nsfwSpy.ClassifyImageAsync(filePath);\n\n            Assert.Equal(\"Neutral\", result.PredictedLabel);\n        }\n\n        [Fact]\n        public void ClassifyGifByteArray_ValidByteArray()\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, @\"Assets/cool.gif\");\n            var imageBytes = File.ReadAllBytes(filePath);\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyGif(imageBytes);\n\n            Assert.Equal(10, result.Frames.Count);\n            Assert.False(result.IsNsfw);\n        }\n\n        [Fact]\n        public void ClassifyGifFilePath_ValidFilePath()\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, @\"Assets/cool.gif\");\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyGif(filePath);\n\n            Assert.Equal(10, result.Frames.Count);\n            Assert.False(result.IsNsfw);\n        }\n\n        [Fact]\n        public void ClassifyGifFilePath_ClassifyEvery2ndFrame()\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, @\"Assets/cool.gif\");\n            var videoOptions = new VideoOptions\n            {\n                ClassifyEveryNthFrame = 2\n            };\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyGif(filePath, videoOptions);\n\n            Assert.Equal(5, result.Frames.Count);\n            Assert.False(result.IsNsfw);\n        }\n\n        [Fact]\n        public void ClassifyGifFilePath_EndEarlyOnNsfw()\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, @\"Assets/bikini.gif\");\n            var videoOptions = new VideoOptions\n            {\n                EarlyStopOnNsfw = true\n            };\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyGif(filePath, videoOptions: videoOptions);\n\n            Assert.True(result.IsNsfw);\n            Assert.True(result.Frames.Count < 181); // This Gif has 181 frames\n        }\n\n        [Fact]\n        public void ClassifyGifUri_ValidUri()\n        {\n            var uri = new Uri(\"https://media2.giphy.com/media/62PP2yEIAZF6g/giphy.gif\");\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyGif(uri);\n\n            Assert.Equal(10, result.Frames.Count);\n            Assert.False(result.IsNsfw);\n        }\n\n        [Fact]\n        public void ClassifyGifUri_ClassifyEvery2ndFrame()\n        {\n            var uri = new Uri(\"https://media2.giphy.com/media/62PP2yEIAZF6g/giphy.gif\");\n            var videoOptions = new VideoOptions\n            {\n                ClassifyEveryNthFrame = 2\n            };\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyGif(uri, videoOptions: videoOptions);\n\n            Assert.Equal(5, result.Frames.Count);\n            Assert.False(result.IsNsfw);\n        }\n\n        [Fact]\n        public void ClassifyGifUri_EndEarlyOnNsfw()\n        {\n            var uri = new Uri(\"https://c.tenor.com/5y-jOowm51MAAAAd/bikini.gif\");\n            var videoOptions = new VideoOptions\n            {\n                EarlyStopOnNsfw = true\n            };\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyGif(uri, videoOptions: videoOptions);\n\n            Assert.True(result.IsNsfw);\n            Assert.True(result.Frames.Count < 181); // This Gif has 181 frames\n        }\n\n        [Fact]\n        public async Task ClassifyGifFilePathAsync_ValidFilePath()\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, @\"Assets/cool.gif\");\n\n            var nsfwSpy = new NsfwSpy();\n            var result = await nsfwSpy.ClassifyGifAsync(filePath);\n\n            Assert.Equal(10, result.Frames.Count);\n            Assert.False(result.IsNsfw);\n        }\n\n        [Fact]\n        public async Task ClassifyGifUriAsync_ValidUri()\n        {\n            var uri = new Uri(\"https://media2.giphy.com/media/62PP2yEIAZF6g/giphy.gif\");\n\n            var nsfwSpy = new NsfwSpy();\n            var result = await nsfwSpy.ClassifyGifAsync(uri);\n\n            Assert.Equal(10, result.Frames.Count);\n            Assert.False(result.IsNsfw);\n        }\n\n        [Theory]\n        [InlineData(\"bikini.avi\")]\n        [InlineData(\"bikini.mkv\")]\n        [InlineData(\"bikini.mp4\")]\n        [InlineData(\"bikini.webm\")]\n        public void ClassifyVideoByteArray_ValidByteArray(string filename)\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, $@\"Assets\\{filename}\");\n            var imageBytes = File.ReadAllBytes(filePath);\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyVideo(imageBytes);\n\n            if (filename.EndsWith(\".webm\"))\n                Assert.Equal(180, result.Frames.Count);\n            else\n                Assert.Equal(181, result.Frames.Count);\n\n            Assert.True(result.IsNsfw);\n        }\n\n        [Theory]\n        [InlineData(\"bikini.avi\")]\n        [InlineData(\"bikini.mkv\")]\n        [InlineData(\"bikini.mp4\")]\n        [InlineData(\"bikini.webm\")]\n        public void ClassifyVideoFilePath_ValidFilePath(string filename)\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, $@\"Assets\\{filename}\");\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyVideo(filePath);\n\n            if (filename.EndsWith(\".webm\"))\n                Assert.Equal(180, result.Frames.Count);\n            else\n                Assert.Equal(181, result.Frames.Count);\n\n            Assert.True(result.IsNsfw);\n        }\n\n        [Theory]\n        [InlineData(\"bikini.avi\")]\n        [InlineData(\"bikini.mkv\")]\n        [InlineData(\"bikini.mp4\")]\n        [InlineData(\"bikini.webm\")]\n        public void ClassifyVideoFilePath_ClassifyEvery2ndFrame(string filename)\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, $@\"Assets\\{filename}\");\n            var videoOptions = new VideoOptions\n            {\n                ClassifyEveryNthFrame = 2\n            };\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyVideo(filePath, videoOptions);\n\n            if (filename.EndsWith(\".webm\"))\n                Assert.Equal(90, result.Frames.Count);\n            else\n                Assert.Equal(91, result.Frames.Count);\n\n            Assert.True(result.IsNsfw);\n        }\n\n        [Theory]\n        [InlineData(\"bikini.avi\")]\n        [InlineData(\"bikini.mkv\")]\n        [InlineData(\"bikini.mp4\")]\n        [InlineData(\"bikini.webm\")]\n        public void ClassifyVideoFilePath_EndEarlyOnNsfw(string filename)\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, $@\"Assets\\{filename}\");\n            var videoOptions = new VideoOptions\n            {\n                EarlyStopOnNsfw = true\n            };\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyVideo(filePath, videoOptions: videoOptions);\n\n            Assert.True(result.IsNsfw);\n            Assert.True(result.Frames.Count < 181); // This video has 181 frames\n        }\n\n        [Fact]\n        public void ClassifyVideoUri_ValidUri()\n        {\n            var uri = new Uri(\"https://i.imgur.com/MjTH5ZS.mp4\");\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyVideo(uri);\n\n            Assert.Equal(120, result.Frames.Count);\n            Assert.True(result.IsNsfw);\n        }\n\n        [Fact]\n        public void ClassifyVideoUri_ClassifyEvery2ndFrame()\n        {\n            var uri = new Uri(\"https://i.imgur.com/MjTH5ZS.mp4\");\n            var videoOptions = new VideoOptions\n            {\n                ClassifyEveryNthFrame = 2\n            };\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyVideo(uri, videoOptions: videoOptions);\n\n            Assert.Equal(60, result.Frames.Count);\n            Assert.True(result.IsNsfw);\n        }\n\n        [Fact]\n        public void ClassifyVideoUri_EndEarlyOnNsfw()\n        {\n            var uri = new Uri(\"https://i.imgur.com/MjTH5ZS.mp4\");\n            var videoOptions = new VideoOptions\n            {\n                EarlyStopOnNsfw = true\n            };\n\n            var nsfwSpy = new NsfwSpy();\n            var result = nsfwSpy.ClassifyVideo(uri, videoOptions: videoOptions);\n\n            Assert.True(result.IsNsfw);\n            Assert.True(result.Frames.Count < 120); // This video has 120 frames\n        }\n\n        [Theory]\n        [InlineData(\"bikini.avi\")]\n        [InlineData(\"bikini.mkv\")]\n        [InlineData(\"bikini.mp4\")]\n        [InlineData(\"bikini.webm\")]\n        public async Task ClassifyVideoFilePathAsync_ValidFilePath(string filename)\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, $@\"Assets\\{filename}\");\n\n            var nsfwSpy = new NsfwSpy();\n            var result = await nsfwSpy.ClassifyVideoAsync(filePath);\n\n            if (filename.EndsWith(\".webm\"))\n                Assert.Equal(180, result.Frames.Count);\n            else\n                Assert.Equal(181, result.Frames.Count);\n\n            Assert.True(result.IsNsfw);\n        }\n\n        [Fact]\n        public async Task ClassifyVideoUriAsync_ValidUri()\n        {\n            var uri = new Uri(\"https://i.imgur.com/MjTH5ZS.mp4\");\n\n            var nsfwSpy = new NsfwSpy();\n            var result = await nsfwSpy.ClassifyVideoAsync(uri);\n\n            Assert.Equal(120, result.Frames.Count);\n            Assert.True(result.IsNsfw);\n        }\n    }\n}\n"
  },
  {
    "path": "NsfwSpy.Train/ImageData.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace NsfwSpyNS.Train\n{\n    class ImageData\n    {\n        public string ImagePath { get; set; }\n        public string Label { get; set; }\n    }\n}\n"
  },
  {
    "path": "NsfwSpy.Train/NsfwSpy.Train.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net6.0</TargetFramework>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Models\\\" />\n    <Folder Include=\"Workspace\\\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.ML\" Version=\"1.7.1\" />\n    <PackageReference Include=\"Microsoft.ML.ImageAnalytics\" Version=\"1.7.1\" />\n    <PackageReference Include=\"Microsoft.ML.Vision\" Version=\"1.7.1\" />\n    <PackageReference Include=\"SciSharp.TensorFlow.Redist-Windows-GPU\" Version=\"2.3.1\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "NsfwSpy.Train/Program.cs",
    "content": "﻿using Microsoft.ML;\nusing Microsoft.ML.Trainers;\nusing Microsoft.ML.Vision;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\n\nnamespace NsfwSpyNS.Train\n{\n    class Program\n    {\n        static void Main(string[] args)\n        {\n            var projectDirectory = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, \"../../../\"));\n            var workspaceRelativePath = Path.Combine(projectDirectory, \"Workspace\");\n            var saveModelPath = Path.Combine(projectDirectory, \"Models\");\n            var assetsPath = @\"E:\\NsfwSpy\\Images\";\n\n            var mlContext = new MLContext();\n            mlContext.Log += (object sender, LoggingEventArgs e) => Console.WriteLine(e.Message);\n\n            var images = LoadImagesFromDirectory(assetsPath);\n            var imageData = mlContext.Data.LoadFromEnumerable(images);\n            var shuffledData = mlContext.Data.ShuffleRows(imageData);\n\n            var preprocessingPipeline = mlContext.Transforms.Conversion.MapValueToKey(\n                    inputColumnName: \"Label\",\n                    outputColumnName: \"LabelAsKey\")\n                .Append(mlContext.Transforms.LoadRawImageBytes(\n                    outputColumnName: \"Image\",\n                    imageFolder: assetsPath,\n                    inputColumnName: \"ImagePath\"));\n\n            var preProcessedData = preprocessingPipeline\n                                .Fit(shuffledData)\n                                .Transform(shuffledData);\n\n            var trainSplit = mlContext.Data.TrainTestSplit(data: preProcessedData, testFraction: 0.1);\n\n            var trainSet = trainSplit.TrainSet;\n            var validationSet = trainSplit.TestSet;\n\n            var classifierOptions = new ImageClassificationTrainer.Options()\n            {\n                FeatureColumnName = \"Image\",\n                LabelColumnName = \"LabelAsKey\",\n                ValidationSet = validationSet,\n                Arch = ImageClassificationTrainer.Architecture.ResnetV250,\n                MetricsCallback = (metrics) => Console.WriteLine(metrics),\n                TestOnTrainSet = true,\n                ReuseTrainSetBottleneckCachedValues = true,\n                ReuseValidationSetBottleneckCachedValues = true,\n                Epoch = 2500,\n                BatchSize = 32,\n                LearningRate = 0.01f,\n                EarlyStoppingCriteria = new ImageClassificationTrainer.EarlyStopping { CheckIncreasing = true, MinDelta = 0.00001f, Patience = 50 },\n                WorkspacePath = workspaceRelativePath,\n                LearningRateScheduler = new ExponentialLRDecay(numEpochsPerDecay: 5)\n            };\n\n            var trainingPipeline = mlContext.MulticlassClassification.Trainers.ImageClassification(classifierOptions)\n                .Append(mlContext.Transforms.Conversion.MapKeyToValue(\"PredictedLabel\"));\n\n            var trainedModel = trainingPipeline.Fit(trainSet);\n\n            if (!Directory.Exists(saveModelPath))\n                Directory.CreateDirectory(saveModelPath);\n\n            saveModelPath = Path.Combine(saveModelPath, \"NsfwSpyModel.zip\");\n            mlContext.Model.Save(trainedModel, trainSet.Schema, saveModelPath);\n\n            Console.WriteLine(\"Complete\");\n        }\n\n        public static IEnumerable<ImageData> LoadImagesFromDirectory(string folder)\n        {\n            var allowedFileExtensions = new[] { \".jpg\", \".jpeg\", \".png\" };\n            var files = Directory.GetFiles(folder, \"*\", searchOption: SearchOption.AllDirectories);\n\n            foreach (var file in files)\n            {\n                var extension = Path.GetExtension(file);\n                if (!allowedFileExtensions.Contains(extension))\n                    continue;\n\n                var label = Directory.GetParent(file).Name;\n\n                yield return new ImageData()\n                {\n                    ImagePath = file,\n                    Label = label\n                };\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "NsfwSpy.Train/tfjs-convert-command.txt",
    "content": "tensorflowjs_converter --input_format=tf_frozen_model {input_file} {export_dir} --output_node_names=Score --skip_op_check"
  },
  {
    "path": "NsfwSpy.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.1.32228.430\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"NsfwSpy\", \"NsfwSpy\\NsfwSpy.csproj\", \"{B041BBD4-B1E3-4BCE-B06B-25146E38CE3E}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"NsfwSpy.Train\", \"NsfwSpy.Train\\NsfwSpy.Train.csproj\", \"{C3F14E31-BDCD-4702-8370-5CD80C964621}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"NsfwSpy.PerformanceTesting\", \"NsfwSpy.PerformanceTesting\\NsfwSpy.PerformanceTesting.csproj\", \"{412E968C-E4D6-4CA1-9C75-8F78BE7275CD}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"NsfwSpy.Test\", \"NsfwSpy.Test\\NsfwSpy.Test.csproj\", \"{3DE7054D-D7B5-4315-ADEF-5E4BA2D21138}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"NsfwSpy.App\", \"NsfwSpy.App\\NsfwSpy.App.csproj\", \"{025998A0-9590-4C70-89E5-311ADBBDCD92}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{B041BBD4-B1E3-4BCE-B06B-25146E38CE3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{B041BBD4-B1E3-4BCE-B06B-25146E38CE3E}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{B041BBD4-B1E3-4BCE-B06B-25146E38CE3E}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{B041BBD4-B1E3-4BCE-B06B-25146E38CE3E}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{C3F14E31-BDCD-4702-8370-5CD80C964621}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{C3F14E31-BDCD-4702-8370-5CD80C964621}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{C3F14E31-BDCD-4702-8370-5CD80C964621}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{C3F14E31-BDCD-4702-8370-5CD80C964621}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{412E968C-E4D6-4CA1-9C75-8F78BE7275CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{412E968C-E4D6-4CA1-9C75-8F78BE7275CD}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{412E968C-E4D6-4CA1-9C75-8F78BE7275CD}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{412E968C-E4D6-4CA1-9C75-8F78BE7275CD}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{3DE7054D-D7B5-4315-ADEF-5E4BA2D21138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3DE7054D-D7B5-4315-ADEF-5E4BA2D21138}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3DE7054D-D7B5-4315-ADEF-5E4BA2D21138}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3DE7054D-D7B5-4315-ADEF-5E4BA2D21138}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{025998A0-9590-4C70-89E5-311ADBBDCD92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{025998A0-9590-4C70-89E5-311ADBBDCD92}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{025998A0-9590-4C70-89E5-311ADBBDCD92}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{025998A0-9590-4C70-89E5-311ADBBDCD92}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {C3F4B0CC-B514-4792-912E-F2A6052FD5E9}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"https://raw.githubusercontent.com/NsfwSpy/NsfwSpy.NET/main/_art/NsfwSpy.jpg\" alt=\"NsfwSpy Logo\" width=\"400\"/>\n\n# Introduction\nNsfwSpy 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:\n\n| Label       | Description | Files |\n| ----------- | ----------- | ----- |\n| Pornography | Images that depict sexual acts and nudity. | 106,000 |\n| Sexy        | Images of people in their underwear and men who are topless. | 78,000 |\n| Hentai      | Drawings or animations of sexual acts and nudity. | 83,000 |\n| Neutral     | Images that are not sexual in nature. | 378,000 |\n\n<img src=\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/_art/Examples.gif\" />\n\n### Other Projects\nLooking for a **JavaScript** version of NsfwSpy? We have you covered - **[NsfwSpy.js](https://github.com/d00ML0rDz/NsfwSpy.js)** 😎\n\n# Performance\nNsfwSpy 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.\n\n|     | Pornography | Sexy | Hentai | Neutral |\n| --- | --- | --- | --- | --- |\n| Is Nsfw <sub><sup>(pornography + sexy + hentai >= 0.5)</sup></sub> | 95.8% | 97.0% | 95.2% | 3.7% | \n| Correctly Predicted Label | 85.7% | 84.4% | 91.9% | 96.54% |\n\n# Quick Start\nLooking to quickly try out NsfwSpy? Check out our steps to use [NsfwSpy.App](https://github.com/d00ML0rDz/NsfwSpy/tree/main/NsfwSpy.App).\n\nThis project is available as a [NuGet](https://www.nuget.org/packages/NsfwSpy/) package and can be installed with the following commands:\n\n**Package Manager**\n```\nInstall-Package NsfwSpy\n```\n\n**.NET CLI**\n```\ndotnet add package NsfwSpy\n```\n\n### Classify an Image File\n```csharp\nvar nsfwSpy = new NsfwSpy();\nvar result = nsfwSpy.ClassifyImage(@\"C:\\Users\\username\\Documents\\flower.jpg\");\n```\n\n### Classify a Web Image\n```csharp\nvar uri = new Uri(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/flower.jpg\");\nvar nsfwSpy = new NsfwSpy();\nvar result = nsfwSpy.ClassifyImage(uri);\n```\n\n### Classify an Image from a Byte Array\n```csharp\nvar fileBytes = File.ReadAllBytes(filePath);\nvar nsfwSpy = new NsfwSpy();\nvar result = nsfwSpy.ClassifyImage(fileBytes);\n```\n\n### Classify Multiple Image Files\n```csharp\nvar files = Directory.GetFiles(@\"C:\\Users\\username\\Pictures\");\nvar nsfwSpy = new NsfwSpy();\nvar results = nsfwSpy.ClassifyImages(files, (filePath, result) =>\n{\n    Console.WriteLine($\"{filePath} - {result.PredictedLabel}\");\n});\n```\n\n### Classify a Gif File\n```csharp\nvar nsfwSpy = new NsfwSpy();\nvar result = nsfwSpy.ClassifyGif(@\"C:\\Users\\username\\Documents\\happy.gif\");\n```\n\n### Classify a Web Gif\n```csharp\nvar uri = new Uri(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/cool.gif\");\nvar nsfwSpy = new NsfwSpy();\nvar result = nsfwSpy.ClassifyGif(uri);\n```\n\n### Classify a Video File\n```csharp\nvar nsfwSpy = new NsfwSpy();\nvar result = nsfwSpy.ClassifyVideo(@\"C:\\Users\\username\\Documents\\happy.mp4\");\n```\n\n### Classify a Web Video\n```csharp\nvar uri = new Uri(\"https://raw.githubusercontent.com/d00ML0rDz/NsfwSpy/main/NsfwSpy.Test/Assets/bikini.mp4\");\nvar nsfwSpy = new NsfwSpy();\nvar result = nsfwSpy.ClassifyVideo(uri);\n```\n\n### Dependency Injection\n```csharp\nservices.AddScoped<INsfwSpy, NsfwSpy>();\n```\n\n# Classify Video Support\nTo 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.\n\n## Windows \nFollow [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.\n\n## macOS\nInstall FFmpeg on macOS using [Homebrew](https://brew.sh/) via the following command:\n```\nbrew install ffmpeg\n```\n\n## Ubuntu\nInstall FFmpeg on Ubuntu using the following command:\n```\nsudo apt install ffmpeg\n```\n\n# GPU Support\nTo 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.\n\n# macOS Support\nTo 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:\n\n**Package Manager**\n```\nInstall-Package SciSharp.TensorFlow.Redist -Version 2.3.1\n```\n\n**.NET CLI**\n```\ndotnet add package SciSharp.TensorFlow.Redist --version 2.3.1\n```\n\nPlease 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.\n\n# Contact Us\nInterested 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).\n\n# Notes\nUsing NsfwSpy? Let us know! We're keen to hear how the technology is being used and improving the safety of applications.\n\nGot 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.\n"
  }
]