├── .gitignore ├── img ├── fumi.png ├── kei.png ├── other.png ├── yui.png ├── aikawa.png ├── finish.png ├── okasan.png ├── yukari.png └── yuzuko.png ├── templates ├── finish.html ├── base.html ├── main.js ├── index.html ├── tag.html └── lib │ └── jquery.hotkeys.js ├── infinity ├── template.html ├── search.html ├── index.cgi ├── index2.cgi ├── search.cgi └── json2sqlite.py ├── README.md ├── LICENSE └── yuyushiki.py /.gitignore: -------------------------------------------------------------------------------- 1 | .pyc 2 | .pyo 3 | *.un~ 4 | .DS_Store 5 | data 6 | yuyushiki.db 7 | -------------------------------------------------------------------------------- /img/fumi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/non117/yuyushiki/HEAD/img/fumi.png -------------------------------------------------------------------------------- /img/kei.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/non117/yuyushiki/HEAD/img/kei.png -------------------------------------------------------------------------------- /img/other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/non117/yuyushiki/HEAD/img/other.png -------------------------------------------------------------------------------- /img/yui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/non117/yuyushiki/HEAD/img/yui.png -------------------------------------------------------------------------------- /img/aikawa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/non117/yuyushiki/HEAD/img/aikawa.png -------------------------------------------------------------------------------- /img/finish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/non117/yuyushiki/HEAD/img/finish.png -------------------------------------------------------------------------------- /img/okasan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/non117/yuyushiki/HEAD/img/okasan.png -------------------------------------------------------------------------------- /img/yukari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/non117/yuyushiki/HEAD/img/yukari.png -------------------------------------------------------------------------------- /img/yuzuko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/non117/yuyushiki/HEAD/img/yuzuko.png -------------------------------------------------------------------------------- /templates/finish.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block extrahead %} 3 | 9 | {% endblock %} 10 | {% block content %} 11 |
12 | 13 |
14 |
15 |

進捗

16 | 17 |
18 | {% endblock %} 19 | 20 | -------------------------------------------------------------------------------- /infinity/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ゆゆ式 5 | 18 | 19 | 20 | {% for frame in frames %} 21 | 22 | {% endfor %} 23 | 24 | 25 | -------------------------------------------------------------------------------- /infinity/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ゆゆ式 5 | 18 | 19 | 20 | {{ count }}件 21 | {% for result in results %} 22 | 23 | {% endfor %} 24 | 25 | 26 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yuyushiki Annotation Tool 6 | 20 | 21 | {% block extrahead %}{% endblock %} 22 | 23 | 24 |

Yuyushiki Annotation Tool

