├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── fastDeploy_recipes ├── build_images.sh └── classifier │ ├── example.jpg │ ├── example.pkl │ ├── predictor.py │ └── requirements.txt ├── nudenet ├── __init__.py ├── classifier.py ├── image_utils.py └── models │ └── classifier_model.onnx └── setup.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Please follow this template for any issues/ bugs 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug and error messages (if any)** 11 | A clear and concise description of what the bug is. 12 | 13 | 14 | **The code snippet which gave this error*** 15 | 16 | **Specify versions of the following libraries** 17 | 1. nudenet 18 | 2. tensorflow/ tensorflow-gpu 19 | 3. keras 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | .idea/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 notAI.tech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NudeNet: Neural Nets for Nudity Classification, Detection and selective censoring 2 | 3 | [![DOI](https://zenodo.org/badge/173154449.svg)](https://zenodo.org/badge/latestdoi/173154449) ![Upload Python package](https://github.com/notAI-tech/NudeNet/actions/workflows/python-publish.yml/badge.svg) 4 | 5 | 6 | ## Fork differences: 7 | 8 | - Only the default classifier is available. 9 | - The classifier no longer throws the `Initializer block1_conv1_bn/keras_learning_phase:0 appears in graph inputs 10 | and will not be treated as constant value/weight. etc.` warning. 11 | - It only works on images. 12 | - The classifier is included in the project itself. 13 | - Only the v2 model is available (the original repo's default). So `v2` from original is `main` here. 14 | 15 | Uncensored version of the following image can be found at https://i.imgur.com/rga6845.jpg (NSFW) 16 | 17 | ![](https://i.imgur.com/0KPJbl9.jpg) 18 | 19 | **Classifier classes:** 20 | |class name | Description | 21 | |--------|:--------------: 22 | |safe | Image is not sexually explicit | 23 | |unsafe | Image is sexually explicit| 24 | 25 | 26 | # As self-hostable API service 27 | ```bash 28 | # Classifier 29 | docker run -it -p8080:8080 notaitech/nudenet:classifier 30 | 31 | # See fastDeploy-file_client.py for running predictions via fastDeploy's REST endpoints 32 | wget https://raw.githubusercontent.com/notAI-tech/fastDeploy/master/cli/fastDeploy-file_client.py 33 | # Single input 34 | python fastDeploy-file_client.py --file PATH_TO_YOUR_IMAGE 35 | 36 | # Client side batching 37 | python fastDeploy-file_client.py --dir PATH_TO_FOLDER --ext jpg 38 | ``` 39 | 40 | **Note: golang example https://github.com/notAI-tech/NudeNet/issues/63#issuecomment-729555360**, thanks to [Preetham Kamidi](https://github.com/preetham) 41 | 42 | 43 | # As Python module 44 | **Installation**: 45 | ```bash 46 | pip install -U git+https://github.com/platelminto/NudeNet 47 | ``` 48 | 49 | **Classifier Usage**: 50 | ```python 51 | # Import module 52 | from nudenet import NudeClassifier 53 | 54 | # initialize classifier (downloads the checkpoint file automatically the first time) 55 | classifier = NudeClassifier() 56 | 57 | # Classify single image 58 | classifier.classify('path_to_image_1') 59 | # Returns {'path_to_image_1': {'safe': PROBABILITY, 'unsafe': PROBABILITY}} 60 | # Classify multiple images (batch prediction) 61 | # batch_size is optional; defaults to 4 62 | classifier.classify(['path_to_image_1', 'path_to_image_2'], batch_size=BATCH_SIZE) 63 | # Returns {'path_to_image_1': {'safe': PROBABILITY, 'unsafe': PROBABILITY}, 64 | # 'path_to_image_2': {'safe': PROBABILITY, 'unsafe': PROBABILITY}} 65 | ``` 66 | 67 | # Notes: 68 | - The current version of NudeDetector is trained on 160,000 entirely auto-labelled (using classification heat maps and 69 | various other hybrid techniques) images. 70 | - The entire data for the classifier is available at https://archive.org/details/NudeNet_classifier_dataset_v1 71 | - A part of the auto-labelled data (Images are from the classifier dataset above) used to train the base Detector is available at https://github.com/notAI-tech/NudeNet/releases/download/v0/DETECTOR_AUTO_GENERATED_DATA.zip 72 | -------------------------------------------------------------------------------- /fastDeploy_recipes/build_images.sh: -------------------------------------------------------------------------------- 1 | rm fastDeploy.py 2 | wget https://raw.githubusercontent.com/notAI-tech/fastDeploy/master/cli/fastDeploy.py 3 | 4 | python3 fastDeploy.py --build temp --source_dir classifier --verbose --base base-v0.1 --port 1238 5 | docker commit temp notaitech/nudenet:classifier 6 | docker push notaitech/nudenet:classifier 7 | 8 | rm fastDeploy.py 9 | 10 | -------------------------------------------------------------------------------- /fastDeploy_recipes/classifier/example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platelminto/NudeNetClassifier/05ad8a29b4a2474e6cc4f7f76c76e77e01b08744/fastDeploy_recipes/classifier/example.jpg -------------------------------------------------------------------------------- /fastDeploy_recipes/classifier/example.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platelminto/NudeNetClassifier/05ad8a29b4a2474e6cc4f7f76c76e77e01b08744/fastDeploy_recipes/classifier/example.pkl -------------------------------------------------------------------------------- /fastDeploy_recipes/classifier/predictor.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from nudenet import NudeClassifier 3 | 4 | classifier = NudeClassifier() 5 | 6 | 7 | """ 8 | 9 | Your function should take list of items as input 10 | This makes batching possible 11 | 12 | """ 13 | 14 | 15 | def predictor(in_images=[], batch_size=32): 16 | if not in_images: 17 | return [] 18 | preds = classifier.classify(in_images, batch_size=batch_size) 19 | 20 | preds = [preds.get(in_image) for in_image in in_images] 21 | 22 | preds = [{k: float(v) for k, v in pred.items()} if pred else pred for pred in preds] 23 | 24 | return preds 25 | 26 | 27 | if __name__ == "__main__": 28 | import json 29 | import pickle 30 | import base64 31 | 32 | example = ["example.jpg"] 33 | 34 | print(json.dumps(predictor(example))) 35 | 36 | example = { 37 | file_name: base64.b64encode(open(file_name, "rb").read()).decode("utf-8") 38 | for file_name in example 39 | } 40 | 41 | pickle.dump(example, open("example.pkl", "wb"), protocol=2) 42 | -------------------------------------------------------------------------------- /fastDeploy_recipes/classifier/requirements.txt: -------------------------------------------------------------------------------- 1 | nudenet==2.0.8 2 | -------------------------------------------------------------------------------- /nudenet/__init__.py: -------------------------------------------------------------------------------- 1 | from .classifier import Classifier as NudeClassifier 2 | -------------------------------------------------------------------------------- /nudenet/classifier.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import onnxruntime 5 | 6 | from .image_utils import load_images 7 | 8 | 9 | class Classifier: 10 | """ 11 | Class for loading model and running predictions. 12 | For example on how to use take a look the if __name__ == '__main__' part. 13 | """ 14 | 15 | nsfw_model = None 16 | 17 | def __init__(self): 18 | """ 19 | model = Classifier() 20 | """ 21 | dirname = os.path.dirname(__file__) 22 | model_path = os.path.join(dirname, "models/classifier_model.onnx") 23 | self.nsfw_model = onnxruntime.InferenceSession(model_path) 24 | 25 | def classify( 26 | self, 27 | image_paths=[], 28 | batch_size=4, 29 | image_size=(256, 256), 30 | categories=["unsafe", "safe"], 31 | ): 32 | """ 33 | inputs: 34 | image_paths: list of image paths or can be a string too (for single image) 35 | batch_size: batch_size for running predictions 36 | image_size: size to which the image needs to be resized 37 | categories: since the model predicts numbers, categories is the list of actual names of categories 38 | """ 39 | if not isinstance(image_paths, list): 40 | image_paths = [image_paths] 41 | 42 | loaded_images, loaded_image_paths = load_images( 43 | image_paths, image_size, image_names=image_paths 44 | ) 45 | 46 | if not loaded_image_paths: 47 | return {} 48 | 49 | preds = [] 50 | model_preds = [] 51 | while len(loaded_images): 52 | _model_preds = self.nsfw_model.run( 53 | [self.nsfw_model.get_outputs()[0].name], 54 | {self.nsfw_model.get_inputs()[0].name: loaded_images[:batch_size]}, 55 | )[0] 56 | model_preds.append(_model_preds) 57 | preds += np.argsort(_model_preds, axis=1).tolist() 58 | loaded_images = loaded_images[batch_size:] 59 | 60 | probs = [] 61 | for i, single_preds in enumerate(preds): 62 | single_probs = [] 63 | for j, pred in enumerate(single_preds): 64 | single_probs.append( 65 | model_preds[int(i / batch_size)][int(i % batch_size)][pred] 66 | ) 67 | preds[i][j] = categories[pred] 68 | 69 | probs.append(single_probs) 70 | 71 | images_preds = {} 72 | 73 | for i, loaded_image_path in enumerate(loaded_image_paths): 74 | if not isinstance(loaded_image_path, str): 75 | loaded_image_path = i 76 | 77 | images_preds[loaded_image_path] = {} 78 | for _ in range(len(preds[i])): 79 | images_preds[loaded_image_path][preds[i][_]] = float(probs[i][_]) 80 | 81 | return images_preds 82 | 83 | 84 | if __name__ == "__main__": 85 | m = Classifier() 86 | 87 | while 1: 88 | print( 89 | "\n Enter single image path or multiple images seperated by || (2 pipes) \n" 90 | ) 91 | images = input().split("||") 92 | images = [image.strip() for image in images] 93 | print(m.classify(images), "\n") 94 | -------------------------------------------------------------------------------- /nudenet/image_utils.py: -------------------------------------------------------------------------------- 1 | import io 2 | import cv2 3 | import logging 4 | import numpy as np 5 | 6 | from PIL import Image as pil_image 7 | 8 | if pil_image is not None: 9 | _PIL_INTERPOLATION_METHODS = { 10 | "nearest": pil_image.NEAREST, 11 | "bilinear": pil_image.BILINEAR, 12 | "bicubic": pil_image.BICUBIC, 13 | } 14 | # These methods were only introduced in version 3.4.0 (2016). 15 | if hasattr(pil_image, "HAMMING"): 16 | _PIL_INTERPOLATION_METHODS["hamming"] = pil_image.HAMMING 17 | if hasattr(pil_image, "BOX"): 18 | _PIL_INTERPOLATION_METHODS["box"] = pil_image.BOX 19 | # This method is new in version 1.1.3 (2013). 20 | if hasattr(pil_image, "LANCZOS"): 21 | _PIL_INTERPOLATION_METHODS["lanczos"] = pil_image.LANCZOS 22 | 23 | 24 | def load_img( 25 | path, grayscale=False, color_mode="rgb", target_size=None, interpolation="nearest" 26 | ): 27 | """Loads an image into PIL format. 28 | 29 | :param path: Path to image file. 30 | :param grayscale: DEPRECATED use `color_mode="grayscale"`. 31 | :param color_mode: One of "grayscale", "rgb", "rgba". Default: "rgb". 32 | The desired image format. 33 | :param target_size: Either `None` (default to original size) 34 | or tuple of ints `(img_height, img_width)`. 35 | :param interpolation: Interpolation method used to resample the image if the 36 | target size is different from that of the loaded image. 37 | Supported methods are "nearest", "bilinear", and "bicubic". 38 | If PIL version 1.1.3 or newer is installed, "lanczos" is also 39 | supported. If PIL version 3.4.0 or newer is installed, "box" and 40 | "hamming" are also supported. By default, "nearest" is used. 41 | 42 | :return: A PIL Image instance. 43 | """ 44 | if grayscale is True: 45 | logging.warn("grayscale is deprecated. Please use " 'color_mode = "grayscale"') 46 | color_mode = "grayscale" 47 | if pil_image is None: 48 | raise ImportError( 49 | "Could not import PIL.Image. " "The use of `load_img` requires PIL." 50 | ) 51 | 52 | if isinstance(path, (str, io.IOBase)): 53 | img = pil_image.open(path) 54 | else: 55 | path = cv2.cvtColor(path, cv2.COLOR_BGR2RGB) 56 | img = pil_image.fromarray(path) 57 | 58 | if color_mode == "grayscale": 59 | if img.mode != "L": 60 | img = img.convert("L") 61 | elif color_mode == "rgba": 62 | if img.mode != "RGBA": 63 | img = img.convert("RGBA") 64 | elif color_mode == "rgb": 65 | if img.mode != "RGB": 66 | img = img.convert("RGB") 67 | else: 68 | raise ValueError('color_mode must be "grayscale", "rgb", or "rgba"') 69 | if target_size is not None: 70 | width_height_tuple = (target_size[1], target_size[0]) 71 | if img.size != width_height_tuple: 72 | if interpolation not in _PIL_INTERPOLATION_METHODS: 73 | raise ValueError( 74 | "Invalid interpolation method {} specified. Supported " 75 | "methods are {}".format( 76 | interpolation, ", ".join(_PIL_INTERPOLATION_METHODS.keys()) 77 | ) 78 | ) 79 | resample = _PIL_INTERPOLATION_METHODS[interpolation] 80 | img = img.resize(width_height_tuple, resample) 81 | return img 82 | 83 | 84 | def img_to_array(img, data_format="channels_last", dtype="float32"): 85 | """Converts a PIL Image instance to a Numpy array. 86 | # Arguments 87 | img: PIL Image instance. 88 | data_format: Image data format, 89 | either "channels_first" or "channels_last". 90 | dtype: Dtype to use for the returned array. 91 | # Returns 92 | A 3D Numpy array. 93 | # Raises 94 | ValueError: if invalid `img` or `data_format` is passed. 95 | """ 96 | if data_format not in {"channels_first", "channels_last"}: 97 | raise ValueError("Unknown data_format: %s" % data_format) 98 | # Numpy array x has format (height, width, channel) 99 | # or (channel, height, width) 100 | # but original PIL image has format (width, height, channel) 101 | x = np.asarray(img, dtype=dtype) 102 | if len(x.shape) == 3: 103 | if data_format == "channels_first": 104 | x = x.transpose(2, 0, 1) 105 | elif len(x.shape) == 2: 106 | if data_format == "channels_first": 107 | x = x.reshape((1, x.shape[0], x.shape[1])) 108 | else: 109 | x = x.reshape((x.shape[0], x.shape[1], 1)) 110 | else: 111 | raise ValueError("Unsupported image shape: %s" % (x.shape,)) 112 | return x 113 | 114 | 115 | def load_images(image_paths, image_size, image_names): 116 | """ 117 | Function for loading images into numpy arrays for passing to model.predict 118 | inputs: 119 | image_paths: list of image paths to load 120 | image_size: size into which images should be resized 121 | 122 | outputs: 123 | loaded_images: loaded images on which keras model can run predictions 124 | loaded_image_indexes: paths of images which the function is able to process 125 | 126 | """ 127 | loaded_images = [] 128 | loaded_image_paths = [] 129 | 130 | for i, img_path in enumerate(image_paths): 131 | try: 132 | image = load_img(img_path, target_size=image_size) 133 | image = img_to_array(image) 134 | image /= 255 135 | loaded_images.append(image) 136 | loaded_image_paths.append(image_names[i]) 137 | except Exception as ex: 138 | logging.exception(f"Error reading {img_path} {ex}", exc_info=True) 139 | 140 | return np.asarray(loaded_images), loaded_image_paths 141 | -------------------------------------------------------------------------------- /nudenet/models/classifier_model.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platelminto/NudeNetClassifier/05ad8a29b4a2474e6cc4f7f76c76e77e01b08744/nudenet/models/classifier_model.onnx -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Note: To use the 'upload' functionality of this file, you must: 5 | # $ pip install twine 6 | 7 | import io 8 | import os 9 | import sys 10 | from shutil import rmtree 11 | 12 | from setuptools import find_packages, setup, Command 13 | 14 | # Package meta-data. 15 | NAME = 'NudeNetClassifier' 16 | DESCRIPTION = 'A Neural Net for Nudity Detection. Classifier only.' 17 | URL = 'https://github.com/platelminto/NudeNetClassifier' 18 | EMAIL = 'gmomigliano@protonmail.com' 19 | AUTHOR = 'Giorgio Momigliano' 20 | REQUIRES_PYTHON = '>=3.6.0' 21 | VERSION = '2.1.1' 22 | 23 | # What packages are required for this module to be executed? 24 | REQUIRED = [ 25 | 'pillow', 26 | 'opencv-python-headless>=4.5.1.48', 27 | 'scikit-image', 28 | 'onnxruntime', 29 | ] 30 | 31 | # What packages are optional? 32 | EXTRAS = { 33 | # 'fancy feature': ['django'], 34 | } 35 | 36 | # The rest you shouldn't have to touch too much :) 37 | # ------------------------------------------------ 38 | # Except, perhaps the License and Trove Classifiers! 39 | # If you do change the License, remember to change the Trove Classifier for that! 40 | 41 | here = os.path.abspath(os.path.dirname(__file__)) 42 | 43 | # Import the README and use it as the long-description. 44 | # Note: this will only work if 'README.md' is present in your MANIFEST.in file! 45 | try: 46 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 47 | long_description = '\n' + f.read() 48 | except FileNotFoundError: 49 | long_description = DESCRIPTION 50 | 51 | # Load the package's __version__.py module as a dictionary. 52 | about = {} 53 | if not VERSION: 54 | with open(os.path.join(here, NAME, '__version__.py')) as f: 55 | exec(f.read(), about) 56 | else: 57 | about['__version__'] = VERSION 58 | 59 | 60 | class UploadCommand(Command): 61 | """Support setup.py upload.""" 62 | 63 | description = 'Build and publish the package.' 64 | user_options = [] 65 | 66 | @staticmethod 67 | def status(s): 68 | """Prints things in bold.""" 69 | print('\033[1m{0}\033[0m'.format(s)) 70 | 71 | def initialize_options(self): 72 | pass 73 | 74 | def finalize_options(self): 75 | pass 76 | 77 | def run(self): 78 | try: 79 | self.status('Removing previous builds…') 80 | rmtree(os.path.join(here, 'dist')) 81 | except OSError: 82 | pass 83 | 84 | self.status('Building Source and Wheel (universal) distribution…') 85 | os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 86 | 87 | self.status('Uploading the package to PyPI via Twine…') 88 | os.system('twine upload dist/*') 89 | 90 | self.status('Pushing git tags…') 91 | os.system('git tag v{0}'.format(about['__version__'])) 92 | os.system('git push --tags') 93 | 94 | sys.exit() 95 | 96 | 97 | # Where the magic happens: 98 | setup( 99 | name=NAME, 100 | version=about['__version__'], 101 | description=DESCRIPTION, 102 | long_description=long_description, 103 | long_description_content_type='text/markdown', 104 | author=AUTHOR, 105 | author_email=EMAIL, 106 | python_requires=REQUIRES_PYTHON, 107 | url=URL, 108 | packages=find_packages(exclude=('tests',)), 109 | data_files=[('nudenet/models', ['nudenet/models/classifier_model.onnx'])], 110 | # If your package is a single module, use this instead of 'packages': 111 | # py_modules=['mypackage'], 112 | 113 | # entry_points={ 114 | # 'console_scripts': ['mycli=mymodule:cli'], 115 | # }, 116 | install_requires=REQUIRED, 117 | extras_require=EXTRAS, 118 | include_package_data=True, 119 | license='GPLv3', 120 | classifiers=[ 121 | # Trove classifiers 122 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 123 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 124 | 'Programming Language :: Python', 125 | 'Programming Language :: Python :: 3', 126 | 'Programming Language :: Python :: 3.6', 127 | 'Programming Language :: Python :: Implementation :: CPython', 128 | 'Programming Language :: Python :: Implementation :: PyPy' 129 | ], 130 | # $ setup.py publish support. 131 | cmdclass={ 132 | 'upload': UploadCommand, 133 | }, 134 | ) 135 | --------------------------------------------------------------------------------