diff --git a/README.md b/README.md index 79ea08c4..3fcacd69 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,25 @@ shape: (1, 512), dtype: uint8 42 181 38 254 177 232 150 99]] ``` +# Server and hydrus compatibility + +This feature is only for python3. + +It is tested on python 3.6.5 on ubuntu 18.04. + +``i2v`` can run a local server by doing the following: + +- put `illust2vec_tag_ver200.caffemodel` and `tag_list.json` on current working command +- to run on on `127.0.0.1` host and port `5011` run following command: +```shell + $ i2v run -h 127.0.0.1 -p 5011 +``` + +Hydrus can use that as parsing by importing following config: +```json +[32, "illustration2vec", 2, ["http://127.0.0.1:5011/image/new/?url=%2Fimage%2Fplausible-tag", 1, 0, [55, 1, [[], "some hash bytes"]], "path", {}, [[29, 1, ["match parser", [27, 6, [[26, 1, [[62, 2, [0, "a", {"id": "hydrus-link"}, null, null, false, [51, 1, [3, "", null, null, "example string"]]]]]], 0, "href", [51, 1, [3, "", null, null, "example string"]], [55, 1, [[], "parsed information"]]]], [[30, 3, ["tags creator", 0, [27, 6, [[26, 1, [[62, 2, [0, "td", {"class": "tag-creator"}, null, null, false, [51, 1, [3, "", null, null, "example string"]]]]]], 1, "", [51, 1, [3, "", null, null, "example string"]], [55, 1, [[], "parsed information"]]]], "creator"]], [30, 3, ["tags series", 0, [27, 6, [[26, 1, [[62, 2, [0, "td", {"class": "tag-copyright"}, null, null, false, [51, 1, [3, "", null, null, "example string"]]]]]], 1, "", [51, 1, [3, "", null, null, "example string"]], [55, 1, [[], "parsed information"]]]], "series"]], [30, 3, ["tags character", 0, [27, 6, [[26, 1, [[62, 2, [0, "td", {"class": "tag-character"}, null, null, false, [51, 1, [3, "", null, null, "example string"]]]]]], 1, "", [51, 1, [3, "", null, null, "example string"]], [55, 1, [[], "parsed information"]]]], "character"]], [30, 3, ["tags unnamespaced", 0, [27, 6, [[26, 1, [[62, 2, [0, "td", {"class": "tag-general"}, null, null, false, [51, 1, [3, "", null, null, "example string"]]]]]], 1, "", [51, 1, [3, "", null, null, "example string"]], [55, 1, [[], "parsed information"]]]], ""]], [30, 3, ["tags rating", 0, [27, 6, [[26, 1, [[62, 2, [0, "td", {"class": "tag-rating", "data-index": "1"}, null, null, false, [51, 1, [3, "", null, null, "example string"]]]]]], 1, "", [51, 1, [3, "", null, null, "example string"]], [55, 1, [[], "parsed information"]]]], "rating"]]]]]]]] +``` + # License The pre-trained models and the other files we have provided are licensed under the MIT License. diff --git a/i2v/__main__.py b/i2v/__main__.py new file mode 100644 index 00000000..80fba261 --- /dev/null +++ b/i2v/__main__.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +from pprint import pprint +import os +import os.path as op +import sys + +from flask import Flask, __version__ as flask_version, send_from_directory +from flask.cli import FlaskGroup +from flask_admin import Admin +from PIL import Image +import click + +from . import make_i2v_with_chainer, views, models + + +__version__ = '0.2.1' + + +class CustomFlaskGroup(FlaskGroup): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.params[0].help = 'Show the program version' + self.params[0].callback = get_custom_version + + +def get_custom_version(ctx, param, value): + if not value or ctx.resilient_parsing: + return + message = 'Illustration2Vec %(app_version)s\nFlask %(version)s\nPython %(python_version)s' + click.echo(message % { + 'app_version': __version__, + 'version': flask_version, + 'python_version': sys.version, + }, color=ctx.color) + ctx.exit() + + +def create_app(): + app = Flask(__name__) + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = 'True' + app.config['DATABASE_FILE'] = 'i2v.sqlite' + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE_FILE'] + app.config['SECRET_KEY'] = os.getenv('ILLUSTRATION2VEC_SECRET_KEY') or os.urandom(24) + # Create directory for file fields to use + try: + os.mkdir(models.file_path) + app.logger.debug('File path created') + except OSError: + pass + # app and db + models.db.init_app(app) + app.app_context().push() + models.db.create_all() + # other setup + admin = Admin( + app, name='Illustration2Vec', template_mode='bootstrap3', + index_view=views.HomeView(url='/') + ) + admin.add_view(views.ImageView(models.Image, models.db.session)) + admin.add_view(views.ChecksumView(models.Checksum, models.db.session)) + admin.add_view(views.TagEstimationView(models.TagEstimation, models.db.session)) + app.add_url_rule('/file/', 'file', view_func=lambda filename: send_from_directory(models.file_path, filename)) + app.logger.debug('file path: {}'.format(models.file_path)) + return app + + +@click.group(cls=CustomFlaskGroup, create_app=create_app) +def cli(): + """Illustration2Vec.""" + pass + + +@cli.command() +@click.option('--output', help='Output format;[default]/pprint', default='default') +@click.argument('images', nargs=-1) +def estimate_plausible_tags(images, output='default'): + """Estimate plausible tags.""" + illust2vec = make_i2v_with_chainer( + "illust2vec_tag_ver200.caffemodel", "tag_list.json") + image_sets = map(lambda x: {'value': x, 'sha256': models.sha256_checksum(x)}, images) + for idx, img_set in enumerate(image_sets): + img = Image.open(img_set['value']) + res = illust2vec.estimate_plausible_tags([img], threshold=0.5) + if isinstance(res[idx]['rating'], zip): + res[idx]['rating'] = list(res[idx]['rating']) + print("image: {}\nsha256: {}".format( + img_set['value'], img_set['sha256'])) + if output == 'pprint': + pprint(res) + else: + print(res) + + +if __name__ == '__main__': + cli() diff --git a/i2v/chainer_i2v.py b/i2v/chainer_i2v.py index 30ee815f..388500ce 100644 --- a/i2v/chainer_i2v.py +++ b/i2v/chainer_i2v.py @@ -2,11 +2,16 @@ import json import warnings import numpy as np +from distutils.version import StrictVersion from scipy.ndimage import zoom from skimage.transform import resize +import chainer from chainer import Variable from chainer.functions import average_pooling_2d, sigmoid -from chainer.functions.caffe import CaffeFunction +try: + from chainer.functions.caffe import CaffeFunction +except: + from chainer.links.caffe import CaffeFunction class ChainerI2V(Illustration2VecBase): @@ -47,7 +52,14 @@ def _forward(self, inputs, layername): input_ -= self.mean # subtract mean input_ = input_.transpose((0, 3, 1, 2)) # (N, H, W, C) -> (N, C, H, W) x = Variable(input_) - y, = self.net(inputs={'data': x}, outputs=[layername], train=False) + + # train argument is not supported from Ver2. + if StrictVersion(chainer.__version__) < StrictVersion('2.0.0'): + y, = self.net(inputs={'data': x}, outputs=[layername], train=False) + else: + chainer.using_config('train', False) + y, = self.net(inputs={'data': x}, outputs=[layername]) + return y def _extract(self, inputs, layername): diff --git a/i2v/models.py b/i2v/models.py new file mode 100644 index 00000000..2d17f77e --- /dev/null +++ b/i2v/models.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Model module.""" +from datetime import datetime +import hashlib +import os +import os.path as op + +from appdirs import user_data_dir +from flask_admin import form +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.event import listens_for +from sqlalchemy.types import TIMESTAMP +from sqlalchemy_utils.types.choice import ChoiceType + +MODE_PLAUSIBLE_TAG = 'plausible' +MODE_TOP_TAG = 'top' +db = SQLAlchemy() +file_path = op.join(user_data_dir('Illustration2Vec', 'Masaki Saito'), 'files') + + +class Base(db.Model): + __abstract__ = True + id = db.Column(db.Integer, primary_key=True) + created_at = db.Column(TIMESTAMP, default=datetime.now, nullable=False) + + +class Image(Base): + id = db.Column(db.Integer, primary_key=True) + path = db.Column(db.String) + checksum_id = db.Column(db.Integer, db.ForeignKey('checksum.id')) + checksum = db.relationship( + 'Checksum', foreign_keys='Image.checksum_id', lazy='subquery', + backref=db.backref('images', lazy=True, cascade='delete')) + + def __repr__(self): + return ''.format(self) + + @property + def full_path(self): + return os.path.join(file_path, self.path) + + def update_checksum(self, session=None): + session = db.session if session is None else session + checksum_val = sha256_checksum(self.full_path) + self.checksum = get_or_create(session, Checksum, value=checksum_val)[0] + + @property + def thumbgen_filename(self): + return form.thumbgen_filename(self.path) + + +def sha256_checksum(filename, block_size=65536): + sha256 = hashlib.sha256() + with open(filename, 'rb') as f: + for block in iter(lambda: f.read(block_size), b''): + sha256.update(block) + return sha256.hexdigest() + + +@listens_for(Image, 'after_delete') +def del_image(mapper, connection, target): + if target.path: + # Delete image + try: + os.remove(op.join(file_path, target.path)) + except OSError: + pass + # Delete thumbnail + try: + os.remove(op.join(file_path, form.thumbgen_filename(target.path))) + except OSError: + pass + + +class Checksum(Base): + value = db.Column(db.String, unique=True) + + def update_tag_estimation(self, tags, mode=MODE_PLAUSIBLE_TAG, session=None): + session = db.session if session is None else session + for nm, list_value in tags.items(): + if list_value: + for tag_value, estimation_value in list_value: + tag_model = get_or_create_tag( + value=str(tag_value), namespace=nm, session=session)[0] + e_item = get_or_create( + session, TagEstimation, + checksum=self, tag=tag_model, mode=mode)[0] + e_item.value = estimation_value + yield e_item + + def __repr__(self): + return ''.format(self) + + def get_estimated_tags(self, mode=MODE_PLAUSIBLE_TAG): + res = {'character': [], 'copyright': [], 'general': [], 'rating': []} + for estimation in self.tag_estimations: + if estimation.mode == mode: + res.setdefault(estimation.tag.namespace.value, []).append( + (estimation.tag.value, estimation.value)) + return res + + +class TagEstimation(Base): + MODES = [ + (MODE_PLAUSIBLE_TAG, MODE_PLAUSIBLE_TAG), + (MODE_TOP_TAG, MODE_TOP_TAG), + ] + checksum_id = db.Column(db.Integer, db.ForeignKey('checksum.id')) + checksum = db.relationship( + 'Checksum', foreign_keys='TagEstimation.checksum_id', lazy='subquery', + backref=db.backref('tag_estimations', lazy=True)) + tag_id = db.Column(db.Integer, db.ForeignKey('tag.id')) + tag = db.relationship( + 'Tag', foreign_keys='TagEstimation.tag_id', lazy='subquery', + backref=db.backref('tag_estimations', lazy=True)) + value = db.Column(db.Float) + mode = db.Column(ChoiceType(MODES)) + + def __repr__(self): + templ = \ + '' + return templ.format( + self, '{0:.2f}'.format(self.value * 100) + ) + + +def get_or_create_tag(value, namespace=None, session=None): + session = db.session if session is None else session + kwargs = dict(value=value) + if namespace: + namespace_model = get_or_create(session, Namespace, value=namespace)[0] + kwargs['namespace'] = namespace_model + model, created = get_or_create(session, Tag, **kwargs) + return model, created + + +class Tag(Base): + value = db.Column(db.String) + namespace_id = db.Column(db.Integer, db.ForeignKey('namespace.id')) + namespace = db.relationship( + 'Namespace', foreign_keys='Tag.namespace_id', lazy='subquery', + backref=db.backref('tags', lazy=True)) + + @property + def fullname(self): + res = '' + if self.namespace: + res = self.namespace.value + ':' + res += self.value + return res + + def __repr__(self): + return ''.format(self) + + +class Namespace(Base): + value = db.Column(db.String, unique=True) + + +def get_or_create(session, model, **kwargs): + """Creates an object or returns the object if exists.""" + instance = session.query(model).filter_by(**kwargs).first() + created = False + if not instance: + instance = model(**kwargs) + session.add(instance) + created = True + return instance, created diff --git a/i2v/templates/i2v/home.html b/i2v/templates/i2v/home.html new file mode 100644 index 00000000..947ff1d5 --- /dev/null +++ b/i2v/templates/i2v/home.html @@ -0,0 +1,7 @@ +{% extends 'admin/master.html' %} +{% block head %} + +{% endblock %} +{% block body %} +

hello world

+{% endblock %} diff --git a/i2v/templates/i2v/image_tag.html b/i2v/templates/i2v/image_tag.html new file mode 100644 index 00000000..35eb5ed1 --- /dev/null +++ b/i2v/templates/i2v/image_tag.html @@ -0,0 +1,114 @@ +{% extends 'admin/master.html' %} +{% block head %} + +{% endblock %} +{% block body %} +
+
+
+ +
+ +
+
+
+
+
+ + Cancel +
+
+
+
+
+ +
+
+ +
+ +
+ + {% for tag_namespace, tag_list in estimated_tags.items() %} + + + + + + + {% for tag, tag_confidence in tag_list %} + + + + + + + {% endfor %} + {% endfor %} +
#{{tag_namespace|capitalize}} TagConfidence
{{loop.index}}{{tag}} +
+
+ {{'%0.1f' | format(tag_confidence*100)}} +
+
+
+ + + + + + + + + + + + + + + + +
+
+ + +
+ +
+{% endblock %} +{% block tail_js %} +{{super()}} + +{% endblock %} diff --git a/i2v/views.py b/i2v/views.py new file mode 100644 index 00000000..f1022c6b --- /dev/null +++ b/i2v/views.py @@ -0,0 +1,221 @@ +import hashlib +import os +import shutil +import time + +from flask import flash, redirect, request, url_for +from flask_admin import AdminIndexView, expose, BaseView, form +from flask_admin.babel import gettext +from flask_admin.contrib.sqla import ModelView, filters +from flask_admin.helpers import get_redirect_target +from flask_admin.model.helpers import get_mdict_item_or_list +from jinja2 import Markup +from PIL import Image +from werkzeug import secure_filename +import arrow +import structlog + +from . import models +from . import make_i2v_with_chainer + + +logger = structlog.getLogger(__name__) +ILLUST2VEC = None + + +class ChecksumView(ModelView): + + # can_export = True + column_formatters = { + 'value': lambda v, c, m, n: Markup('{}'.format( + url_for('tagestimation.index_view', flt4_checksum_value_equals=getattr(m, n)), + getattr(m, n) + )) + } + edit_modal = True + # export_types = ['json'] + + +class HomeView(AdminIndexView): + + @expose('/') + def index(self): + return self.render('i2v/home.html') + + +class ImageView(ModelView): + + def _list_thumbnail(view, context, model, name): + res_templ = 'Plausible Tag' + res = res_templ.format(url_for( + '.plausible_tag_view', id=model.id)) + res_templ = 'Top Tag' + res += res_templ.format(url_for( + '.top_tag_view', id=model.id)) + res += '
' + if not model.path: + return Markup(res) + res += '' % url_for( + 'file', filename=form.thumbgen_filename(model.path)) + return Markup(res) + + can_view_details = True + column_default_sort = ('created_at', True) + create_modal = True + form_excluded_columns = ('checksum', 'created_at') + form_extra_fields = { + 'path': form.ImageUploadField( + 'Image', base_path=models.file_path, thumbnail_size=(100, 100, True)) + } + column_formatters = { + 'path': _list_thumbnail, + 'checksum': lambda v, c, m, n: m.checksum.value[:7] if m.checksum else '', + 'created_at': + lambda v, c, m, n: + Markup('

{}

'.format( + m.created_at, + arrow.Arrow.fromdatetime(m.created_at, tzinfo='local').humanize(arrow.now()) + )), + } + + @expose('/plausible-tag') + def plausible_tag_view(self): + return self._tag_view_base(mode=models.MODE_PLAUSIBLE_TAG) + + @expose('/top-tag') + def top_tag_view(self): + return self._tag_view_base(mode=models.MODE_TOP_TAG) + + def _tag_view_base(self, mode): + return_url = get_redirect_target() or self.get_url('.index_view') + id = get_mdict_item_or_list(request.args, 'id') + if id is None: + return redirect(return_url) + model = self.get_one(id) + if model is None: + flash(gettext('Record does not exist.'), 'error') + return redirect(return_url) + if mode not in [models.MODE_PLAUSIBLE_TAG, models.MODE_TOP_TAG]: + flash(gettext('Unknown mode.'), 'error') + return redirect(return_url) + estimated_tags = model.checksum.get_estimated_tags(mode=mode) + if not any(estimated_tags.values()): + img = Image.open(model.full_path) + start_time = time.time() + global ILLUST2VEC + if not ILLUST2VEC: + ILLUST2VEC = make_i2v_with_chainer( + "illust2vec_tag_ver200.caffemodel", "tag_list.json") + illust2vec = ILLUST2VEC + end = time.time() + logger.debug('i2v initiated', time=(time.time() - start_time)) + if mode == models.MODE_PLAUSIBLE_TAG: + res = illust2vec.estimate_plausible_tags([img]) + else: + res = illust2vec.estimate_top_tags([img]) + res = res[0] + session = models.db.session + tags = list(model.checksum.update_tag_estimation( + res, mode=mode, session=session)) + list(map(session.add, tags)) + session.commit() + estimated_tags = model.checksum.get_estimated_tags(mode=mode) + return self.render('i2v/image_tag.html', estimated_tags=estimated_tags, model=model, mode=mode) + + def after_model_change(self, form, model, is_created): + if is_created: + session = models.db.session + model.update_checksum(session) + # old path + full_path = model.full_path + thumbgen_filename = model.thumbgen_filename + # + model.path = '{}{}'.format( + model.checksum.value, os.path.splitext(full_path)[1]) + shutil.move(full_path, model.full_path) + new_thumbgen_filename = model.thumbgen_filename + if thumbgen_filename != new_thumbgen_filename: + try: + shutil.move( + os.path.join(models.file_path, thumbgen_filename), + os.path.join(models.file_path, new_thumbgen_filename)) + logger.debug('Thumbnail moved.'.format( + src_thumb=thumbgen_filename, dst_thumb=new_thumbgen_filename)) + except FileNotFoundError as e: + logger.debug('Thumbnail not found.'.format( + thumb=thumbgen_filename)) + session.commit() + + @expose('/new/', methods=('GET', 'POST')) + def create_view(self): + res = super().create_view() + if get_redirect_target() == url_for('image.plausible_tag_view') and \ + request.method == 'POST': + form = self.create_form() + if self.validate_form(form): + model = self.create_model(form) + return redirect(url_for('image.plausible_tag_view', id=model.id)) + return res + + def create_model(self, form): + """ + Create model from form. + + :param form: + Form instance + """ + try: + model = self.model() + form.populate_obj(model) + checksum = models.sha256_checksum(model.full_path) + checksum_m = models.get_or_create(self.session, models.Checksum, value=checksum)[0] + instance = self.session.query(self.model).filter_by(checksum=checksum_m).first() + if instance: + model = instance + self.session.add(model) + self._on_model_change(form, model, True) + self.session.commit() + except Exception as ex: + if not self.handle_view_exception(ex): + flash(gettext('Failed to create record. %(error)s', error=str(ex)), 'error') + logger.exception('Failed to create record.') + self.session.rollback() + return False + else: + self.after_model_change(form, model, True) + return model + + +class TagEstimationModeFilter(filters.BaseSQLAFilter): + + def apply(self, query, value, alias=None): + res = query.filter(self.column.mode == value) + return res + + def operation(self): + return 'equal' + + +class TagEstimationView(ModelView): + + column_formatters = { + 'checksum': lambda v, c, m, n: getattr(m, n).value[:7], + 'created_at': + lambda v, c, m, n: + Markup('

{}

'.format( + m.created_at, + arrow.Arrow.fromdatetime(m.created_at, tzinfo='local').humanize(arrow.now()) + )), + 'mode': lambda v, c, m, n: getattr(m, n).code, + 'tag': lambda v, c, m, n: getattr(m, n).fullname, + 'value': lambda v, c, m, n: '{0:0.2f}'.format(getattr(m, n) * 100), + } + column_filters = ( + 'checksum', 'tag', 'value', + TagEstimationModeFilter(models.TagEstimation, 'mode', options=models.TagEstimation.MODES) + ) + column_list = ('created_at', 'checksum', 'mode', 'tag', 'value') + form_excluded_columns = ('created_at', ) + named_filter_urls = True diff --git a/i2v_parsing_script.json b/i2v_parsing_script.json new file mode 100644 index 00000000..c15a6e8a --- /dev/null +++ b/i2v_parsing_script.json @@ -0,0 +1,425 @@ +[ + 32, + "illustration2vec", + 2, + [ + "http://127.0.0.1:5011/image/new/?url=%2Fimage%2Fplausible-tag", + 1, + 0, + [ + 55, + 1, + [ + [], + "some hash bytes" + ] + ], + "path", + {}, + [ + [ + 29, + 1, + [ + "match parser", + [ + 27, + 6, + [ + [ + 26, + 1, + [ + [ + 62, + 2, + [ + 0, + "a", + { + "id": "hydrus-link" + }, + null, + null, + false, + [ + 51, + 1, + [ + 3, + "", + null, + null, + "example string" + ] + ] + ] + ] + ] + ], + 0, + "href", + [ + 51, + 1, + [ + 3, + "", + null, + null, + "example string" + ] + ], + [ + 55, + 1, + [ + [], + "parsed information" + ] + ] + ] + ], + [ + [ + 30, + 3, + [ + "tags creator", + 0, + [ + 27, + 6, + [ + [ + 26, + 1, + [ + [ + 62, + 2, + [ + 0, + "td", + { + "class": "tag-creator" + }, + null, + null, + false, + [ + 51, + 1, + [ + 3, + "", + null, + null, + "example string" + ] + ] + ] + ] + ] + ], + 1, + "", + [ + 51, + 1, + [ + 3, + "", + null, + null, + "example string" + ] + ], + [ + 55, + 1, + [ + [], + "parsed information" + ] + ] + ] + ], + "creator" + ] + ], + [ + 30, + 3, + [ + "tags series", + 0, + [ + 27, + 6, + [ + [ + 26, + 1, + [ + [ + 62, + 2, + [ + 0, + "td", + { + "class": "tag-copyright" + }, + null, + null, + false, + [ + 51, + 1, + [ + 3, + "", + null, + null, + "example string" + ] + ] + ] + ] + ] + ], + 1, + "", + [ + 51, + 1, + [ + 3, + "", + null, + null, + "example string" + ] + ], + [ + 55, + 1, + [ + [], + "parsed information" + ] + ] + ] + ], + "series" + ] + ], + [ + 30, + 3, + [ + "tags character", + 0, + [ + 27, + 6, + [ + [ + 26, + 1, + [ + [ + 62, + 2, + [ + 0, + "td", + { + "class": "tag-character" + }, + null, + null, + false, + [ + 51, + 1, + [ + 3, + "", + null, + null, + "example string" + ] + ] + ] + ] + ] + ], + 1, + "", + [ + 51, + 1, + [ + 3, + "", + null, + null, + "example string" + ] + ], + [ + 55, + 1, + [ + [], + "parsed information" + ] + ] + ] + ], + "character" + ] + ], + [ + 30, + 3, + [ + "tags unnamespaced", + 0, + [ + 27, + 6, + [ + [ + 26, + 1, + [ + [ + 62, + 2, + [ + 0, + "td", + { + "class": "tag-general" + }, + null, + null, + false, + [ + 51, + 1, + [ + 3, + "", + null, + null, + "example string" + ] + ] + ] + ] + ] + ], + 1, + "", + [ + 51, + 1, + [ + 3, + "", + null, + null, + "example string" + ] + ], + [ + 55, + 1, + [ + [], + "parsed information" + ] + ] + ] + ], + "" + ] + ], + [ + 30, + 3, + [ + "tags rating", + 0, + [ + 27, + 6, + [ + [ + 26, + 1, + [ + [ + 62, + 2, + [ + 0, + "td", + { + "class": "tag-rating", + "data-index": "1" + }, + null, + null, + false, + [ + 51, + 1, + [ + 3, + "", + null, + null, + "example string" + ] + ] + ] + ] + ] + ], + 1, + "", + [ + 51, + 1, + [ + 3, + "", + null, + null, + "example string" + ] + ], + [ + 55, + 1, + [ + [], + "parsed information" + ] + ] + ] + ], + "rating" + ] + ] + ] + ] + ] + ] + ] +] diff --git a/manifest.in b/manifest.in new file mode 100644 index 00000000..26f8dc35 --- /dev/null +++ b/manifest.in @@ -0,0 +1,2 @@ +recursive-include i2v/templates * +recursive-include i2v/static * diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..9c558e35 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +. diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..2b62136e --- /dev/null +++ b/setup.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +from setuptools import setup, find_packages + +with open('README.md') as f: + long_description = f.read() + + +setup( + name="illustration2vec", + version="2.0.1", + packages=find_packages(), + install_requires=[ + 'appdirs==1.4.3', + 'arrow>=0.12.1', + 'chainer>=4.1.0', + 'click>=6.7', + 'Flask-Admin==1.5.1', + 'Flask-Migrate==2.1.1', + 'flask-shell-ipython==0.3.0', + 'Flask-SQLAlchemy==2.3.2', + 'Flask-WTF==0.14.2', + 'Flask==1.0.2', + 'numpy>=1.14.3', + 'Pillow>=5.1.0', + 'scikit-image>=0.14.0', + 'SQLAlchemy-Utils>=0.33.3', + 'structlog==18.1.0', + ], + author="rezoo", + author_email="rezoolab@gmail.com", + maintainer="Rachmadani Haryono", + maintainer_email="foreturiga@gmail.com", + description="A simple deep learning library for estimating a set of tags " + "and extracting semantic feature vectors from given illustrations.", + license="MIT License", + keywords="machine learning tag image illustration", + url="https://github.com/rezoo/illustration2vec/", # project home page, if any + project_urls={ + "Bug Tracker": "https://github.com/rezoo/illustration2vec/issues", + }, + long_description=long_description, + long_description_content_type='text/markdown', + zip_safe=True, + entry_points={'console_scripts': ['i2v = i2v.__main__:cli', ],}, + extras_require={ + 'dev': [ + 'pdbpp>=0.9.2', + 'ipython>=6.4.0', + ], + }, +)