25 |
26 | {% block content %}{% endblock %} 27 | 28 | 29 | -------------------------------------------------------------------------------- /infinity/index.cgi: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import sqlite3 4 | from functools import reduce 5 | from jinja2 import Environment, FileSystemLoader 6 | 7 | conn = sqlite3.connect('yuyushiki.db', check_same_thread=False) 8 | 9 | def get4frame(): 10 | sql = 'select path from comics order by random() limit 4' 11 | cursor = conn.cursor() 12 | l = cursor.execute(sql).fetchall() 13 | cursor.close() 14 | return reduce(lambda x, y: x+y,l) 15 | 16 | def index(): 17 | env = Environment(loader=FileSystemLoader('./', encoding='utf8')) 18 | tpl = env.get_template('template.html') 19 | frames = get4frame() 20 | html = tpl.render({'frames':frames}) 21 | print('Content-Type: text/html; charset=utf-8\n') 22 | print(html.encode('utf-8')) 23 | 24 | if __name__ == '__main__': 25 | index() 26 | 27 | -------------------------------------------------------------------------------- /infinity/index2.cgi: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import sqlite3 4 | from jinja2 import Environment, FileSystemLoader 5 | 6 | conn = sqlite3.connect('yuyushiki.db', check_same_thread=False) 7 | 8 | def get4frame(): 9 | sql = 'select path from comics where frameno={0} order by random() limit 1' 10 | cursor = conn.cursor() 11 | res = [] 12 | for i in range(4): 13 | l = cursor.execute(sql.format(i+1)).fetchall() 14 | res.append(l[0][0]) 15 | cursor.close() 16 | return res 17 | 18 | def index(): 19 | env = Environment(loader=FileSystemLoader('./', encoding='utf8')) 20 | tpl = env.get_template('template.html') 21 | frames = get4frame() 22 | html = tpl.render({'frames':frames}) 23 | print('Content-Type: text/html; charset=utf-8\n') 24 | print(html.encode('utf-8')) 25 | 26 | if __name__ == '__main__': 27 | index() 28 | 29 | -------------------------------------------------------------------------------- /infinity/search.cgi: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import cgi 4 | import sqlite3 5 | from functools import reduce 6 | from jinja2 import Environment, FileSystemLoader 7 | 8 | conn = sqlite3.connect('yuyushiki.db', check_same_thread=False) 9 | 10 | def search(word): 11 | sql = 'select path from comics where script like "%{0}%"'.format(word) 12 | cursor = conn.cursor() 13 | l = cursor.execute(sql).fetchall() 14 | cursor.close() 15 | if l == []: 16 | return l 17 | return reduce(lambda x, y: x+y,l) 18 | 19 | def index(): 20 | env = Environment(loader=FileSystemLoader('./', encoding='utf8')) 21 | tpl = env.get_template('search.html') 22 | word = cgi.FieldStorage().getvalue('word') 23 | results = search(word) 24 | params = {'results':results,'count':len(results)} 25 | html = tpl.render(params) 26 | print('Content-Type: text/html; charset=utf-8\n') 27 | print(html.encode('utf-8')) 28 | 29 | if __name__ == '__main__': 30 | index() 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | yuyushiki 2 | ========= 3 | 4 | Yuyushiki Annotation Tool 5 | 6 | 依存関係 7 | ----------- 8 | * Python 3.4 or higher ( 実は2.xでも動く ) 9 | * flask ( cgiではアレかもわからん ) 10 | * MongoDB + pymongo 11 | 12 | 仕様 13 | ------------ 14 | 1. data/hoge ディレクトリの画像を見に行きます 15 | 2. 画像とフォームをブラウザに表示します 16 | 3. タグ付け等アノテーションをしてもらいます 17 | 4. submitされたら、データを記録し、次の画像を表示します 18 | 19 | データ形式 20 | ---------------- 21 | monogoDB使う。yuyushiki.comics 22 | 23 | * 画像ファイルのパス [主キー] : path 24 | * 台詞文字列 : script 25 | * [人物タグ] : characters 26 | * 要修正 bool : reedit, デフォルトオフ 27 | * いらない画像 bool : useless, デフォルトオフ 28 | 29 | * characters : ["野々原ゆずこ", "櫟井唯", "日向縁", "相川千穂", "岡野佳", "長谷川ふみ", "松本頼子", "その他", "無し"] 30 | 31 | 32 | UI 33 | ---------------- 34 | * 台詞入力, データの分別と、キャラのタグ付け作業はurlごと分離する 35 | * ↑ 独立して作業が可能に 36 | * 台詞は人間ががんばって入力する 37 | * 人物タグは補完ができるようにすると良いかも 38 | * ↑ あるいはシステム側にタグとして持たせておく 39 | * 3人全員タグ 40 | * キーボードショートカットを実装しておく 41 | * 単一ページでがんばるぞい 42 | * 進捗バーを表示する 43 | * 戻るボタンが実装された 44 | 45 | その他 46 | ---------------- 47 | * アノテーションしたデータが飛ぶとつらいので逐次DBにinsert 48 | * データができたらランダム4コマシステムに応用します 49 | 50 | 無限4コマシステム 51 | ==================== 52 | 53 | 永遠にゆゆ式を楽しめる楽園を実現します 54 | https://github.com/non117/yuyushiki/issues/5 55 | 56 | 仕様 57 | --------- 58 | * 59 | 60 | 61 | -------------------------------------------------------------------------------- /templates/main.js: -------------------------------------------------------------------------------- 1 | function setVal(obj){ 2 | var id = $(obj).attr("id"); 3 | if($(obj).hasClass('selected')){ 4 | $(obj).removeClass('selected'); 5 | $("#"+id+"_input").val(''); 6 | }else{ 7 | $(obj).addClass('selected'); 8 | $("#"+id+"_input").val(id); 9 | } 10 | }; 11 | 12 | $(function(){ 13 | $('input').focus(function(){ 14 | $(this).parent().addClass('focus'); 15 | }).blur(function(){ 16 | $(this).parent().removeClass('focus'); 17 | }); 18 | $(document).bind('keydown', 'ctrl+return', function(){ 19 | $('#submit').click(); 20 | }); 21 | $(document).bind('keydown', 'A', function(){ 22 | $('#fumi').click(); 23 | }); 24 | $(document).bind('keydown', 'S', function(){ 25 | $('#other').click(); 26 | }); 27 | $(document).bind('keydown', 'D', function(){ 28 | $('#aikawa').click(); 29 | }); 30 | $(document).bind('keydown', 'F', function(){ 31 | $('#yui').click(); 32 | }); 33 | $(document).bind('keydown', 'H', function(){ 34 | $('#kei').click(); 35 | }); 36 | $(document).bind('keydown', 'J', function(){ 37 | $('#yukari').click(); 38 | }); 39 | $(document).bind('keydown', 'K', function(){ 40 | $('#yuzuko').click(); 41 | }); 42 | $(document).bind('keydown', 'L', function(){ 43 | $('#okasan').click(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block extrahead %} 3 | 4 | 16 | {% endblock %} 17 | {% block content %} 18 |
19 |

