[
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto\n"
  },
  {
    "path": ".gitignore",
    "content": "/.idea/*\n/.venv/*"
  },
  {
    "path": "app.py",
    "content": "from flask import Flask, jsonify, request, redirect\nfrom flask_cors import CORS, cross_origin\nfrom flask_sqlalchemy import SQLAlchemy\nfrom sqlalchemy.sql import func\nfrom flask_migrate import Migrate\nfrom datetime import datetime\n\napp = Flask(__name__)\ncors = CORS(app, resources={r\"/*\": {\"origins\": \"*\"}})\napp.config['CORS_HEADERS'] = 'Content-Type'\napp.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'\napp.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False\n\ndb = SQLAlchemy(app)\n\ndb.init_app(app)\nmigrate = Migrate(app, db)\n\n\nclass ImageRecord(db.Model):\n    id = db.Column(db.Integer, primary_key=True)\n    gender = db.Column(db.String(10))\n    age = db.Column(db.Integer)\n\n    filename = db.Column(db.String(100))\n    hosting = db.Column(db.String(100), default=\"local\")\n\n    def image_url(self):\n        output = \"\"\n        if self.hosting == \"local\":\n            output = \"https://content.fakeface.rest/\" + self.filename\n        return output\n\n    def thumb_url(self):\n        output = \"\"\n        if self.hosting == \"local\":\n            output = \"https://thumb.fakeface.rest/thumb_\" + self.filename\n        return output\n\n    date_added = db.Column(db.DateTime)\n    source = db.Column(db.String(100))\n\n    last_served = db.Column(db.DateTime)\n\n    created_at = db.Column(db.DateTime)\n    updated_at = db.Column(db.DateTime)\n    is_deleted = db.Column(db.DateTime)\n    deleted_at = db.Column(db.Boolean)\n\n\n@app.route('/')\ndef index():\n    return redirect (\"https://hankhank10.github.io/fakeface/\", code=307)\n\n\ndef get_url(gender = \"\", minimum_age = 0, maximum_age = 0, thumb=False):\n    if gender == '':\n        db_output = ImageRecord.query.filter(ImageRecord.age >= minimum_age, ImageRecord.age <= maximum_age).order_by(func.random()).first_or_404()\n\n    if gender != '':\n        db_output = ImageRecord.query.filter(ImageRecord.gender == gender, ImageRecord.age >= minimum_age, ImageRecord.age <= maximum_age).order_by(func.random()).first_or_404()\n\n    db_output.last_served = datetime.utcnow()\n    db.session.commit()\n    if thumb == False: return db_output.image_url()\n    if thumb == True: return db_output.thumb_url()\n\n\n@app.route('/face/json')\ndef output_json():\n    gender = request.args.get('gender', '')\n    minimum_age = request.args.get('minimum_age', 0)\n    maximum_age = request.args.get('maximum_age', 99)\n\n    if gender == '':\n        db_output = ImageRecord.query.filter(ImageRecord.age >= minimum_age, ImageRecord.age <= maximum_age).order_by(func.random()).first_or_404()\n\n    if gender != '':\n        db_output = ImageRecord.query.filter(ImageRecord.gender == gender, ImageRecord.age >= minimum_age, ImageRecord.age <= maximum_age).order_by(func.random()).first_or_404()\n\n    dict_output = {\n        'gender': db_output.gender,\n        'age': db_output.age,\n        'filename': db_output.filename,\n        'date_added': db_output.date_added,\n        'source': db_output.source,\n        'image_url': db_output.image_url(),\n        'last_served': db_output.last_served\n    }\n\n    db_output.last_served = datetime.utcnow()\n    db.session.commit()\n\n    return jsonify(dict_output)\n\n\n@app.route ('/face/view')\n@app.route ('/face/view/<x>')\ndef output_redirect_image(x = 0):\n    gender = request.args.get('gender', '')\n    minimum_age = request.args.get('minimum_age', 0)\n    maximum_age = request.args.get('maximum_age', 99)\n\n    url_to_show = get_url(gender, minimum_age, maximum_age, False)\n    return redirect (url_to_show)\n\n\n@app.route ('/thumb/view')\n@app.route ('/thumb/view/<x>')\ndef output_redirect_thumb(x = 0):\n    gender = request.args.get('gender', '')\n    minimum_age = request.args.get('minimum_age', 0)\n    maximum_age = request.args.get('maximum_age', 99)\n\n    url_to_show = get_url(gender, minimum_age, maximum_age, True)\n    return redirect(url_to_show)\n\n\n@app.route('/stats')\ndef stats():\n    stats_count = ImageRecord.query.count()\n    return (str(stats_count) + \" faces\")\n\n\n@app.after_request\ndef set_response_headers(response):\n    response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'\n    response.headers['Pragma'] = 'no-cache'\n    response.headers['Expires'] = '0'\n    response.headers['Access-Control-Allow-Origin'] = '*'\n    return response\n\nif __name__ == '__main__':\n    app.run(host='0.0.0.0', port=11000)\n"
  },
  {
    "path": "generate_faces.py",
    "content": "import requests\nimport shutil\nimport cv2\nimport secrets\nfrom pyagender import PyAgender\nimport time\nfrom datetime import datetime\n\nfrom time import sleep\n\nfrom active_alchemy import ActiveAlchemy\n\ndb = ActiveAlchemy('sqlite:///db.sqlite')\n\n# settings\nurl = \"https://thispersondoesnotexist.com/image\"\nmale_threshold = 0.4\nfemale_threshold = 0.6\ntemp_file = \"temp_img.jpg\"\ntimes_to_run = 500\nseconds_to_sleep = 2\n\n\nclass ImageRecord(db.Model):\n    id = db.Column(db.Integer, primary_key=True)\n    gender = db.Column(db.String(10))\n    age = db.Column(db.Integer)\n\n    filename = db.Column(db.String(100))\n    hosting = db.Column(db.String(100), default=\"local\")\n\n    def image_url(self):\n        if self.hosting == \"local\":\n            output = \"https://fakeface.rest/to_be_uploaded_to_static_host/\" + self.filename\n        return output\n\n    date_added = db.Column(db.DateTime)\n    source = db.Column(db.String(100))\n\n    last_served = db.Column(db.DateTime)\n\n    created_at = db.Column(db.DateTime)\n    updated_at = db.Column(db.DateTime)\n    is_deleted = db.Column(db.DateTime)\n    deleted_at = db.Column(db.Boolean)\n\n\ndef download_face():\n    response = requests.get(url, stream=True)\n    with open(temp_file, 'wb') as out_file:\n        shutil.copyfileobj(response.raw, out_file)\n    return\n\n\ndef recoginise_face():\n    faces = agender.detect_genders_ages(cv2.imread(temp_file))\n    if len(faces) == 1:\n        face = faces[0]\n        gender_numeric = face['gender']\n        age = int(face['age'])\n\n        gender = \"unclear\"\n        if gender_numeric < male_threshold: gender = \"male\"\n        if gender_numeric > male_threshold: gender = \"female\"\n    else:\n        #face not detected or multiple faces detected\n        gender = \"unclear\"\n        age = 0\n\n    return gender, age\n\n\ndef move_file(gender, age):\n    filename = gender + \"_\" + str(age) + \"_\" + secrets.token_hex(20) + \".jpg\"\n    location_to_move_to = \"static/classified/\" + filename\n    shutil.move(temp_file, location_to_move_to)\n    return filename\n\n\ndef write_db(gender, age, filename):\n    image_record = ImageRecord(\n        gender=gender,\n        age=age,\n        filename=filename,\n        date_added=datetime.utcnow(),\n        source=\"thispersondoesnotexist\",\n        hosting=\"local\",\n        last_served=datetime.utcnow()\n    )\n\n    db.session.add(image_record)\n    db.session.commit()\n    return\n\n\nagender = PyAgender()\nstarttime = time.time()\n\nfor a in range(1, times_to_run):\n    download_face()\n    gender, age = recoginise_face()\n    if gender != \"unclear\":\n        print(str(age) + \" year old \" + gender)\n        filename = move_file(gender, age)\n        write_db (gender, age, filename)\n    else:\n        print(\"gender unclear, so skipping\")\n    sleep(seconds_to_sleep)\n\n"
  },
  {
    "path": "migrations/README",
    "content": "Generic single-database configuration."
  },
  {
    "path": "migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom sqlalchemy import engine_from_config\nfrom sqlalchemy import pool\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nfrom flask import current_app\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url, target_metadata=target_metadata, literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = engine_from_config(\n        config.get_section(config.config_ini_section),\n        prefix='sqlalchemy.',\n        poolclass=pool.NullPool,\n    )\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "migrations/versions/dcb1be9b8bcc_.py",
    "content": "\"\"\"empty message\n\nRevision ID: dcb1be9b8bcc\nRevises: \nCreate Date: 2020-08-02 14:59:29.227831\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'dcb1be9b8bcc'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('image_record',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('gender', sa.String(length=10), nullable=True),\n    sa.Column('age', sa.Integer(), nullable=True),\n    sa.Column('filename', sa.String(length=100), nullable=True),\n    sa.Column('hosting', sa.String(length=100), nullable=True),\n    sa.Column('date_added', sa.DateTime(), nullable=True),\n    sa.Column('source', sa.String(length=100), nullable=True),\n    sa.Column('last_served', sa.DateTime(), nullable=True),\n    sa.Column('created_at', sa.DateTime(), nullable=True),\n    sa.Column('updated_at', sa.DateTime(), nullable=True),\n    sa.Column('is_deleted', sa.DateTime(), nullable=True),\n    sa.Column('deleted_at', sa.Boolean(), nullable=True),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('image_record')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "readme.md",
    "content": "# This API is deprecated and no longer hosted. You are welcome to make use of the code to host your own version.\n\n\nThis API returns image URLs of fake human faces generated by [thispersondoesnotexist](https://thispersondoesnotexist.com).\n\n![alt text](https://fakeface.rest/thumb/view/7 \"Dynamically generated image\")\n\nEach image has been pre-analysed by an AI algorithm called [pypy-agender](https://github.com/aristofun/py-agender) to identify gender and age (obviously non-exact).\n\nAs such, the user of the API can specify gender, minimum and maximum age for the image to be returned.\n\nData can be returned via JSON format, a direct redirect to the image or a simple response with the image URL.\n\n\n# JSON data for a face\n\n### Endpoint\nhttps://fakeface.rest/face/json\n\n### Optional query parameters\n\n* `gender` : accepts \"male\" or \"female\"; defaults to both if not provided\n\n* `minimum_age` : integer\n\n* `maximum_age` : integer\n\n### Response\n````\n{\n  age: 45,\n  date_added: \"Sun, 02 Aug 2020 22:08:56 GMT\",\n  filename: \"female_45_b3e57178eb323fee36df8e8b4690c11ef82f3baa.jpg\",\n  gender: \"female\",\n  image_url: \"https://content.fakeface.rest/female_45_b3e57178eb323fee36df8e8b4690c11ef82f3baa.jpg\",\n  last_served: \"Sun, 02 Aug 2020 22:08:56 GMT\",\n  source: \"thispersondoesnotexist\"\n}\n````\n\n### Example queries:\n\n<https://fakeface.rest/face/json>\n\n<https://fakeface.rest/face/json?gender=male>\n\n<https://fakeface.rest/face/json?gender=female&minimum_age=35>\n\n<https://fakeface.rest/face/json?maximum_age=50&gender=female>\n\n# Redirect to a face\n\n### Endpoint\nhttps://fakeface.rest/face/view\n\n### Query parameters\n(same as above for JSON)\n\n### Response\n\nBrowser redirects right to image\n\n### Example queries:\n\n<https://fakeface.rest/face/view>\n\n<https://fakeface.rest/face/view?gender=male>\n\n### Inserting into HTML\n\nThe above address can be used in the src for an img in HTML to dynamically generate a new face on each load:\n\n![alt text](https://fakeface.rest/face/view?gender=female \"Dynamically generated image\")\n\nIf you want to insert multiple different faces and prevent the browser caching then you can append any number or random string to the end of the endpoint as follows:\n\n<https://fakeface.rest/face/view/55?gender=male>\n\n<https://fakeface.rest/face/view/anythingcangohere_theapidoesntdoanythingwithit?gender=male>\n\n\n# Redirect to a thumbnail of a face\n\n### Endpoint\nhttps://fakeface.rest/thumb/view\n\n### Query parameters\n(same as above for JSON)\n\n### Response\n\nBrowser redirects right to thumbnail (350x350 maximum) image.\n\n### Example queries:\n\n<https://fakeface.rest/thumb/view>\n\n<https://fakeface.rest/thumb/view?gender=male>\n\n### Inserting into html:\n\n![alt text](https://fakeface.rest/thumb/view/77 \"Dynamically generated image\")\n\n![alt text](https://fakeface.rest/thumb/view/66 \"Dynamically generated image\")\n\n\n# Licence\n\nAll of these images are generated by https://thispersondoesnotexist.com and are provided for usage only as allowed by that project's creators.\n\n# Credits\n\nAll the hard work was done by the makers of https://thispersondoesnotexist.com and [pypy-agender](https://github.com/aristofun/py-agender)\n"
  },
  {
    "path": "requirements.txt",
    "content": "alembic==1.8.1\nclick==8.1.3\nFlask==2.2.2\nFlask-Cors==3.0.10\nFlask-Migrate==3.1.0\nFlask-SQLAlchemy==2.5.1\nitsdangerous==2.1.2\nJinja2==3.1.2\nMako==1.2.2\nMarkupSafe==2.1.1\nsix==1.16.0\nSQLAlchemy==1.4.40\nWerkzeug==2.2.2\n"
  },
  {
    "path": "resize.py",
    "content": "from PIL import Image\nimport pathlib\n\nmaxsize = (350,350)\n\nfor input_img_path in pathlib.Path(\"input\").iterdir():\n    output_img_path = str(input_img_path).replace(\"classified/\",\"thumb/thumb_\")\n    with Image.open(input_img_path) as im:\n        im.thumbnail(maxsize)\n        im.save(output_img_path, \"JPEG\", dpi=(300,300))\n        print(f\"processing file {input_img_path} done...\")"
  }
]