{{ path.name }}

20 | this is a image 21 |
22 |
23 |

24 | 台詞 : 25 |

26 |

27 | 28 | 29 | 30 | 31 | 32 |

33 |
34 |
35 |
36 |
37 |

進捗

38 | 39 |
40 | {% endblock %} 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, non 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /infinity/json2sqlite.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | from sqlalchemy import create_engine, Column, String, Integer 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.orm import sessionmaker 6 | 7 | engine = create_engine('sqlite:///yuyushiki.db', echo=True) 8 | Base = declarative_base() 9 | 10 | class Frame(Base): 11 | __tablename__ = 'comics' 12 | 13 | path = Column(String, primary_key=True) 14 | script = Column(String) 15 | characters = Column(String) 16 | frameno = Column(Integer) 17 | 18 | def __init__(self, script, path, characters, frameno): 19 | self.path = path 20 | self.script = script 21 | self.characters = self.list2str(characters) 22 | self.frameno = frameno 23 | 24 | def __repr__(self): 25 | return self.path 26 | 27 | @staticmethod 28 | def list2str(l): 29 | return ','.join(l) 30 | 31 | @staticmethod 32 | def str2list(s): 33 | return s.split(',') 34 | 35 | def get_frameno(filename): 36 | no = int(filename[-5]) 37 | return no % 4 + 1 38 | 39 | def main(): 40 | Base.metadata.create_all(engine) 41 | session = sessionmaker(bind=engine)() 42 | filepath = '../yuyushiki.json' 43 | db = json.load(open(filepath)) 44 | for data in db: 45 | if data['useless'] or data['reedit']: 46 | continue 47 | no = get_frameno(data['path']) 48 | frame = Frame(data['script'], data['path'], data['characters'], no) 49 | session.add(frame) 50 | session.commit() 51 | 52 | main() 53 | 54 | -------------------------------------------------------------------------------- /templates/tag.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block extrahead %} 3 | 31 | 32 | 33 | {% endblock %} 34 | {% block content %} 35 |
36 |

{{ path.name }}

37 | this is a image 38 |
39 |
40 |
    41 |
  • a:長谷川ふみ
  • 42 |
  • s:その他
  • 43 |
  • d:相川千穂
  • 44 |
  • f:唯
  • 45 |
  • h:岡野佳
  • 46 |
  • j:縁
  • 47 |
  • k:ゆずこ
  • 48 |
  • l:松本頼子
  • 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |

59 | 60 | 61 | 62 |

63 |
64 |
65 |
66 |
67 |

進捗

68 | 69 |
70 | {% endblock %} 71 | 72 | -------------------------------------------------------------------------------- /templates/lib/jquery.hotkeys.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Hotkeys Plugin 3 | * Copyright 2010, John Resig 4 | * Dual licensed under the MIT or GPL Version 2 licenses. 5 | * 6 | * Based upon the plugin by Tzury Bar Yochay: 7 | * http://github.com/tzuryby/hotkeys 8 | * 9 | * Original idea by: 10 | * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ 11 | */ 12 | 13 | /* 14 | * One small change is: now keys are passed by object { keys: '...' } 15 | * Might be useful, when you want to pass some other data to your handler 16 | */ 17 | 18 | (function(jQuery){ 19 | 20 | jQuery.hotkeys = { 21 | version: "0.8", 22 | 23 | specialKeys: { 24 | 8: "backspace", 9: "tab", 10: "return", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", 25 | 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", 26 | 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 59: ";", 61: "=", 27 | 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", 28 | 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", 29 | 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", 30 | 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 173: "-", 186: ";", 187: "=", 31 | 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'" 32 | }, 33 | 34 | shiftNums: { 35 | "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", 36 | "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", 37 | ".": ">", "/": "?", "\\": "|" 38 | }, 39 | 40 | // excludes: button, checkbox, file, hidden, image, password, radio, reset, search, submit, url 41 | textAcceptingInputTypes: [ 42 | "text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime", 43 | "datetime-local", "search", "color", "tel"], 44 | 45 | options: { 46 | filterTextInputs: true 47 | } 48 | }; 49 | 50 | function keyHandler( handleObj ) { 51 | if ( typeof handleObj.data === "string" ) { 52 | handleObj.data = { keys: handleObj.data }; 53 | } 54 | 55 | // Only care when a possible input has been specified 56 | if ( !handleObj.data || !handleObj.data.keys || typeof handleObj.data.keys !== "string" ) { 57 | return; 58 | } 59 | 60 | var origHandler = handleObj.handler, 61 | keys = handleObj.data.keys.toLowerCase().split(" "); 62 | 63 | handleObj.handler = function( event ) { 64 | // Don't fire in text-accepting inputs that we didn't directly bind to 65 | if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || 66 | ( jQuery.hotkeys.options.filterTextInputs && 67 | jQuery.inArray(event.target.type, jQuery.hotkeys.textAcceptingInputTypes) > -1 ) ) ) { 68 | return; 69 | } 70 | 71 | var special = jQuery.hotkeys.specialKeys[ event.keyCode ], 72 | character = String.fromCharCode( event.which ).toLowerCase(), 73 | modif = "", possible = {}; 74 | 75 | jQuery.each([ "alt", "ctrl", "meta", "shift" ], function(index, specialKey) { 76 | if (event[specialKey + 'Key'] && special !== specialKey) { 77 | modif += specialKey + '+'; 78 | } 79 | }); 80 | 81 | 82 | modif = modif.replace('alt+ctrl+meta+shift', 'hyper'); 83 | 84 | if ( special ) { 85 | possible[ modif + special ] = true; 86 | } 87 | 88 | if ( character ) { 89 | possible[ modif + character ] = true; 90 | possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; 91 | 92 | // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" 93 | if ( modif === "shift+" ) { 94 | possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; 95 | } 96 | } 97 | 98 | for ( var i = 0, l = keys.length; i < l; i++ ) { 99 | if ( possible[ keys[i] ] ) { 100 | return origHandler.apply( this, arguments ); 101 | } 102 | } 103 | }; 104 | } 105 | 106 | jQuery.each([ "keydown", "keyup", "keypress" ], function() { 107 | jQuery.event.special[ this ] = { add: keyHandler }; 108 | }); 109 | 110 | })( this.jQuery ); 111 | -------------------------------------------------------------------------------- /yuyushiki.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import atexit 3 | from functools import reduce 4 | from pathlib import Path 5 | 6 | from flask import Flask, render_template, send_from_directory, request, send_file 7 | from pymongo import Connection 8 | 9 | app = Flask(__name__) 10 | app.config.update( 11 | DEBUG=True, 12 | ) 13 | 14 | 15 | con = Connection('localhost', 27017) 16 | collection = con.yuyushiki.comics 17 | atexit.register(con.close) 18 | 19 | root = Path('data') 20 | pages = reduce(lambda a, b:a+b, [[p for p in path.iterdir()] for path in root.iterdir()]) 21 | pages.sort() 22 | 23 | def get_latest(): 24 | latest = list(collection.find({}).sort('_id',-1).limit(1)) 25 | if latest == []: 26 | return pages[0] 27 | else: 28 | i = pages.index(Path(latest[0]['path'])) 29 | return pages[i+1] 30 | 31 | def get_tag_latest(): 32 | latest = list(collection.find({'characters':[]}).limit(1)) 33 | if latest == [] and collection.count() > 0: 34 | return get_latest() 35 | if latest == []: 36 | return pages[0] 37 | else: 38 | i = pages.index(Path(latest[0]['path'])) 39 | return pages[i] 40 | 41 | def find_one(path): 42 | return collection.find_one({'path':path}) 43 | 44 | def upsert(path, script='', characters=[], reedit=False, useless=False): 45 | d = {'path':path, 'script':script, 'characters':characters, 46 | 'reedit':reedit, 'useless':useless} 47 | collection.update({'path':path}, d, upsert=True) 48 | 49 | @app.route('/', methods=['GET', 'POST']) 50 | def index(): 51 | prev = False 52 | if request.method == 'POST': 53 | data = request.form 54 | if data.get('action') == 'prev': #前の画像に戻る 55 | path = data.get('path') 56 | prev = True 57 | else: 58 | script = data.get('script', '') 59 | path = data.get('path') 60 | characters = [] 61 | reedit = bool(data.get('reedit', False)) 62 | useless = bool(data.get('useless', False)) 63 | upsert(path, script, characters, reedit, useless) 64 | 65 | i = pages.index(Path(path)) 66 | if prev: 67 | if i - 1 < 0: 68 | p = pages[i] 69 | else: 70 | p = pages[i-1] 71 | else: 72 | try: 73 | p = pages[i+1] 74 | except IndexError: 75 | return render_template('finish.html') 76 | elif request.method == 'GET': 77 | p = get_latest() 78 | 79 | data = find_one(p.as_posix()) 80 | progress = round(collection.count() * 100 / len(pages), 2) 81 | return render_template('index.html', path=p, progress=progress, data=data) 82 | 83 | def skip_next(data, orig): 84 | if data is None: 85 | return orig 86 | if data['useless']: 87 | data['characters'] = ['none'] 88 | collection.update({'path':data['path']}, data, upsert=True) 89 | i = pages.index(Path(data['path'])) 90 | next_data = find_one(pages[i+1].as_posix()) 91 | if next_data is None: 92 | return find_one(pages[i].as_posix()) 93 | return skip_next(next_data, orig) 94 | else: 95 | return data 96 | 97 | def skip_prev(data, orig): 98 | if data['useless']: 99 | i = pages.index(Path(data['path'])) 100 | prev_data = find_one(pages[i-1].as_posix()) 101 | if prev_data is None: 102 | i = pages.index(Path(orig['path'])) 103 | return find_one(pages[i+1].as_posix()) 104 | return skip_prev(prev_data, orig) 105 | else: 106 | return data 107 | 108 | # TODO: complete画面, pagesの最後 109 | @app.route('/tag/', methods=['GET', 'POST']) 110 | def tag(): 111 | prev = False 112 | if request.method == 'POST': 113 | data = request.form 114 | if data.get('action') == 'prev': 115 | path = data.get('path') 116 | prev = True 117 | else: 118 | path = data.get('path') 119 | characters = list(filter(bool, data.getlist('characters'))) 120 | if characters == []: 121 | characters.append('none') 122 | data = find_one(path) 123 | if data: 124 | data['characters'] = characters 125 | collection.update({'path':path}, data, upsert=True) 126 | else: 127 | upsert(path, characters=characters) 128 | 129 | i = pages.index(Path(path)) 130 | if prev: 131 | if i - 1 < 0: 132 | p = pages[i] 133 | else: 134 | p = pages[i-1] 135 | else: 136 | try: 137 | p = pages[i+1] 138 | except IndexError: 139 | return render_template('finish.html') 140 | elif request.method == 'GET': 141 | p = get_tag_latest() 142 | 143 | data = find_one(p.as_posix()) 144 | #if prev: 145 | # data = skip_prev(data, data) 146 | #else: 147 | # data = skip_next(data, data) 148 | p = Path(data['path']) if data else p 149 | characters = {c:True for c in data['characters']} if data else [] 150 | progress = round(collection.find({'characters':{'$ne':[]}}).count() * 100 / len(pages), 2) 151 | return render_template('tag.html', path=p, progress=progress, characters=characters) 152 | 153 | #@app.route('/test') 154 | def test(): 155 | return render_template('finish.html') 156 | 157 | @app.route('/data/') 158 | def data(filename): 159 | return send_from_directory(root.as_posix(), filename) 160 | 161 | @app.route('/img/') 162 | def img(filename): 163 | return send_from_directory('img', filename) 164 | 165 | @app.route('/js/main.js') 166 | def mainjs(): 167 | return send_file('templates/main.js') 168 | 169 | @app.route('/js/lib/') 170 | def js(filename): 171 | return send_from_directory('templates/lib', filename) 172 | 173 | if __name__ == '__main__': 174 | app.run(host='0.0.0.0') 175 | 176 | --------------------------------------------------------------------------------