From 696ade92fe6baef5254c4c9a56141fd758daa35e Mon Sep 17 00:00:00 2001 From: lotress Date: Thu, 28 Feb 2019 01:07:25 +0800 Subject: [PATCH 01/10] 4.5 preview --- package-lock.json | 8 +- package.json | 2 +- python/worker.py | 1 + src/css/style.css | 751 +++++++++++++++++++++++++------------------ src/js/app.js | 65 ++-- src/js/common.js | 31 +- src/js/progress.js | 28 +- src/js/steps.js | 621 +++++++++++++++++++++++++++++++++++ static/lock.html | 4 +- templates/index.html | 70 +--- webpack.config.js | 3 +- 11 files changed, 1169 insertions(+), 415 deletions(-) create mode 100644 src/js/steps.js diff --git a/package-lock.json b/package-lock.json index 9873cf4..9c2c1c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "moephoto", - "version": "4.3.1", + "version": "4.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -5087,9 +5087,9 @@ } }, "terser-webpack-plugin": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz", - "integrity": "sha512-1DMkTk286BzmfylAvLXwpJrI7dWa5BnFmscV/2dCr8+c56egFcbaeFAl7+sujAjdmpLam21XRdhA4oifLyiWWg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz", + "integrity": "sha512-GOK7q85oAb/5kE12fMuLdn2btOS9OBZn4VsecpHDywoUC/jLhSAKOiYo0ezx7ss2EXPMzyEWFoE0s1WLE+4+oA==", "dev": true, "requires": { "cacache": "^11.0.2", diff --git a/package.json b/package.json index a26a45b..a6f7404 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moephoto", - "version": "4.4.2", + "version": "4.5.0", "description": "MoePhoto Image Toolbox萌图工具箱 一个基于深度学习的AI图像修复软件(更新中...) 感兴趣的加QQ群320785467讨论 ### 基本功能 * 图像降噪 * 超分辨率 * 去雾 * 涂抹及风格化", "private": true, "directories": { diff --git a/python/worker.py b/python/worker.py index 1f37ac3..d94dec1 100644 --- a/python/worker.py +++ b/python/worker.py @@ -58,6 +58,7 @@ def g(*args, **kwargs): code = 400 finally: clean() + onProgress(context.root, res) return (json.dumps(res, ensure_ascii=False), code) return g diff --git a/src/css/style.css b/src/css/style.css index 8192f2f..26696ac 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,7 +1,7 @@ body{ margin:0; font-family: 'Roboto', sans-serif; - background: #fff; + background: #fff; } body a{ transition: 0.5s all; @@ -26,23 +26,23 @@ ul,label{ body a:hover,body a:focus{ text-decoration:none; outline: none; -} +} /*-- banner --*/ .agileits-banner { background:url(../images/1.png)no-repeat center 0px; -webkit-background-size:cover; background-size:cover; -moz-background-size:cover; - position: relative; -} + position: relative; +} .agileits-banner1 { background:url(../images/1s.png)no-repeat center 0px; -webkit-background-size:cover; background-size:cover; -moz-background-size:cover; - position: relative; -} -/*-- logo --*/ + position: relative; +} +/*-- logo --*/ .w3llogo h1 { font-size: 3em; font-weight: 100; @@ -50,12 +50,12 @@ body a:hover,body a:focus{ } .w3llogo h1 a { display: inline-block; - color: #fff; + color: #fff; } .w3llogo h1 a span { font-size: .25em; letter-spacing: 11px; - display: block; + display: block; font-weight: 300; } /*-- //logo --*/ @@ -96,7 +96,7 @@ ul.nav.navbar { } .nav > li > a:hover, .nav > li > a:focus { background: none; -} +} .top-nav ul li a:hover, .top-nav ul li a.active { color: #FF5722 !important; -webkit-border-radius: 2px; @@ -137,11 +137,11 @@ ul.nav.navbar { text-decoration: none; background-color: transparent; } -.navbar-right { +.navbar-right { margin-right: 0; } /*-- //top-nav --*/ -/*-- banner-text --*/ +/*-- banner-text --*/ .banner-w3text { text-align: center; padding: 19em 0; @@ -162,7 +162,7 @@ ul.nav.navbar { font-family: 'Yanone Kaffeesatz', sans-serif; text-transform: capitalize; } -/*-- social-icons --*/ +/*-- social-icons --*/ .w3ls-bnr-icons.social-icon { margin-top: 4em; } @@ -190,7 +190,7 @@ ul.nav.navbar { transform: scale(1.5); } .social-icon a.twit:hover { - color: #1da1f2; + color: #1da1f2; } .social-icon a.social-button.fb:hover { color: #3b5998; @@ -216,7 +216,7 @@ ul.nav.navbar { p{ font-size:1em; color:#999; - line-height:1.8em; + line-height:1.8em; } .effect-ruby { position: relative; @@ -230,7 +230,7 @@ p{ min-height: 100%; max-width: 100%; opacity: 0.8; -} +} .effect-ruby .agile-caption { padding: 2em; -webkit-backface-visibility: hidden; @@ -240,19 +240,19 @@ p{ left: 0; width: 100%; height: 100%; -} +} .effect-ruby img { opacity: 0.7; -webkit-transition: opacity 0.35s, -webkit-transform 0.35s; transition: opacity 0.35s, transform 0.35s; -webkit-transform: scale(1.15); transform: scale(1.15); -} +} .effect-ruby:hover img { opacity: 0.5; -webkit-transform: scale(1); transform: scale(1); -} +} .effect-ruby h3 { color: #fff; font-size: 2.2em; @@ -284,13 +284,13 @@ p{ .effect-ruby:hover h3 { -webkit-transform: translate3d(0,0,0); transform: translate3d(0,0,0); -} +} .effect-ruby:hover p { opacity: 1; -webkit-transform: translate3d(0,0,0) scale(1); transform: translate3d(0,0,0) scale(1); } -/*-- //banner-bottom --*/ +/*-- //banner-bottom --*/ /*-- features --*/ h3.agileits-title,h2.agileits-title{ text-align: center; @@ -299,8 +299,8 @@ h3.agileits-title,h2.agileits-title{ margin-bottom: 0.6em; font-weight: 300; letter-spacing: 2px; -} -.features h2.agileits-title { +} +.features h2.agileits-title { margin-bottom: 0.1em; } .features p.pagile-text { @@ -348,16 +348,16 @@ h3.agileits-title,h2.agileits-title{ transform:rotatey(180deg); } /*-- //features --*/ -/*-- stats --*/ +/*-- stats --*/ .stats{ background-size: cover; background:#FF5722; margin-top:6em; } -.stats-agileinfo{ +.stats-agileinfo{ text-align: center; -} - +} + .stats-agileinfo h6 { font-weight: 900; letter-spacing: 5px; @@ -380,7 +380,7 @@ h3.agileits-title,h2.agileits-title{ /*-- services --*/ .services h3.agileits-title { text-align: left; -} +} .services-grid{ text-align:center; } @@ -414,11 +414,11 @@ h3.agileits-title,h2.agileits-title{ opacity: 1; } /*-- //services --*/ -/*-- footer start here --*/ +/*-- footer start here --*/ .footer-agile { padding: 4em 0 3em; - background:#191d22; -} + background:#191d22; +} .footer-top-agileinfo{ text-align:center; } @@ -426,7 +426,7 @@ h3.agileits-title,h2.agileits-title{ border-top: 1px solid #272e35; padding-top: 2.5em; margin-top: 1.5em; -} +} .footer-grid h3 { font-size: 2.8em; color: #ff5722; @@ -446,12 +446,12 @@ h3.agileits-title,h2.agileits-title{ margin-right: 8px; font-size: .8em; } -.footer-grid li a:hover { +.footer-grid li a:hover { color: #17b9ea; -} +} .footer-grid form { position: relative; -} +} .footer-grid input[type="email"] { width: 63%; padding: 0.8em; @@ -465,10 +465,10 @@ h3.agileits-title,h2.agileits-title{ transition: 0.5s all; -webkit-transition: 0.5s all; -moz-transition: 0.5s all; -} +} .footer-grid form:hover input[type="email"] { border-color:#17b9ea; -} +} .footer-grid input[type="submit"] { float: left; color: #fff; @@ -482,14 +482,14 @@ h3.agileits-title,h2.agileits-title{ -webkit-appearance: none; background: none; margin-left: 1em; -} +} .footer-grid form:hover input[type="submit"]{ background-color: #17b9ea; border-color: #17b9ea; -} +} .footer-grid li a:hover{ color: #17b9ea; -} +} .copy-w3lsright p { color: #fff; margin-top: 1.2em; @@ -497,17 +497,17 @@ h3.agileits-title,h2.agileits-title{ font-weight: 300; font-size: .9em; } -.copy-w3lsright p a{ +.copy-w3lsright p a{ color: #17b9ea; text-decoration:none; -webkit-transition:.5s all; -moz-transition:.5s all; transition:.5s all; } -.copy-w3lsright p a:hover{ - color: #fff; +.copy-w3lsright p a:hover{ + color: #fff; } -/*-- //footer end here --*/ +/*-- //footer end here --*/ /*-- slider-up-arrow --*/ #toTop { display: none; @@ -532,8 +532,8 @@ h3.agileits-title,h2.agileits-title{ -moz-opacity: 0; filter: alpha(opacity=0); } -/*-- //slider-up-arrow --*/ -/*-- about-page --*/ +/*-- //slider-up-arrow --*/ +/*-- about-page --*/ .about-w3lsbnr .banner-w3text{ padding: 5em 0; } @@ -549,7 +549,7 @@ h3.agileits-title,h2.agileits-title{ /*-- about --*/ .about-w3right { padding: 0 3em; -} +} .about h4 { font-size: 2em; color: #17b9ea; @@ -584,20 +584,20 @@ h3.agileits-title,h2.agileits-title{ z-index: 1; -webkit-backface-visibility: hidden; -moz-osx-font-smoothing: grayscale; - border: 1px solid #17b9ea; + border: 1px solid #17b9ea; overflow: hidden; font-size: 1em; -webkit-transition: .5s all; - -moz-transition: .5s all; - transition: .5s all; + -moz-transition: .5s all; + transition: .5s all; -webkit-border-radius: 3px; - -moz-border-radius: 3px; + -moz-border-radius: 3px; border-radius: 3px; } .button:focus { outline: none; -} -/* Isi */ +} +/* Isi */ .button-isi::before { content: ''; z-index: -1; @@ -608,7 +608,7 @@ h3.agileits-title,h2.agileits-title{ width: 30px; height: 30px; -webkit-border-radius: 50%; - -moz-border-radius: 50%; + -moz-border-radius: 50%; border-radius: 50%; background: #17b9ea; -webkit-transform-origin: 100% 50%; @@ -618,13 +618,13 @@ h3.agileits-title,h2.agileits-title{ -webkit-transform: scale3d(1, 2, 1); -moz-transform: scale3d(1, 2, 1); -o-transform: scale3d(1, 2, 1); - -ms-transform: scale3d(1, 2, 1); + -ms-transform: scale3d(1, 2, 1); transform: scale3d(1, 2, 1); -webkit-transition: -webkit-transform 0.5s, opacity 0.5s; - -moz-transition: transform 0.5s, opacity 0.5s; + -moz-transition: transform 0.5s, opacity 0.5s; transition: transform 0.5s, opacity 0.5s; -webkit-transition-timing-function: cubic-bezier(0.7,0,0.9,1); - -moz-transition-timing-function: cubic-bezier(0.7,0,0.9,1); + -moz-transition-timing-function: cubic-bezier(0.7,0,0.9,1); transition-timing-function: cubic-bezier(0.7,0,0.9,1); } .button-isi .icon { @@ -651,7 +651,7 @@ h3.agileits-title,h2.agileits-title{ background: url(../images/1s.png)no-repeat center 0px; -webkit-background-size: cover; background-size: cover; - -moz-background-size: cover; + -moz-background-size: cover; text-align:center; } .about-slid h4 { @@ -671,11 +671,11 @@ h3.agileits-title,h2.agileits-title{ color: #eaeaea; letter-spacing: 3px; } -/*-- //about-page --*/ +/*-- //about-page --*/ /*-- about-team --*/ .team-row-agileinfo{ margin: 4em 0; -} +} .social-icon.social-w3lsicon a { margin: 0 0.5em; } @@ -696,23 +696,23 @@ h3.agileits-title,h2.agileits-title{ left: 10%; z-index: 9; -webkit-box-shadow: 0px 2px 3px #ccc; - -moz-box-shadow: 0px 2px 3px #ccc; + -moz-box-shadow: 0px 2px 3px #ccc; box-shadow: 0px 2px 3px #ccc; height: 30%; -webkit-transition: .5s all; - -moz-transition: .5s all; - transition: .5s all; + -moz-transition: .5s all; + transition: .5s all; } -.team h4 { +.team h4 { color: #17b9ea; - font-size: 1.6em; + font-size: 1.6em; } .team .social-w3lsicon { - padding: .8em; + padding: .8em; margin-top: 0.4em; background:#191d22; -webkit-transition: .5s all; - -moz-transition: .5s all; + -moz-transition: .5s all; transition: .5s all; -webkit-transform: scale(1,0); -moz-transform: scale(1,0); @@ -720,7 +720,7 @@ h3.agileits-title,h2.agileits-title{ -ms-transform: scale(1,0); transform: scale(1,0); } -.team .social-w3lsicon a { +.team .social-w3lsicon a { -webkit-transition-delay: .2s; -moz-transition-delay: .2s; -o-transition-delay: .2s; @@ -729,19 +729,19 @@ h3.agileits-title,h2.agileits-title{ } .thumbnail.team-agileits:hover .social-w3lsicon { -webkit-transform: scale(1); - -moz-transform: scale(1); - -o-transform: scale(1); - -ms-transform: scale(1); - transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); } .thumbnail.team-agileits:hover .w3agile-caption { height: 49%; } /*-- //about-team --*/ -/*-- gallery --*/ +/*-- gallery --*/ .w3gallery-grids { padding: 1em; -} +} .w3gallery-grids img.img-responsive { width: 100%; } @@ -758,9 +758,9 @@ h3.agileits-title,h2.agileits-title{ a.w3effct-agile.w3effct-agile-mdl { margin: 2em 0; } -a.w3effct-agile { +a.w3effct-agile { position: relative; - overflow: hidden; + overflow: hidden; display: block; animation: anima 2s; -webkit-animation: anima 2s; @@ -768,7 +768,7 @@ a.w3effct-agile { backface-visibility: hidden; -webkit-backface-visibility: hidden; border: 6px double #17b9ea; -} +} .w3gallery-grids img.img-responsive { -webkit-transform: scale(1.3); -moz-transform: scale(1.3); @@ -776,7 +776,7 @@ a.w3effct-agile { -ms-transform: scale(1.3); transform: scale(1.3); } -.agile-figcap{ +.agile-figcap{ position: absolute; width: 100%; height: 100%; @@ -801,9 +801,9 @@ a.w3effct-agile:hover img.img-responsive { } a.w3effct-agile:hover .agile-figcap{ top: 0%; -} -/*-- //gallery --*/ -/*-- contact --*/ +} +/*-- //gallery --*/ +/*-- contact --*/ .contact input[type="text"],.contact input[type="email"]{ width: 47.7%; color: #999; @@ -839,9 +839,9 @@ a.w3effct-agile:hover .agile-figcap{ font-size: 1em; margin: 1em 0 0 0; -webkit-appearance: none; - background: #17b9ea; + background: #17b9ea; border: 2px solid #17b9ea; - -webkit-transition: 0.5s all; + -webkit-transition: 0.5s all; -moz-transition: 0.5s all; transition: 0.5s all; } @@ -858,33 +858,33 @@ a.w3effct-agile:hover .agile-figcap{ .contact::-moz-placeholder { /* Firefox 19+ */ color:#999 !important; } -.contact:-ms-input-placeholder { +.contact:-ms-input-placeholder { color:#999 !important; } .contact-right h4 { font-size: 1.8em; color: #17b9ea; line-height: 1.6em; - letter-spacing: 2px; + letter-spacing: 2px; } .contact-text { - padding: 3em; + padding: 3em; -webkit-box-shadow: 5px 5px 13px rgba(0, 0, 0, 0.5); -moz-box-shadow: 5px 5px 13px rgba(0, 0, 0, 0.5); box-shadow: 3px 3px 13px rgba(0, 0, 0, 0.5); -} -.contact-text p { +} +.contact-text p { margin-top: 1em; text-align: left; } -.contact-text p a{ - color:#FF5722; - -webkit-transition: 0.5s all; +.contact-text p a{ + color:#FF5722; + -webkit-transition: 0.5s all; -moz-transition: 0.5s all; transition: 0.5s all; } -.contact-text p a:hover{ - color:#999; +.contact-text p a:hover{ + color:#999; } .contact-text p i.fa { margin-right: 5px; @@ -895,7 +895,7 @@ a.w3effct-agile:hover .agile-figcap{ border: none; } /*-- //contact --*/ -/*-- single --*/ +/*-- single --*/ .w3ls_single_left_grid_right h3 { font-size: 2em; color: #17b9ea; @@ -955,18 +955,18 @@ a.w3effct-agile:hover .agile-figcap{ .w3ls_single_left_grid1_right{ float:left; margin-left:2em; -} +} .w3ls_single_left_grid1_right .social-icon a { color: #555; margin: 0 0.8em; -} +} .w3l_admin { padding: 2em; background: #333; margin:3em 0 0; } .w3l_admin p { - color: #f5f5f5; + color: #f5f5f5; position: relative; padding-left: 3.5em; margin-bottom: 1em; @@ -984,7 +984,7 @@ a.w3effct-agile:hover .agile-figcap{ } .w3l_admin a:hover{ color:#fff; -} +} .wthree_recent h4, .agileits_three_comments h3, .w3_leave_comment h3, .agile_cat_grid h4, .agileits-tags h4 { font-size: 2em; color: #FF5722; @@ -1008,7 +1008,7 @@ a.w3effct-agile:hover .agile-figcap{ } .wthree_recent ul li a{ color: #999; - text-decoration: none; + text-decoration: none; -webkit-transition: .5s all; -moz-transition: .5s all; transition: .5s all; @@ -1025,7 +1025,7 @@ a.w3effct-agile:hover .agile-figcap{ } .wthree_recent ul li span i { left: -1em; -} +} .wthree_recent ul li a i { padding-right: 1em; } @@ -1034,9 +1034,9 @@ a.w3effct-agile:hover .agile-figcap{ } .agileits_three_comments { margin: 5em 0; -} +} .agile_cat_grid ul.categories li { - list-style-type: none; + list-style-type: none; font-size: 1em; margin-bottom:1em; } @@ -1103,7 +1103,7 @@ a.w3effct-agile:hover .agile-figcap{ .reply a:hover{ background:#333; } -.agileits_tom_right p.lorem { +.agileits_tom_right p.lorem { margin: 1em 0 0; } .agileits_three_comments h3,.w3_leave_comment h3{ @@ -1128,14 +1128,14 @@ a.w3effct-agile:hover .agile-figcap{ .agileits_three_comment_grid1{ padding-left:5em; } -.w3_leave_comment{ +.w3_leave_comment{ width:60%; } .w3_leave_comment form{ margin:3em 0 0; } .w3_leave_comment input[type="text"], .w3_leave_comment input[type="email"], .w3_leave_comment textarea { - outline: none; + outline: none; border: 1px solid #dedede; padding: .8em 1em; font-size: 1em; @@ -1167,7 +1167,7 @@ a.w3effct-agile:hover .agile-figcap{ background:#333; } /*-- //single --*/ -/*-- pages --*/ +/*-- pages --*/ .well { font-weight: 300; font-size: 14px; @@ -1211,14 +1211,14 @@ li.list-group-item1 { font-weight: 300; } .grid_4{ - background:none; + background:none; } .label { font-weight: 300 !important; border-radius:4px; -} +} .grid_5{ - background:none; + background:none; } .grid_5 h3, .grid_5 h2, .grid_5 h1, .grid_5 h4, .grid_5 h5, h3.w3ls-hdg, h3.bars { margin-bottom: 1em; @@ -1314,7 +1314,7 @@ ol { } h2.typoh2{ margin: 0 0 10px; -} +} .tab-content > .active { display: block; visibility: visible; @@ -1329,7 +1329,7 @@ h2.typoh2{ } .nav-tabs { margin-bottom: 1em; -} +} h1.t-button,h2.t-button,h3.t-button,h4.t-button,h5.t-button { line-height:1.8em; margin-top:0.5em; @@ -1403,7 +1403,7 @@ h2.typoh2{ -ms-transition: 0.5s all; -moz-transition: 0.5s all; cursor: pointer; -} +} .icon-box:hover { background: #5f5f5f; transition:0.5s all; @@ -1422,11 +1422,11 @@ h2.typoh2{ float: left; width: 12.5%; height: 115px; - padding: 10px; + padding: 10px; line-height: 1.4; - text-align: center; + text-align: center; font-size: 12px; - list-style-type: none; + list-style-type: none; } .codes .bs-glyphicons .glyphicon { margin-top: 5px; @@ -1444,7 +1444,7 @@ h2.typoh2{ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #777; -} +} .codes .bs-glyphicons .glyphicon-class { display: block; text-align: center; @@ -1498,7 +1498,7 @@ h3.icon-subheading { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #777; -} +} .icons .bs-glyphicons .glyphicon-class { display: block; text-align: center; @@ -1516,7 +1516,7 @@ h3.icon-subheading { padding: 4px 6px; border: none; text-shadow: none; -} +} h1.t-button,h2.t-button,h3.t-button,h4.t-button,h5.t-button { line-height:1.8em; margin-top:0.5em; @@ -1572,7 +1572,7 @@ h2.typoh2{ } @media (max-width:1080px){ .icon-box { - padding: 8px 9px; + padding: 8px 9px; } } @media (max-width:768px){ @@ -1648,17 +1648,17 @@ h2.typoh2{ .grid_3.grid_4.w3layouts { margin-top: 0; } - .agileits-icons-title { - font-size: 30px; + .agileits-icons-title { + font-size: 30px; } h3.icon-subheading { - font-size: 22px; + font-size: 22px; + } + .icons .bs-glyphicons li { + width: 31%; } - .icons .bs-glyphicons li { - width: 31%; - } } -@media (max-width: 320px){ +@media (max-width: 320px){ .alert,ol.breadcrumb li, .grid_3 p,.well, ul.list-group li, li.list-group-item1,a.list-group-item { font-size: 13px; } @@ -1685,7 +1685,7 @@ h2.typoh2{ .codes .row { margin: 0; } - .agileits-icons-title { + .agileits-icons-title { font-size: 25px; } h3.icon-subheading { @@ -1694,12 +1694,12 @@ h2.typoh2{ } } /*-- //pages --*/ -/*-- responsive-design --*/ +/*-- responsive-design --*/ @media(max-width:1280px){ .about-w3lsbnr .banner-w3text h2 { font-size: 4em; } -.banner-w3text { +.banner-w3text { padding: 15em 0; } .about-w3lsbnr .banner-w3text { @@ -1708,38 +1708,38 @@ h2.typoh2{ } @media(max-width:1080px){ .features p.pagile-text { - width: 85%; + width: 85%; } .welcome, .stats, .services, .about, .about-slid, .team, .contact, .gallery, .codes { padding: 4em 0; } -.stats { +.stats { margin-top: 4.5em; } -h3.agileits-title, h2.agileits-title { - font-size: 3.5em; +h3.agileits-title, h2.agileits-title { + font-size: 3.5em; letter-spacing: 2px; } .footer-agile { - padding: 3em 0; + padding: 3em 0; } .footer-grid h3 { - font-size: 2.6em; + font-size: 2.6em; } -.copy-w3lsright p { - letter-spacing: 1px; +.copy-w3lsright p { + letter-spacing: 1px; } .about-w3lsbnr .banner-w3text h2 { font-size: 3.5em; } .about h4 { - font-size: 1.8em; + font-size: 1.8em; } -.team .w3agile-caption { - height: 37%; +.team .w3agile-caption { + height: 37%; } .team .social-w3lsicon { - padding: .5em; + padding: .5em; } .thumbnail.team-agileits:hover .w3agile-caption { height: 57%; @@ -1747,37 +1747,37 @@ h3.agileits-title, h2.agileits-title { .contact-right.wthree { padding-right: 0; } -.map iframe { - min-height: 300px; +.map iframe { + min-height: 300px; } } @media(max-width:1024px){ .top-nav ul li { - margin: 0 0 0 3.5em; + margin: 0 0 0 3.5em; } .banner-w3text { padding: 14em 0; } .services-grid i.fa { - font-size: 3.5em; + font-size: 3.5em; } .about-slid h4 { - font-size: 3.5em; + font-size: 3.5em; } .team-row-agileinfo { margin: 3em 0; } .w3ls_single_left_grid_right h3 { - font-size: 1.8em; + font-size: 1.8em; letter-spacing: 1px; } .wthree_recent ul, .agileits-tags ul, .agile_cat_grid ul { padding: 1em 0 0; } -.w3ls_single_left_grid_right h5 { +.w3ls_single_left_grid_right h5 { margin-bottom: 1em; - padding-bottom: 1em; - letter-spacing: 4px; + padding-bottom: 1em; + letter-spacing: 4px; } .w3ls_single_left_grid1 { margin: 2em 0; @@ -1797,16 +1797,16 @@ h3.agileits-title, h2.agileits-title { } @media(max-width:991px){ .w3llogo h1 { - font-size: 2.6em; + font-size: 2.6em; } -.w3llogo h1 a span { - letter-spacing: 10px; +.w3llogo h1 a span { + letter-spacing: 10px; } -.top-nav ul li a { - font-size: 1.3em; +.top-nav ul li a { + font-size: 1.3em; } -.banner-w3text #vertical-ticker li, .banner-w3text h2 { - font-size: 4em; +.banner-w3text #vertical-ticker li, .banner-w3text h2 { + font-size: 4em; } .w3ls-bnr-icons.social-icon { margin-top: 2em; @@ -1814,38 +1814,38 @@ h3.agileits-title, h2.agileits-title { .banner-w3text { padding: 12em 0; } -.social-icon a { - margin: 0 1.2em; +.social-icon a { + margin: 0 1.2em; } .welcome-w3lsgrids { padding: 0 .5em; } -.effect-ruby { +.effect-ruby { height: 180px; } -.effect-ruby h3 { - font-size: 2em; - margin-top: 2%; +.effect-ruby h3 { + font-size: 2em; + margin-top: 2%; } .effect-ruby .agile-caption { - padding: 1.5em; + padding: 1.5em; } -.effect-ruby p { +.effect-ruby p { letter-spacing: 1px; -} -.features-right img { +} +.features-right img { width: 50%; margin: 0 auto; } .features p.pagile-text { - width: 90%; - margin: 0 auto 3em; + width: 90%; + margin: 0 auto 3em; } .features-grid.features-grid-mdl { margin: 2em 0; } .features h4 { - font-size: 1.8em; + font-size: 1.8em; } .features-right { margin-bottom: 3em; @@ -1854,14 +1854,14 @@ h3.agileits-title, h2.agileits-title { margin-top: 3.5em; } .numscroller { - font-size: 1.8em; + font-size: 1.8em; padding: 1.2em 0; width: 95px; - height: 95px; + height: 95px; } -.stats-agileinfo h6 { - letter-spacing: 4px; - font-size: 1.3em; +.stats-agileinfo h6 { + letter-spacing: 4px; + font-size: 1.3em; } .welcome, .stats, .services, .about, .about-slid, .team, .contact, .gallery, .codes { padding: 3.5em 0; @@ -1876,8 +1876,8 @@ h3.agileits-title, h2.agileits-title { font-size: 2.3em; letter-spacing: 1px; } -ul.nav.navbar { - margin: 1em 0 0; +ul.nav.navbar { + margin: 1em 0 0; } .about-w3left { padding-right: 0; @@ -1887,13 +1887,13 @@ ul.nav.navbar { letter-spacing: 6px; } .about-slid h5 { - font-size: 1.3em; - letter-spacing: 4px; + font-size: 1.3em; + letter-spacing: 4px; margin-bottom: 0.8em; } -.about-slid p { +.about-slid p { letter-spacing: 1px; -} +} .team-row-agileinfo { width: 75%; margin: 0 auto; @@ -1912,17 +1912,17 @@ ul.nav.navbar { font-size: 3em; } .contact-text { - padding: 2em 3em; + padding: 2em 3em; margin-top: 3em; } -.contact textarea { - min-height: 10em; +.contact textarea { + min-height: 10em; } -.w3l_admin { +.w3l_admin { margin: 2.5em 0; } -.w3ls_single_left_grid1_left h4 { - margin-top: 0; +.w3ls_single_left_grid1_left h4 { + margin-top: 0; } .wthree_recent ul, .agileits-tags ul, .agile_cat_grid ul { padding: 0.8em 0 0; @@ -1934,10 +1934,10 @@ ul.nav.navbar { margin: 2em 0; } .agileits_tom i.fa { - font-size: 3em; + font-size: 3em; } .agileits_three_comment_grid { - padding: 2em 0; + padding: 2em 0; } .agileits_three_comment_grid1 { padding-left: 3em; @@ -1948,11 +1948,11 @@ ul.nav.navbar { .w3_leave_comment input[type="email"], .w3_leave_comment textarea { margin: 0.5em 0; } -.w3_leave_comment input[type="text"], .w3_leave_comment input[type="email"], .w3_leave_comment textarea { - padding: .6em 1em; +.w3_leave_comment input[type="text"], .w3_leave_comment input[type="email"], .w3_leave_comment textarea { + padding: .6em 1em; } .w3_leave_comment textarea { - min-height: 150px; + min-height: 150px; } .about-w3lsbnr .banner-w3text { padding: 3em 0; @@ -1961,17 +1961,17 @@ ul.nav.navbar { margin: 0 0 0 2.5em; } .agileits_three_comment_grid:nth-child(2) { - margin: 2.5em 0 0; + margin: 2.5em 0 0; } .w3_leave_comment { width: 80%; } .contact input[type="text"], .contact input[type="email"] { - width: 49%; - margin-bottom: 1em; + width: 49%; + margin-bottom: 1em; } -.contact input[type="submit"] { - margin: 0.5em 0 0 0; +.contact input[type="submit"] { + margin: 0.5em 0 0 0; } } @media(max-width:800px){ @@ -1984,20 +1984,20 @@ ul.nav.navbar { ul.nav.navbar { margin: 0.8em 0 0; } -.top-nav { - padding: 1.3em 0 0.8em; +.top-nav { + padding: 1.3em 0 0.8em; } .banner-w3text #vertical-ticker li{ font-size: 3.5em; } .banner-w3text #vertical-ticker { - height: 66px !important; + height: 66px !important; } .banner-w3text { padding: 10em 0; } .effect-ruby h3 { - font-size: 1.8em; + font-size: 1.8em; } .about-slid h4 { font-size: 3em; @@ -2010,21 +2010,21 @@ a.w3effct-agile.w3effct-agile-mdl { margin: 1em 0; } .agile-figcap h4 { - font-size: 1.8em; - margin: 0.5em 0 0; + font-size: 1.8em; + margin: 0.5em 0 0; } -.agileits_tom_right { +.agileits_tom_right { margin-left: 2em; } } @media(max-width:768px){ .banner-w3text #vertical-ticker { height: 59px !important; -} -.effect-ruby p { - padding: .3em 0.8em; } -.features p.pagile-text { +.effect-ruby p { + padding: .3em 0.8em; +} +.features p.pagile-text { margin: 0 auto 2em; } .stats { @@ -2034,7 +2034,7 @@ a.w3effct-agile.w3effct-agile-mdl { font-size: 2.8em; letter-spacing: 5px; } -.contact-text { +.contact-text { margin-top: 2.5em; } .map iframe { @@ -2042,16 +2042,16 @@ a.w3effct-agile.w3effct-agile-mdl { } } @media(max-width:767px){ - -.w3llogo h1 a { + +.w3llogo h1 a { margin-left: 1em; -} +} button.navbar-toggle { margin: 0.5em 2em 0 0; z-index: 9999; border-color: #fff; } -.navbar-toggle .icon-bar { +.navbar-toggle .icon-bar { background: #fff; } div#bs-example-navbar-collapse-1 { @@ -2069,12 +2069,12 @@ ul.nav.navbar { } .top-nav ul li { margin: 1.5em 0; - display: block; + display: block; } -.top-nav ul li a { +.top-nav ul li a { display: block; } -.top-nav ul li a:hover, .top-nav ul li a.active { +.top-nav ul li a:hover, .top-nav ul li a.active { -webkit-transform: scale(1); -moz-transform: scale(1); -o-transform: scale(1); @@ -2088,7 +2088,7 @@ ul.nav.navbar { left: inherit; width: 100%; position: inherit; -} +} .effect-ruby p { padding: .3em 0.5em; } @@ -2099,20 +2099,20 @@ ul.nav.navbar { margin-bottom: 3em; } h3.agileits-title, h2.agileits-title { - font-size: 3.3em; + font-size: 3.3em; } .w3l_admin { margin: 2em 0; } -.w3l_admin p { - padding-left: 2em; +.w3l_admin p { + padding-left: 2em; } .w3l_admin p i.fa.fa-quote-left { margin-left: -2em; margin-right: 1em; } -.agileits_tom_right { - width: 82%; +.agileits_tom_right { + width: 82%; } .about-w3right { padding: 0 12em; @@ -2125,11 +2125,11 @@ h3.agileits-title, h2.agileits-title { .about-w3left p { margin-top: 0.8em; } -.button { - margin-top: 1em; +.button { + margin-top: 1em; } .about-slid h4 { - font-size: 2.5em; + font-size: 2.5em; } .welcome, .stats, .services, .about, .about-slid, .team, .contact, .gallery, .codes { padding: 3em 0; @@ -2138,13 +2138,13 @@ h3.agileits-title, h2.agileits-title { height: 51%; } .team .w3agile-caption { - height: 34%; + height: 34%; } .w3ls_single_left_grid_right h3 { - font-size: 1.6em; + font-size: 1.6em; } -} -@media(max-width:667px){ +} +@media(max-width:667px){ .w3llogo h1 a span { letter-spacing: 8px; font-size: .3em; @@ -2159,8 +2159,8 @@ h3.agileits-title, h2.agileits-title { .effect-ruby .agile-caption { padding: 1.5em 0.5em; } -.effect-ruby p { - margin-top: 0.8em; +.effect-ruby p { + margin-top: 0.8em; } .features p.pagile-text { margin: 0 auto 1.5em; @@ -2179,28 +2179,28 @@ h3.agileits-title, h2.agileits-title { margin: 2em 0 0; } .services-grid h5 { - font-size: 1.4em; + font-size: 1.4em; } .footer-agile { padding: 2em 0; } .footer-grid h3 { - font-size: 2em; + font-size: 2em; } -.footer-grid input[type="submit"] { - padding: 0.8em 1.3em; +.footer-grid input[type="submit"] { + padding: 0.8em 1.3em; } -.footer-btm-agileinfo { - padding-top: 1.5em; +.footer-btm-agileinfo { + padding-top: 1.5em; } .about-w3lsbnr .banner-w3text h2 { font-size: 2.6em; } .about-w3right { - padding: 0 10em; + padding: 0 10em; } .team-row-agileinfo { - width: 82%; + width: 82%; } .w3gallery-grids { padding: 0 0.2em; @@ -2208,7 +2208,7 @@ h3.agileits-title, h2.agileits-title { a.w3effct-agile.w3effct-agile-mdl { margin: 0.4em 0; } -.agile-figcap p { +.agile-figcap p { letter-spacing: 0px; line-height: 1.4em; margin-top: .3em; @@ -2217,7 +2217,7 @@ a.w3effct-agile.w3effct-agile-mdl { min-height: 8em; } .contact-text { - padding: 2em 2.5em; + padding: 2em 2.5em; } .agileits_three_comment_grid:nth-child(2) { margin: 2em 0 0; @@ -2251,8 +2251,8 @@ a.w3effct-agile.w3effct-agile-mdl { .stats-grid:nth-child(2) { margin-bottom: 2em; } -.stats-agileinfo h6 { - margin: 0.8em 0 0; +.stats-agileinfo h6 { + margin: 0.8em 0 0; } .team-row-agileinfo { width: 84%; @@ -2265,7 +2265,7 @@ a.w3effct-agile.w3effct-agile-mdl { padding-top: 1em; margin-top: 1em; } -.footer-grid li { +.footer-grid li { font-size: 0.9em; } .footer-grid h3 { @@ -2284,19 +2284,19 @@ a.w3effct-agile.w3effct-agile-mdl { .top-nav ul li a { font-size: 1.2em; } -.top-nav .open > .dropdown-menu li a { - font-size: 1em; +.top-nav .open > .dropdown-menu li a { + font-size: 1em; } -a.w3effct-agile { +a.w3effct-agile { border: 3px double #17b9ea; } .map iframe { min-height: 200px; -} +} } @media(max-width:600px){ .agile-figcap p { - letter-spacing: 0.5px; + letter-spacing: 0.5px; font-size: .9em; } .agileits_tom_right { @@ -2306,32 +2306,32 @@ a.w3effct-agile { .agileits_tom_right p.lorem { margin: 0.2em 0 0; } -.Hardy p { +.Hardy p { margin: .2em 0 0; font-size: .9em; } .w3_leave_comment textarea { min-height: 120px; -} +} } @media(max-width:480px){ .w3llogo h1 { font-size: 2.2em; } button.navbar-toggle { - margin: 0.4em 1.5em 0 0; + margin: 0.4em 1.5em 0 0; } .w3llogo h1 a { margin-left: 0.7em; } .top-nav ul li { - margin: 1.2em 0; + margin: 1.2em 0; } .banner-w3text #vertical-ticker li { font-size: 3em; } .social-icon a { - font-size: 0.9em; + font-size: 0.9em; } .banner-w3text #vertical-ticker { height: 46px !important; @@ -2350,7 +2350,7 @@ button.navbar-toggle { padding: 1.5em 2.5em; } .features i.fa { - font-size: 1.4em; + font-size: 1.4em; } .features-grid-right { padding: 0; @@ -2364,7 +2364,7 @@ button.navbar-toggle { .services-grid i.fa { font-size: 2.5em; } -.footer-grid { +.footer-grid { float: none; width: 100%; } @@ -2386,12 +2386,12 @@ button.navbar-toggle { .team-grids { padding: 0 .5em; } -.team h4 { +.team h4 { font-size: 1.4em; letter-spacing: 1px; } .wthree_recent h4, .agileits_three_comments h3, .w3_leave_comment h3, .agile_cat_grid h4, .agileits-tags h4 { - font-size: 1.8em; + font-size: 1.8em; } .team .w3agile-caption { height: 36%; @@ -2411,24 +2411,24 @@ button.navbar-toggle { a.w3effct-agile.w3effct-agile-mdl{ margin: 1em 0; } -.agile-figcap h4 { +.agile-figcap h4 { margin: 2em 0 0; } -.agile-figcap p { +.agile-figcap p { margin-top: 0.8em; } .contact-form.wthree,.contact-right.wthree { padding: 0; } -.contact input[type="submit"] { - padding: 0.6em 3em; +.contact input[type="submit"] { + padding: 0.6em 3em; } .contact-text { padding: 1.8em 2em; margin-top: 2em; } .contact-right h4 { - font-size: 1.6em; + font-size: 1.6em; } .contact-text p i.fa { margin-right: 10px; @@ -2441,9 +2441,9 @@ a.w3effct-agile.w3effct-agile-mdl{ } .w3ls_single_left_grid1 { margin: 1.5em 0; -} +} .w3ls_single_left_grid1 p { - margin: 1em 0; + margin: 1em 0; } .w3ls_single_left_grid_right h5 { font-size: 0.9em; @@ -2457,14 +2457,14 @@ a.w3effct-agile.w3effct-agile-mdl{ .agileits_tom i.fa { font-size: 2em; } -.agileits_tom { +.agileits_tom { padding: 0.8em 1.2em; } .reply a { - padding: 8px 30px; + padding: 8px 30px; } -.reply { - margin-left: 6em; +.reply { + margin-left: 6em; } .w3_leave_comment { width: 90%; @@ -2472,8 +2472,8 @@ a.w3effct-agile.w3effct-agile-mdl{ .about-w3lsbnr .banner-w3text h2 { font-size: 2.5em; } -.button { - font-size: 0.9em; +.button { + font-size: 0.9em; } } @media(max-width:414px){ @@ -2505,7 +2505,7 @@ a.w3effct-agile.w3effct-agile-mdl{ height: 45%; } p { - font-size: 0.9em; + font-size: 0.9em; } .banner-w3text #vertical-ticker li { font-size: 2.5em; @@ -2520,7 +2520,7 @@ p { margin: 0.8em 0; } .features-right img { - width: 70%; + width: 70%; } .services-w3ls-left,.services-w3ls-row.agileits-w3layouts { padding: 0; @@ -2528,36 +2528,36 @@ p { h3.agileits-title, h2.agileits-title { font-size: 2.8em; } -.services-grid.srvs-wthree-mdl, .services-grid { +.services-grid.srvs-wthree-mdl, .services-grid { padding: 0; } .about-w3left { padding: 0; } -.contact input[type="text"], .contact input[type="email"],.contact textarea{ - font-size: 0.9em; -} -.contact input[type="submit"] { - font-size: 0.9em; +.contact input[type="text"], .contact input[type="email"],.contact textarea{ + font-size: 0.9em; } -.w3ls_single_left_grid_right ul li { +.contact input[type="submit"] { font-size: 0.9em; } -.w3_leave_comment input[type="text"], .w3_leave_comment input[type="email"], .w3_leave_comment textarea { - font-size: 0.9em; +.w3ls_single_left_grid_right ul li { + font-size: 0.9em; } -.w3_leave_comment input[type="submit"] { - font-size: 0.9em; -} -.wthree_recent ul li span { - font-size: 12px; +.w3_leave_comment input[type="text"], .w3_leave_comment input[type="email"], .w3_leave_comment textarea { + font-size: 0.9em; } -.wthree_recent ul li ,.agile_cat_grid_tags ul li,.agile_cat_grid ul.categories li,.reply a { +.w3_leave_comment input[type="submit"] { font-size: 0.9em; -} +} +.wthree_recent ul li span { + font-size: 12px; +} +.wthree_recent ul li ,.agile_cat_grid_tags ul li,.agile_cat_grid ul.categories li,.reply a { + font-size: 0.9em; +} .agileits_three_comment_grid { padding: 1.5em 0; -} +} .w3ls_single_left_grid_right h3 { font-size: 1.3em; } @@ -2566,14 +2566,14 @@ h3.agileits-title, h2.agileits-title { .agile-figcap h4 { margin: 1.2em 0 0; } -.w3gallery-grids { - width: 90%; +.w3gallery-grids { + width: 90%; } .footer-grid.w3social { margin: 1.5em 0; } .footer-grid h3 { - font-size: 1.6em; + font-size: 1.6em; } .features-grid-left { text-align: left; @@ -2585,16 +2585,16 @@ h3.agileits-title, h2.agileits-title { .features h2.agileits-title { margin-bottom: 0.3em; } -div#bs-example-navbar-collapse-1 { - margin: 0.6em 0 0; +div#bs-example-navbar-collapse-1 { + margin: 0.6em 0 0; } } @media(max-width:375px){ .welcome-w3lsgrids { - padding: 0; + padding: 0; } .numscroller { - font-size: 1.5em; + font-size: 1.5em; width: 75px; height: 75px; } @@ -2603,11 +2603,11 @@ div#bs-example-navbar-collapse-1 { font-size: 1.2em; margin: 0.6em 0 0; } -h3.agileits-title, h2.agileits-title { - margin-bottom: 0.5em; +h3.agileits-title, h2.agileits-title { + margin-bottom: 0.5em; } .contact-text { - padding: 1.5em 2em; + padding: 1.5em 2em; } .about-w3right { padding: 0 1em; @@ -2622,7 +2622,7 @@ h3.agileits-title, h2.agileits-title { } .about-slid h5 { font-size: 1.1em; - letter-spacing: 2px; + letter-spacing: 2px; } .welcome, .stats, .services, .about, .about-slid, .team, .contact, .gallery, .codes { padding: 2em 0; @@ -2640,13 +2640,13 @@ h3.agileits-title, h2.agileits-title { } @media(max-width:320px){ .w3llogo h1 { - font-size: 2em; + font-size: 2em; } .w3llogo h1 a { margin-left: 0.5em; } .w3llogo h1 a span { - letter-spacing: 7px; + letter-spacing: 7px; } button.navbar-toggle { margin: 0.2em 1em 0 0; @@ -2672,9 +2672,9 @@ button.navbar-toggle { margin-top: 1em; } .effect-ruby h3 { - font-size: 1.4em; + font-size: 1.4em; } -.features-grid-left { +.features-grid-left { padding: 0; } h3.agileits-title, h2.agileits-title { @@ -2713,9 +2713,9 @@ h3.agileits-title, h2.agileits-title { margin: 1.5em 0; } .w3l_admin { - padding: 1em; + padding: 1em; } -.w3l_admin p { +.w3l_admin p { margin-bottom: 0.5em; } .wthree_recent h4, .agileits_three_comments h3, .w3_leave_comment h3, .agile_cat_grid h4, .agileits-tags h4 { @@ -2736,4 +2736,141 @@ h3.agileits-title, h2.agileits-title { #progress .progress-bar { width: 100%; height: 0.5em +} +[draggable] { + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + user-select: none; + /* Required to make elements draggable in old WebKit */ + -khtml-user-drag: element; + -webkit-user-drag: element; +} +.step { + height: 6em; + width: 100%; + float: left; + border: 2px solid #666666; + background-color: #ccc; + margin-right: 5px; + -webkit-border-radius: 10px; + -ms-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + -webkit-box-shadow: inset 0 0 3px #000; + -ms-box-shadow: inset 0 0 3px #000; + box-shadow: inset 0 0 3px #000; + cursor: pointer; +} +.step[draggable]{ + border-color: #336633; + background-color: #6c6; +} +.step:hover { + border-color: #336633; + background-color: #6c6; +} +.step.selected{ + -webkit-box-shadow: inset 0 0 5px 5px white; + -ms-box-shadow: inset 0 0 5px 5px white; + box-shadow: inset 0 0 5px 5px white; +} +.step.add{ + border: 1px dotted #666666; + background-color: white; + text-align: center +} +.step.add p{ + font-size: 200% +} +.step.add:hover{ + border: 1px double #ccc; + background-color: #cc6; + color: black +} +.step header { + text-shadow: #666 0 1px; + padding: 0.5em 1em 0.5em 1em; +} +.step .op { + padding-left: 1em; +} +.step .visible-lg { + padding-left: 1em; + font-size: 90%; +} +.delete { + float: right; + color: red +} +.delete:hover { + color: #660000 +} +#options header { + font-size: 200%; + font-weight: bold +} +#options .op { + padding-left: 2em; +} +#options .opValue { + padding-left: .5em; + padding-right: 1em; +} +#options .argName { + padding-left: .5em; + padding-right: 0; +} +.step .configs { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100% +} +.step .configs:hover { + overflow:visible +} +.step .opValue { + padding-left: .3em; + padding-right: .3em; + text-overflow: inherit; + max-width: 90%; +} +.description { + margin: 0.5em; +} +.description li{ + margin-left: 1em; + font-size: 90%; + color: #666 +} +input[type="file"] { + display: inherit +} +.input-number { + width: 5em; + position: relative; + text-align: center; + min-height: 12px; + padding-left: 10px +} +.input-text { + width: 40em; + position: relative; + min-height: 12px; + padding-left: 5px +} +#steps .drag-indicator { + width: 100%; + margin: -45px 0 -45px 0; + border: none; + height: 47px +} +#steps .over { + border: 2px dotted red; + border-radius: 10px; + margin: 0 +} +#steps .dragging { + opacity: 0.4 } \ No newline at end of file diff --git a/src/js/app.js b/src/js/app.js index 972cb45..f551cf1 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -1,8 +1,8 @@ -import { getResource, getSession, newMessager } from './common.js' +import { getResource, getSession, newMessager, texts } from './common.js' import $ from 'jquery' const reconnectPeriod = 5 const setup = opt => { - var imgInp = $("#imgInp") + var options = $('#options') if (opt.inputImg && opt.inputImg.length) { const readURL = function () { if (this.files && this.files[0]) { @@ -11,7 +11,7 @@ const setup = opt => { reader.readAsDataURL(this.files[0]) } } - imgInp.change(readURL) + options.on('change', '.imgInp', readURL) } if (opt.dropZone && opt.dropZone.length) { var dropZone = opt.dropZone[0] @@ -23,11 +23,10 @@ const setup = opt => { dropZone.addEventListener('drop', e => { e.stopPropagation() e.preventDefault() - imgInp[0].files = e.dataTransfer.files + options.find('.imgInp')[0].files = e.dataTransfer.files }, false) } - var downloader = $('#downloader'), loading = $('#FG'), runButton = $('#RunButton'), - imgUpload = document.querySelector("#imgUpload"), intervalId, inTaskFlag = 0 + var downloader = $('#downloader'), loading = $('#FG'), runButton = $('#RunButton'), intervalId loading.hide() downloader.hide() @@ -38,7 +37,13 @@ const setup = opt => { runButton.attr('disabled', false) } else { clearInterval(intervalId) - runButton.attr('disabled', true) + let result = event.data.result + if (result === 'Fail') + onError(0, 400, event.data.exception) + else if (result) + onSuccess(result) + else + runButton.attr('disabled', true) } }).on('error', event => { console.error(event) @@ -60,11 +65,27 @@ const setup = opt => { const openMessager = _ => messager.open({ path: opt.path }) openMessager() + const onSuccess = result => { + console.log(result) + clearInterval(intervalId) + loading.hide() + downloader.show() + runButton.attr('disabled', false) + opt.success && opt.success(result.result) + } + + const onError = (xhr, status, error) => { + console.error(xhr, status, error) + clearInterval(intervalId) + loading.hide() + opt.error ? opt.error(texts.errorMsg) : alert(texts.errorMsg) + } + if (opt.session) { - let noFileMsg = '缺少输入文件', errorMsg = '出错啦' runButton.bind('click', function () { - var fdata = new FormData(imgUpload) - if (!fdata.get('file').size) return opt.setMessage ? opt.setMessage(noFileMsg) : alert(noFileMsg) + var fdata = new FormData() + opt.beforeSend && opt.beforeSend(fdata) + if (!fdata.get('file').size) return opt.setMessage ? opt.setMessage(texts.noFileMsg) : alert(texts.noFileMsg) $.ajax({ url: `${opt.path}?session=${opt.session}`, type: "POST", @@ -78,22 +99,9 @@ const setup = opt => { loading.show() runButton.attr('disabled', true) intervalId = setInterval(openMessager, 200) - opt.beforeSend && opt.beforeSend(fdata) - }, - success: result => { - console.log(result) - clearInterval(intervalId) - loading.hide() - downloader.show() - runButton.attr('disabled', false) - opt.success && opt.success(result.result) }, - error: (xhr, status, error) => { - console.error(xhr, status, error) - clearInterval(intervalId) - loading.hide() - opt.error ? opt.error(errorMsg) : alert(errorMsg) - } + success: onSuccess, + error: onError }) }) } else { @@ -102,6 +110,9 @@ const setup = opt => { } return messager } -const app = { getSession, getResource, setup } -window.app = app +const app = { getSession, getResource, setup, texts } +if (window.app) + Object.assign(window.app, app) +else + window.app = app export default app \ No newline at end of file diff --git a/src/js/common.js b/src/js/common.js index bc7cd87..23bbe58 100644 --- a/src/js/common.js +++ b/src/js/common.js @@ -43,4 +43,33 @@ $(document).ready($ => { }) }) const getResource = path => [path, '?', (new Date()).getTime()].join('') -export { getResource, getSession, newMessager } \ No newline at end of file +const texts = { + step: '步骤', + add: '点击添加...', + delete: '删除', + labelSplitter: ':', + pixel: '像素', + fps: '帧每秒', + noFileMsg: '缺少输入文件', + errorMsg: '出错啦', + idle: '空闲中', + finish: '完成啦', + running: '正在处理您的任务', + processing: '处理中', + stopping: '等待保存已处理部分', + onBusy: gone => '忙碌中' + (gone == null ? '' : `,已经过${gone}秒`), + timeFormatter: time => `,预计还需要${time.toFixed(2)}秒` +} +const appendText = key => text => text + texts[key] +const [setLanguage, registryLanguageListener] = (_ => { + const listeners = [] + return [language => { + for (let key of language) + key in texts && (texts[key] = language[key]) + listeners.forEach(language) + }, + listener => { + listener in listeners || listeners.push(listener) + }] +})() +export { getResource, getSession, newMessager, appendText, texts, setLanguage, registryLanguageListener } \ No newline at end of file diff --git a/src/js/progress.js b/src/js/progress.js index 87735ca..ff0fc6f 100644 --- a/src/js/progress.js +++ b/src/js/progress.js @@ -1,15 +1,14 @@ import $ from 'jquery' -import { getResource, getSession } from './common.js' +import { getResource, getSession, texts } from './common.js' import app from './app.js' const bindProgress = $ele => { var intervalId = 0, remain = 0, bar = $ele.find('.progress-bar'), msgBox = $ele.find('.message'), timeBox = $ele.find('.time'), progress - const timeFormatter = time => `,预计还需要${time.toFixed(2)}秒` const elapse = _ => { bar[0].value += 1 remain -= 1 if (remain < 1) remain = 1 - timeBox.text(timeFormatter(remain)) + timeBox.text(texts.timeFormatter(remain)) } const show = _ => { intervalId && clearInterval(intervalId) @@ -39,7 +38,7 @@ const bindProgress = $ele => { intervalId || show() bar[0].max = eta + +bar[0].value remain = eta - timeBox.text(timeFormatter(remain)) + timeBox.text(texts.timeFormatter(remain)) return progress } return progress = { show, hide, setMessage, setStatus } @@ -48,7 +47,7 @@ const bindMessager = ($ele, messager) => { const progress = bindProgress($ele) const onMessage = event => { if (!event.data) { - progress.hide().setMessage('空闲中') + progress.hide().setMessage(texts.idle) progress.status || messager.abort() } else { let data = event.data @@ -73,6 +72,7 @@ const bindMessager = ($ele, messager) => { } return progress } +let _setup = app.setup const setup = opt => { var stopButton = $('#StopButton').hide(), runButton = $('#RunButton'), total = 0 const setPreview = opt.outputImg ? (_ => { @@ -86,8 +86,7 @@ const setup = opt => { } })() : _ => _ if (!opt.session) opt.session = getSession() - const onErrorMsg = gone => '忙碌中' + (gone == null ? '' : `,已经过${gone}秒`) - if (!opt.onProgress) opt.onProgress = onErrorMsg + if (!opt.onProgress) opt.onProgress = texts.onBusy const onMessage = e => { if (!e.data) return let data = e.data @@ -99,17 +98,17 @@ const setup = opt => { data.total ? total = data.total : 0 data.gone ? progress.setMessage(opt.onProgress(data.gone, total, data)) : 0 } - const messager = app.setup(opt) + const messager = _setup(opt) messager.on('message', onMessage).on('open', onMessage) const progress = bindMessager(opt.progress, messager) - opt.onErrorMsg = data => progress.setMessage(onErrorMsg(data.gone, total, data)) + opt.onErrorMsg = data => progress.setMessage(texts.onBusy(data.gone, total, data)) opt.setStatus = progress.setStatus opt.setMessage = progress.setMessage let beforeSend = opt.beforeSend opt.beforeSend = data => { runButton.hide() stopButton.attr('disabled', false).show() - progress.begin('正在处理您的任务') + progress.begin(texts.running) beforeSend && beforeSend(data) } let success = opt.success @@ -128,16 +127,19 @@ const setup = opt => { url: `/stop?session=${opt.session}`, beforeSend: _ => { stopButton.attr('disabled', true) - progress.hide().setMessage('等待保存已处理部分') + progress.hide().setMessage(texts.stopping) }, error: (xhr, status, error) => { console.error(xhr, status, error) - progress.final('出错啦') + progress.final(texts.errorMsg) } }) }) return progress } const exportApp = { getSession, getResource, setup } -window.app = exportApp +if (window.app) + Object.assign(window.app, exportApp) +else + window.app = exportApp export default exportApp \ No newline at end of file diff --git a/src/js/steps.js b/src/js/steps.js new file mode 100644 index 0000000..c656efb --- /dev/null +++ b/src/js/steps.js @@ -0,0 +1,621 @@ +import $ from 'jquery' +import { texts, appendText, registryLanguageListener } from './common.js' + +const setAll = (arr, key) => values => arr.map((o, i) => o[key] = values[i]) +const copyTruly = obj => { + let res = {} + for (let key in obj) obj[key] && (res[key] = obj[key]) + return res +} +var SRScaleValues = [ + { value: 2, text: '2倍', checked: 1 }, + { value: 3, text: '3倍' }, + { value: 4, text: '4倍' } +] +const setScaleDisabled = setAll(SRScaleValues, 'disabled') +const scaleModelMapping = { + a: [0, 0, 0], + p: [0, 0, 0], + lite: [0, 1, 0], + gan: [1, 1, 0] +} +var getResizeView = (by, scale, size) => + by === 'scale' ? scale + '倍' : appendText('pixel')(size) +const panels = { + index: { + text: '请选择一项功能', + draggable: 1 + }, + input: { + text: '输入', + description: '选择一张你需要放大的图片,开始体验吧!运行完毕请点击保存', + position: 0, + submit: (opt, data) => opt.file && opt.file[0] && data.set('file', opt.file[0]) && void 0, + view: opt => ({ file: opt.file && opt.file[0] ? opt.file[0].name : '请选择' }), + args: { + file: { + type: 'file', + name: 'file', + text: '图片', + classes: ['inputfile-6', 'imgInp'] + } + } + }, + inputVideo: { + text: '输入', + description: '选择一段需要放大的视频!运行完毕请点击保存', + position: 0, + submit: (opt, data) => opt.file && opt.file[0] && data.set('file', opt.file[0]) && void 0, + view: opt => ({ file: opt.file && opt.file[0] ? opt.file[0].name : '请选择' }), + args: { + file: { + type: 'file', + name: 'file', + text: '视频', + classes: ['inputfile-6'], + notes: [ + '视频会复制一份上传,存放在程序的upload目录下' + ] + } + } + }, + inputBatch: { + text: '批量输入', + description: '将所有需要放大的图片放置到一个文件夹内,并在下方选择路径', + position: 0, + submit: (opt, data) => opt.file && opt.file.length && data.set('file', opt.file) && void 0, + view: opt => ({ file: opt.file && opt.file.length ? [opt.file[0].name, '等', opt.file.length, '个'].join('') : '请选择' }), + args: { + file: { + type: 'file', + name: 'file', + text: '文件', + classes: ['inputfile-6'], + attributes: ['webkitdirectory', 'directory'] + } + } + }, + SR: { + text: '超分辨率', + description: '以2、3、4倍整数比例放大图像', + draggable: 1, + args: { + scale: { + type: 'radio', + text: '放大倍数', + values: SRScaleValues + }, + model: { + type: 'radio', + text: '超分模型', + change: (_, opt) => { + setScaleDisabled(scaleModelMapping[opt.model]) + if (SRScaleValues[opt.scale - 2].disabled) + for (opt.scale = 2; SRScaleValues[opt.scale - 2].disabled; opt.scale++); + return 1 + }, + values: [ + { value: 'a', text: '动漫', checked: 1 }, + { value: 'p', text: '照片' }, + { + value: 'lite', + text: '快速', + notes: [ + '快速模型仅能放大2倍或4倍,可以在后面添加“缩放”步骤配合使用' + ] + }, + { + value: 'gan', + text: 'GAN', + notes: [ + 'GAN模型仅适用于RGB图像', + 'GAN模型仅能放大4倍,可以在后面添加“缩放”步骤配合使用' + ] + } + ] + } + } + }, + resize: { + text: '缩放', + description: '以插值方法缩放图像,对图像的宽高可以分别设定长度大小或者缩放比例', + draggable: 1, + submit: opt => { + let res = { method: opt.method } + opt.byW === 'scale' ? res.scaleW = opt.scaleW : res.width = opt.width + opt.byH === 'scale' ? res.scaleH = opt.scaleH : res.height = opt.height + return res + }, + view: opt => { + let res = { method: panels.resize.args.method.values.find(item => item.value === opt.method).text } + res.byW = getResizeView(opt.byW, opt.scaleW, opt.width) + res.byH = getResizeView(opt.byH, opt.scaleH, opt.height) + return res + }, + args: { + method: { + type: 'radio', + text: '插值方法', + values: [ + { value: 'nearest', text: '最近邻' }, + { value: 'bilinear', text: '双线性', checked: 1 } + ] + }, + byW: { + type: 'radio', + text: '宽度', + values: [ + { value: 'scale', binds: ['scaleW'] }, + { value: 'pixel', binds: ['width'], checked: 1 } + ] + }, + scaleW: { + type: 'number', + text: '缩放比例', + value: 1, + classes: ['input-number'] + }, + width: { + type: 'number', + text: '大小', + value: 1920, + view: appendText('pixel'), + classes: ['input-number'] + }, + byH: { + type: 'radio', + text: '高度', + values: [ + { value: 'scale', binds: ['scaleH'] }, + { value: 'pixel', binds: ['height'], checked: 1 } + ] + }, + scaleH: { + type: 'number', + text: '缩放比例', + value: 1, + classes: ['input-number'] + }, + height: { + type: 'number', + text: '大小', + value: 1080, + view: appendText('pixel'), + classes: ['input-number'] + } + } + }, + DN: { + text: '降噪', + description: '相对较快的轻量级降噪', + draggable: 1, + args: { + model: { + type: 'radio', + text: '降噪强度', + values: [ + { value: 'lite5', text: '弱', checked: 1 }, + { value: 'lite10', text: '中' }, + { value: 'lite15', text: '强' } + ] + } + } + }, + ednoise: { + op: 'DN', + text: '强力降噪', + description: '这里的降噪非常强,涂抹效果显著,可以试试制作油画风格的照片或者galgame的背景图~', + draggable: 1, + args: { + model: { + type: 'radio', + text: '级别', + values: [ + { value: 'lite5', text: '弱', checked: 1 }, + { value: 'lite10', text: '中' }, + { value: 'lite15', text: '强' } + ] + } + } + }, + dehaze: { + text: '去雾', + description: '去雾AODnet', + draggable: 1 + }, + decode: { + text: '输入解码', + description: '传入ffmpeg的解码参数设定,跟命令行使用是一样的,默认不做额外的解码处理', + position: 1, + submit: copyTruly, + view: copyTruly, + args: { + codec: { + type: 'text', + text: '解码参数', + value: '', + classes: ['input-text'], + notes: [ + '请不要在这里设置颜色格式', + '注意无论下一步的开始于设定为多少,解码都是从视频头开始的' + ] + }, + width: { + type: 'number', + text: '覆盖输入宽度', + value: 0, + view: appendText('pixel'), + classes: ['input-number'] + }, + height: { + type: 'number', + text: '覆盖输入高度', + value: 0, + view: appendText('pixel'), + classes: ['input-number'], + notes: ['我们从输入视频文件里获取画面大小信息,如果这里的解码处理改变了画面大小,请设置上面的两个覆盖值从而告诉后面的处理过程,否则就不要动它们啦'] + } + } + }, + range: { + text: '处理范围', + description: '如果不需要处理整段视频,可以在这里设置', + position: 2, + submit: copyTruly, + view: copyTruly, + args: { + start: { + type: 'number', + text: '开始于', + value: 0, + view: html => `第${html}帧`, + classes: ['input-number'], + notes: [ + '处理范围指的是经过上一步解码处理后的第几帧到第几帧,首帧为第0帧', + '如果想继续上次中止掉的视频处理,上次完成到第几帧这次这里就填多少,其他配置保持一致,最后的多个输出可以用其他工具直接无损拼接', + '注意只有视频画面才有帧的概念,若开始大于0,则除画面外的其余轨道将会被忽略,否则其余轨道将会被直接完整复制到输出中' + ] + }, + stop: { + type: 'number', + text: '结束于', + value: 0, + view: html => `第${html}帧`, + classes: ['input-number'], + notes: [ + '输出不包括上述编号的帧,即输出帧数为结束减开始,若该值小于等于开始则忽略并处理到视频结尾' + ] + } + } + }, + encode: { + text: '输出编码', + description: '传入ffmpeg的编码参数设定,跟命令行使用是一样的,这里通常指定常见的编码器和颜色格式', + position: -1, + submit: copyTruly, + view: copyTruly, + args: { + codec: { + type: 'text', + text: '编码参数', + value: 'libx264 -pix_fmt yuv420p', + classes: ['input-text'], + notes: [ + '传入ffmpeg的编码参数设定,默认设定兼容性最好但是质量和效率都普通', + '一定要以编码器名开头,也一定要指定输出颜色格式(-pix_fmt <格式名>)', + '输出视频以mkv格式封装,因为这啥都能装,有许多免费工具能够无损转换封装格式,我们就不做这事了' + ] + }, + frameRate: { + type: 'number', + text: '覆盖输出帧率', + value: 0, + view: appendText('fps'), + classes: ['input-number'], + notes: [ + '默认的输出帧率就是输入乘上插帧倍数,这样保持时间长度与输入一致,如果您想要看慢动作什么的可以在这里设定', + '注意我们只处理视频画面部分,设置了这个之后声音字幕什么的通常就对不上了' + ] + } + } + }, + slomo: { + text: '插帧', + description: '以可设定的整倍数填充视频画面帧', + draggable: 1, + args: { + sf: { + type: 'number', + text: '倍数', + value: 1, + classes: ['input-number'], + notes: [ + '输出帧数是输入的多少倍,必须是正整数', + '为了方便精确地拼接输出视频,若之前的视频处理开始于设定大于0且这里设置了大于1的倍数,那么开始帧之前的那一帧会被用作参考帧,输出的头几帧将会是它与开始帧之间的插入帧,但这一参考帧本身将不会被输出' + ] + } + } + } +} + +const isType = type => x => type === typeof x +const setSubText = target => (item, key) => + isType('object')(target[key]) && (isType('object')(item) || isType('string')(item)) + ? setPanelTexts(target[key], item) : target[key] = item +const setPanelTexts = (target, t) => { + Array.isArray(t) ? t.forEach(setSubText(target)) + : isType('string')(t) ? (target.text = t) + : (_ => { for (let key in t) setSubText(target)(t[key], key) })() +} +registryLanguageListener(lang => setPanelTexts(panels, lang)) + +const eventTypes = ['change', 'click', 'focus', 'blur', 'hover'] +const optionType = { radio: 1, checkbox: 1 } +const bindChild = panel => child => { + child = panel.args[child] + child.slave = true + return child +} +const addPanel = (panelName, panel) => { + panel.name = panelName + panel.initOpt = {} + let listeners = {} + eventTypes.forEach(type => listeners[type] = []) + for (let argName in panel.args) { + let arg = panel.args[argName] + let selector = `input[name=${argName}]`, bindFlag = 0 + arg.name = argName + if (optionType[arg.type]) + arg.values.forEach(v => { + v.name = argName + v.type = arg.type + v.binds = v.binds && v.binds.map(bindChild(panel)) + bindFlag |= !!v.binds + v.checked ? panel.initOpt[argName] = v.value : 0 + }) + else + panel.initOpt[argName] = arg.value + let changeOpt = arg.type === 'checkbox' ? (ev, opt) => { + opt[argName] || (opt[argName] = {}) + opt[argName][ev.target.value] = ev.target.checked + } : arg.type === 'file' ? (ev, opt) => opt[argName] = ev.target.files + : (ev, opt) => opt[argName] = ev.target.value, + _c = arg.change ? arg.change.bind(arg) : _ => 0 + arg.change = (ev, opt) => { + changeOpt(ev, opt) + context.refreshCurrentStep() + return _c(ev, opt) || bindFlag + } + eventTypes.filter(type => arg[type]).forEach(type => listeners[type].push({ + selector, + f: ev => arg[type](ev, context.getOpt()) && context.refreshPanel() + })) + } + panel.listeners = listeners + panels[panelName] = panel +} + +const reduceApply = (...arr) => arr.reduce((pre, cur) => cur(pre)) +const endPoint = _ => '' +const getAttributes = next => (item, opt) => (item.attributes ? item.attributes.join(' ') : '') + next(item, opt) +const getValue = (item, opt) => opt[item.name] != null ? opt[item.name] : item.value +const getClassAttr = classes => classes && classes.length ? ` class="${classes.join(' ')}"` : '' +const getValueLabel = next => (item, opt) => item.text ? `${item.text}${next(item, opt)}` : '' +const getOptionLabel = next => (item, opt) => next(item, opt) + (item.text ? `${item.text}` : '') +const getDisabled = next => (item, opt) => (item.disabled ? ' disabled' : '') + next(item, opt) +const isChecked = opt => item => (!opt[item.name] && item.checked) || opt[item.name] === item.value || opt[item.name][item.value] +const getChecked = next => (item, opt) => (isChecked(opt)(item) ? ' checked' : '') + next(item, opt) +const getInputTag = (next, getValue = item => item.value) => (item, opt) => + `` +const getInputView = next => (item, opt) => item.view ? item.view(next(item, opt)) : next(item, opt) +const getInputText = getInputView(getInputTag(getDisabled(getAttributes(endPoint)), getValue)) +const getBindHTML = (parent, opt) => item => { + var _d = item.disabled, res + item.disabled = !isChecked(opt)(parent) + res = getArgHTML(item, opt, false) + item.disabled = _d + return res +} +const getInputCheckBinds = next => (item, opt) => next(item, opt) + + (item.binds ? item.binds.map(getBindHTML(item, opt)).join('') : '') +const getInputCheckOption = reduceApply(endPoint, getAttributes, getDisabled, getChecked, + getInputTag, getInputView, getOptionLabel, getInputCheckBinds) +const getNoteHTML = text => `
  • ${text}
  • ` +const getNotes = item => + item.notes && item.notes.length ? + [''].join('') : '' +const getArgHTML = (item, opt, hr = true) => + (hr ? '
    ' : '') + + `${item.text}` + + elementTypeMapping[item.type](item, opt) + + getNotes(item, opt) +const getCheckedItem = (item, opt) => item.values.filter(isChecked(opt)) +const flatArray = arrs => arrs.reduce((res, arr) => res.concat(arr)) +const getInputCheck = (item, opt) => + item.values.map(v => getInputCheckOption(v, opt)) + .concat(flatArray(getCheckedItem(item, opt).map(getNotes))) + .join('') +const elementTypeMapping = { + radio: getInputCheck, + checkbox: getInputCheck, + number: getInputText, + text: getInputText, + file: getInputText +} +const getArgsHTML = (args, opt) => { + var res = [] + for (let key in args) + if (!args[key].slave) + res.push(getArgHTML(args[key], opt)) + return res.join('') +} +const getPanelTitle = (pos, text) => + pos != null ? `
    ${texts.step}${context.getCorrectedPos(pos)} + ${text}
    ` + : `
    ${$('#options header').html()}
    ` +const getPanelView = next => (panel, opt, pos) => getPanelTitle(pos, panel.text) + next(panel, opt) +const getPanelInnerView = next => (panel, opt) => '
    ' + getDiv(x => x)(panel.description) + next(panel.args, opt) +const getIndexItemHTML = name => `${panels[name].text}` +const getDiv = next => (item, id) => + ['', next(item), ''].join('') +const getIndexHTML = _ => + ['
    ', + ...context.getFeatures().map(getIndexItemHTML), + '
    ', getDiv(endPoint)(0, 'description')].join('') +const getIndexStepHTML = pos => + `

    ${texts.add}

    ` +const getIndicatorHTML = (pos, condition) => condition ? `
    ` : '' +const getSelected = selected => selected ? ' selected' : '' +const getDraggable = step => step.panel.draggable ? ' draggable' : '' +const getDelete = step => step.panel.draggable ? `${texts.delete}` : '' +const getStepOpt = step => { + var panel = step.panel, opt = Object.assign({}, step.opt), args = panel.args, res = [] + for (let key in opt) args[key].type === 'number' && (opt[key] = +opt[key]) + if (panel.view) { + opt = panel.view(opt) + } else for (let key in opt) + if (optionType[args[key].type]) { + let f = args[key].type === 'radio' ? item => item.value === opt[key] : item => opt[key][item.value] + opt[key] = args[key].values.filter(f).map(item => item.text).join(',') + } + for (let key in opt) res.push(getValueLabel((_, opt) => texts.labelSplitter + opt[key])(args[key], opt)) + return res.join('') +} +const getStepInnerHTML = (step, pos) => + `
    ${texts.step}${context.getCorrectedPos(pos)}${step.panel.text} + ${getDelete(step)}
    ${getStepOpt(step)}
    ` +const getPureStepHTML = (step, pos, selected = 0) => + `
    ${getStepInnerHTML(step, pos)}
    ` +const getStepNIndicatorHTML = pos => (step, i) => + getIndicatorHTML(i, step.panel.draggable && (i === 0 || !steps[i - 1].panel.draggable)) + + (step == indexStep ? getIndexStepHTML(i) : getPureStepHTML(step, i, i === pos)) + + getIndicatorHTML(i + 1, step.panel.draggable) + +const toggleSelected = ele => { + $('#steps .step').removeClass('selected') + ele.classList.add('selected') +} + +for (let panelName in panels) addPanel(panelName, panels[panelName]) + +const indexStep = { panel: panels.index } +const steps = [] + +const newStep = panel => ({ panel, opt: Object.assign({}, panel.initOpt) }) +const context = (steps => { + var pos = 0, index, addibleFeatures + const getOpt = _ => steps[pos].opt + const getCorrectedPos = p => p > index ? p - 1 : p + const addStep = panelName => { + steps.splice(index, 0, newStep(panels[panelName])) + pos = index + index += 1 + refreshSteps() + } + const selectStep = (p, ele) => { + if (p !== pos) { + pos = p + refreshPanel(0) + toggleSelected(ele) + } + } + const removeStep = pos => { + steps.splice(pos, 1) + refreshSteps() + } + const compareOp = (a, b) => a.position - b.position + const pushNewStep = panel => steps.push(newStep(panel)) + const setFeatures = features => { + var arr = ['index'].concat(features) + addibleFeatures = features.filter(name => panels[name].draggable) + let ps = arr.map(name => panels[name]), + tops = ps.filter(panel => panel.position >= 0).sort(compareOp), + bottoms = ps.filter(panel => panel.position < 0).sort(compareOp) + tops.forEach(pushNewStep) + index = steps.length + steps.push(indexStep) + bottoms.forEach(pushNewStep) + let listeners = panels.index.listeners + addibleFeatures.forEach(name => listeners.click.push({ + selector: `a.btn[name=${name}]`, + f: _ => addStep(name) + })) + refreshSteps() + } + const getFeatures = _ => addibleFeatures + const refreshPanel = (refreshStep = 1) => { + var step = steps[pos], panel = step.panel, + listeners = panel.listeners + $('#options').html(getPanelView(panel === panels.index ? getIndexHTML : getPanelInnerView(getArgsHTML))(panel, step.opt, pos)) + refreshStep && context.refreshCurrentStep() + eventTypes.forEach(type => listeners[type].forEach(o => $(o.selector).on(type, o.f))) + } + const refreshCurrentStep = _ => $(`#steps .step[data-position=${pos}]`).html(getStepInnerHTML(steps[pos], pos)) + const refreshSteps = target => { + target && (pos = target) + index = steps.indexOf(indexStep) + refreshPanel(0) + $('#steps').html(steps.map(getStepNIndicatorHTML(pos)).join('')) + } + return { + getOpt, getFeatures, getCorrectedPos, setFeatures, selectStep, + refreshPanel, removeStep, refreshSteps, refreshCurrentStep + } +})(steps) + +const initListeners = _ => { + $('#steps').on('dragenter', '.drag-indicator', function () { + this.classList.add('over') + }).on('dragleave drop', '.drag-indicator', function () { + this.classList.remove('over') + }).on('dragstart', '.step', function (ev) { + this.classList.add('dragging') + ev.originalEvent.dataTransfer.setData('text', this.getAttribute('data-position')) + }).on('dragend', '.step', function () { + this.classList.remove('dragging') + }).on('drop', '.drag-indicator', function (ev) { + ev.stopPropagation() + let posSrc = +ev.originalEvent.dataTransfer.getData('text'), posTarget = +this.getAttribute('data-position') + if (!(posSrc === posTarget || posSrc === posTarget - 1)) { + posTarget > posSrc && (posTarget -= 1) + let src = steps[posSrc] + steps[posSrc] = steps[posTarget] + steps[posTarget] = src + context.refreshSteps(posTarget) + } + }).on('dragover', '.drag-indicator', ev => ev.preventDefault()) + .on('click', 'a.delete', function (ev) { + ev.stopPropagation() + context.removeStep(+this.parentNode.parentNode.getAttribute('data-position')) + }).on('click', '.step', function () { + context.selectStep(+this.getAttribute('data-position'), this) + }) + $('#options').on('mouseenter', 'a.btn', function () { + let panelName = this.getAttribute('name') + panels[panelName].description && $('#description').html(panels[panelName].description) + }).on('mouseout', 'a.btn', _ => $('#description').html('')) +} + +const submit = data => data.set('steps', JSON.stringify(steps + .filter(step => step !== indexStep) + .map(step => { + let opt = Object.assign({}, step.opt), panel = step.panel + if (panel.submit) { + for (let key in panel.args) { + let arg = panel.args[key] + if (arg.type === 'number' && opt[key] != null) + opt[key] = +opt[key] + } + opt = panel.submit(opt, data) + } + opt && (opt.op = panel.op ? panel.op : panel.name) + return opt + }).filter(opt => !!opt))) + +const app = { addPanel, initListeners, submit, context } +if (window.app) + Object.assign(window.app, app) +else + window.app = app +export default app \ No newline at end of file diff --git a/static/lock.html b/static/lock.html index a8af533..2683551 100644 --- a/static/lock.html +++ b/static/lock.html @@ -15,8 +15,8 @@
    - - + + + diff --git a/webpack.config.js b/webpack.config.js index ca1e421..2e4acc2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,7 +11,8 @@ module.exports = (env, argv) => { entry: { app: './src/js/app.js', progress: './src/js/progress.js', - about: './src/js/about.js' + about: './src/js/about.js', + steps: './src/js/steps.js' }, output: { filename: 'static/js/[name].js', From 795ce646784e4146b8e0dcdb915b0925e172cd74 Mon Sep 17 00:00:00 2001 From: lotress Date: Thu, 28 Feb 2019 23:20:28 +0800 Subject: [PATCH 02/10] fix no root --- python/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/worker.py b/python/worker.py index d94dec1..2afbbe0 100644 --- a/python/worker.py +++ b/python/worker.py @@ -36,7 +36,7 @@ def onProgress(node, kwargs={}): 'eta': context.root.eta, 'gone': context.root.gone, 'total': context.root.total - } + } if context.root else {} res.update(kwargs) if hasattr(node, 'name'): res['stage'] = node.name From c987bcde6ec2ee0f2fc05228cff8cc24819f73d5 Mon Sep 17 00:00:00 2001 From: lotress Date: Thu, 28 Feb 2019 23:21:04 +0800 Subject: [PATCH 03/10] fix serve globally --- python/MoePhoto.py | 2 +- python/server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/MoePhoto.py b/python/MoePhoto.py index bde5b21..b7f22b0 100644 --- a/python/MoePhoto.py +++ b/python/MoePhoto.py @@ -65,7 +65,7 @@ def imageEnhance(size, *args): port = defaultConfig['port'][0] if len(sys.argv) > 1: if '-g' in sys.argv: - host = '' + host = '0.0.0.0' run(host, port) else: from webbrowser import open as startBrowser diff --git a/python/server.py b/python/server.py index e3fd53d..dbb2fe5 100644 --- a/python/server.py +++ b/python/server.py @@ -282,7 +282,7 @@ def writeFile(file): current.writeFile = writeFile def f(host, port): app.debug = False - app.config['SERVER_NAME'] = '{}:{}'.format(host, port) + app.config['SERVER_NAME'] = None server = pywsgi.WSGIServer((host, port), app) print('Current working directory: {}'.format(cwd)) print('Server starts to listen on http://{}:{}/, press Ctrl+C to exit.'.format(host, port)) From c5c33ed77c154d0258fbe4b02159c5b5e7759f6e Mon Sep 17 00:00:00 2001 From: lotress Date: Fri, 1 Mar 2019 02:14:10 +0800 Subject: [PATCH 04/10] use css scroll-behavior: smooth --- src/css/style.css | 3 +++ src/js/SmoothScroll.min.js | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 src/js/SmoothScroll.min.js diff --git a/src/css/style.css b/src/css/style.css index 26696ac..c8cfb9a 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -2732,6 +2732,9 @@ h3.agileits-title, h2.agileits-title { padding: 1.5em 0; } } +html { + scroll-behavior: smooth; +} /*-- //responsive-design --*/ #progress .progress-bar { width: 100%; diff --git a/src/js/SmoothScroll.min.js b/src/js/SmoothScroll.min.js deleted file mode 100644 index 892f843..0000000 --- a/src/js/SmoothScroll.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(){function e(){z.keyboardSupport&&f("keydown",a)}function t(){if(!A&&document.body){A=!0;var t=document.body,o=document.documentElement,n=window.innerHeight,r=t.scrollHeight;if(B=document.compatMode.indexOf("CSS")>=0?o:t,D=t,e(),top!=self)X=!0;else if(r>n&&(t.offsetHeight<=n||o.offsetHeight<=n)){var a=document.createElement("div");a.style.cssText="position:absolute; z-index:-10000; top:0; left:0; right:0; height:"+B.scrollHeight+"px",document.body.appendChild(a);var i;T=function(){i||(i=setTimeout(function(){L||(a.style.height="0",a.style.height=B.scrollHeight+"px",i=null)},500))},setTimeout(T,10),f("resize",T);var l={attributes:!0,childList:!0,characterData:!1};if(M=new V(T),M.observe(t,l),B.offsetHeight<=n){var c=document.createElement("div");c.style.clear="both",t.appendChild(c)}}z.fixedBackground||L||(t.style.backgroundAttachment="scroll",o.style.backgroundAttachment="scroll")}}function o(){M&&M.disconnect(),h(I,r),h("mousedown",i),h("keydown",a),h("resize",T),h("load",t)}function n(e,t,o){if(p(t,o),1!=z.accelerationMax){var n=Date.now(),r=n-R;if(r1&&(a=Math.min(a,z.accelerationMax),t*=a,o*=a)}R=Date.now()}if(q.push({x:t,y:o,lastX:0>t?.99:-.99,lastY:0>o?.99:-.99,start:Date.now()}),!P){var i=e===document.body,l=function(){for(var n=Date.now(),r=0,a=0,c=0;c=z.animationTime,m=s?1:d/z.animationTime;z.pulseAlgorithm&&(m=x(m));var f=u.x*m-u.lastX>>0,h=u.y*m-u.lastY>>0;r+=f,a+=h,u.lastX+=f,u.lastY+=h,s&&(q.splice(c,1),c--)}i?window.scrollBy(r,a):(r&&(e.scrollLeft+=r),a&&(e.scrollTop+=a)),t||o||(q=[]),q.length?_(l,e,1e3/z.frameRate+1):P=!1};_(l,e,0),P=!0}}function r(e){A||t();var o=e.target,r=u(o);if(!r||e.defaultPrevented||e.ctrlKey)return!0;if(w(D,"embed")||w(o,"embed")&&/\.pdf/i.test(o.src)||w(D,"object"))return!0;var a=-e.wheelDeltaX||e.deltaX||0,i=-e.wheelDeltaY||e.deltaY||0;return K&&(e.wheelDeltaX&&b(e.wheelDeltaX,120)&&(a=-120*(e.wheelDeltaX/Math.abs(e.wheelDeltaX))),e.wheelDeltaY&&b(e.wheelDeltaY,120)&&(i=-120*(e.wheelDeltaY/Math.abs(e.wheelDeltaY)))),a||i||(i=-e.wheelDelta||0),1===e.deltaMode&&(a*=40,i*=40),!z.touchpadSupport&&v(i)?!0:(Math.abs(a)>1.2&&(a*=z.stepSize/120),Math.abs(i)>1.2&&(i*=z.stepSize/120),n(r,a,i),e.preventDefault(),void l())}function a(e){var t=e.target,o=e.ctrlKey||e.altKey||e.metaKey||e.shiftKey&&e.keyCode!==N.spacebar;document.contains(D)||(D=document.activeElement);var r=/^(textarea|select|embed|object)$/i,a=/^(button|submit|radio|checkbox|file|color|image)$/i;if(r.test(t.nodeName)||w(t,"input")&&!a.test(t.type)||w(D,"video")||y(e)||t.isContentEditable||e.defaultPrevented||o)return!0;if((w(t,"button")||w(t,"input")&&a.test(t.type))&&e.keyCode===N.spacebar)return!0;var i,c=0,d=0,s=u(D),m=s.clientHeight;switch(s==document.body&&(m=window.innerHeight),e.keyCode){case N.up:d=-z.arrowScroll;break;case N.down:d=z.arrowScroll;break;case N.spacebar:i=e.shiftKey?1:-1,d=-i*m*.9;break;case N.pageup:d=.9*-m;break;case N.pagedown:d=.9*m;break;case N.home:d=-s.scrollTop;break;case N.end:var f=s.scrollHeight-s.scrollTop-m;d=f>0?f+10:0;break;case N.left:c=-z.arrowScroll;break;case N.right:c=z.arrowScroll;break;default:return!0}n(s,c,d),e.preventDefault(),l()}function i(e){D=e.target}function l(){clearTimeout(E),E=setInterval(function(){F={}},1e3)}function c(e,t){for(var o=e.length;o--;)F[j(e[o])]=t;return t}function u(e){var t=[],o=document.body,n=B.scrollHeight;do{var r=F[j(e)];if(r)return c(t,r);if(t.push(e),n===e.scrollHeight){var a=s(B)&&s(o),i=a||m(B);if(X&&d(B)||!X&&i)return c(t,$())}else if(d(e)&&m(e))return c(t,e)}while(e=e.parentElement)}function d(e){return e.clientHeight+100?1:-1,t=t>0?1:-1,(Y.x!==e||Y.y!==t)&&(Y.x=e,Y.y=t,q=[],R=0)}function v(e){return e?(O.length||(O=[e,e,e]),e=Math.abs(e),O.push(e),O.shift(),clearTimeout(H),H=setTimeout(function(){window.localStorage&&(localStorage.SS_deltaBuffer=O.join(","))},1e3),!g(120)&&!g(100)):void 0}function b(e,t){return Math.floor(e/t)==e/t}function g(e){return b(O[0],e)&&b(O[1],e)&&b(O[2],e)}function y(e){var t=e.target,o=!1;if(-1!=document.URL.indexOf("www.youtube.com/watch"))do if(o=t.classList&&t.classList.contains("html5-video-controls"))break;while(t=t.parentNode);return o}function S(e){var t,o,n;return e*=z.pulseScale,1>e?t=e-(1-Math.exp(-e)):(o=Math.exp(-1),e-=1,n=1-Math.exp(-e),t=o+n*(1-o)),t*z.pulseNormalize}function x(e){return e>=1?1:0>=e?0:(1==z.pulseNormalize&&(z.pulseNormalize/=S(1)),S(e))}function k(e){for(var t in e)C.hasOwnProperty(t)&&(z[t]=e[t])}var D,M,T,E,H,C={frameRate:150,animationTime:400,stepSize:100,pulseAlgorithm:!0,pulseScale:4,pulseNormalize:1,accelerationDelta:50,accelerationMax:3,keyboardSupport:!0,arrowScroll:50,touchpadSupport:!1,fixedBackground:!0,excluded:""},z=C,L=!1,X=!1,Y={x:0,y:0},A=!1,B=document.documentElement,O=[],K=/^Mac/.test(navigator.platform),N={left:37,up:38,right:39,down:40,spacebar:32,pageup:33,pagedown:34,end:35,home:36},q=[],P=!1,R=Date.now(),j=function(){var e=0;return function(t){return t.uniqueID||(t.uniqueID=e++)}}(),F={};window.localStorage&&localStorage.SS_deltaBuffer&&(O=localStorage.SS_deltaBuffer.split(","));var I,_=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||function(e,t,o){window.setTimeout(e,o||1e3/60)}}(),V=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver,$=function(){var e;return function(){if(!e){var t=document.createElement("div");t.style.cssText="height:10000px;width:1px;",document.body.appendChild(t);{var o=document.body.scrollTop;document.documentElement.scrollTop}window.scrollBy(0,1),e=document.body.scrollTop!=o?document.body:document.documentElement,window.scrollBy(0,-1),document.body.removeChild(t)}return e}}(),U=window.navigator.userAgent,W=/Edge/.test(U),G=/chrome/i.test(U)&&!W,J=/safari/i.test(U)&&!W,Q=/mobile/i.test(U),Z=(G||J)&&!Q;"onwheel"in document.createElement("div")?I="wheel":"onmousewheel"in document.createElement("div")&&(I="mousewheel"),I&&Z&&(f(I,r),f("mousedown",i),f("load",t)),k.destroy=o,window.SmoothScrollOptions&&k(window.SmoothScrollOptions),"object"==typeof exports?module.exports=k:window.SmoothScroll=k}(); \ No newline at end of file From 926641c320f73aa8236b643a0426510a5cae7cfa Mon Sep 17 00:00:00 2001 From: lotress Date: Fri, 1 Mar 2019 02:15:03 +0800 Subject: [PATCH 05/10] clean about.js --- src/js/about.js | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/js/about.js b/src/js/about.js index a4b394b..0b8bab4 100644 --- a/src/js/about.js +++ b/src/js/about.js @@ -1,26 +1,6 @@ import './common.js' import $ from 'jquery' -/* -support_doc ='
    \ -
    \ -

    %s

    ' -support_row_doc = '
    ' -def about_supporter(): - info_doc = codecs.open('static/supporter.json',encoding='utf-8').read() - info_doc = json.loads(info_doc) - show_doc = support_row_doc - counter = 0 - for k in info_doc.keys(): - show_doc += support_doc%('static/savatar/'+k+'.jpg',k,info_doc[k]) - counter += 1 - if counter%4 ==0: - counter = 0 - show_doc += '
    ' - show_doc += support_row_doc - return show_doc -*/ + const prefix = 'http://may-workshop.com/moephoto/' const jsonUrl = `${prefix}supporter.json` const supportView = (img, name, link) => `
    From 3344f9fd200d9a8b425dae2f97e57569175a156c Mon Sep 17 00:00:00 2001 From: lotress Date: Fri, 1 Mar 2019 02:16:05 +0800 Subject: [PATCH 06/10] refactor js --- python/server.py | 6 +- src/js/app.js | 10 +- src/js/common.js | 10 +- src/js/main.js | 356 +++++++++++++++++++++++++++++++++++++++++++ src/js/progress.js | 11 +- src/js/steps.js | 339 +--------------------------------------- templates/batch.html | 72 ++------- templates/index.html | 20 +-- webpack.config.js | 5 +- 9 files changed, 399 insertions(+), 430 deletions(-) create mode 100644 src/js/main.js diff --git a/python/server.py b/python/server.py index dbb2fe5..fcce978 100644 --- a/python/server.py +++ b/python/server.py @@ -233,19 +233,21 @@ def batchEnhance(): os.makedirs(output_path) opt = readOpt(request) total = len(fileList) - print(total) + print('batch total: {}'.format(total)) current.notifier.send({ 'eta': total, 'gone': 0, 'total': total }) + opt[-1]['trace'] = False for image in fileList: if current.stopFlag.is_set(): result = 'Interrupted' break name = output_path + os.path.basename(image.filename) start = time.time() - sender.send(('image_enhance', current.writeFile(image), *opt, { 'op': 'output', 'file': name, 'trace': False })) + opt[-1]['file'] = name + sender.send(('image_enhance', current.writeFile(image), *opt)) while not receiver.poll(): idle() if receiver.poll(): diff --git a/src/js/app.js b/src/js/app.js index f551cf1..51db972 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -1,4 +1,4 @@ -import { getResource, getSession, newMessager, texts } from './common.js' +import { newMessager, texts } from './common.js' import $ from 'jquery' const reconnectPeriod = 5 const setup = opt => { @@ -110,9 +110,5 @@ const setup = opt => { } return messager } -const app = { getSession, getResource, setup, texts } -if (window.app) - Object.assign(window.app, app) -else - window.app = app -export default app \ No newline at end of file + +export { setup, texts } \ No newline at end of file diff --git a/src/js/common.js b/src/js/common.js index 23bbe58..fdc6293 100644 --- a/src/js/common.js +++ b/src/js/common.js @@ -9,7 +9,6 @@ import $ from 'jquery' window.$ = $ import 'bootstrap' import { jarallax } from 'jarallax' -import './SmoothScroll.min.js' import './jquery.totemticker.js' import './move-top.min.js' import './easing.js' @@ -58,7 +57,14 @@ const texts = { processing: '处理中', stopping: '等待保存已处理部分', onBusy: gone => '忙碌中' + (gone == null ? '' : `,已经过${gone}秒`), - timeFormatter: time => `,预计还需要${time.toFixed(2)}秒` + timeFormatter: time => `,预计还需要${time.toFixed(2)}秒`, + batchSucc: result => [ + result[0] === 'Success' ? texts.finish : '中途被打断了', + `,处理了${result[1]}张图片`, + result[2] ? `,然而有${result[2]}张失败了` : '', + `,请查看这里` + ].join(''), + batchRunning: (gone, total) => `处理中,共${total}张,已处理${gone}张` } const appendText = key => text => text + texts[key] const [setLanguage, registryLanguageListener] = (_ => { diff --git a/src/js/main.js b/src/js/main.js new file mode 100644 index 0000000..f9cede0 --- /dev/null +++ b/src/js/main.js @@ -0,0 +1,356 @@ +import $ from 'jquery' +import { appendText, texts, getResource } from './common.js' +import { addPanel, initListeners, submit, context } from './steps.js' +import { setup } from './progress.js' + +const setAll = (arr, key) => values => arr.map((o, i) => o[key] = values[i]) +const copyTruly = obj => { + let res = {} + for (let key in obj) obj[key] && (res[key] = obj[key]) + return res +} +var SRScaleValues = [ + { value: 2, text: '2倍', checked: 1 }, + { value: 3, text: '3倍' }, + { value: 4, text: '4倍' } +] +const setScaleDisabled = setAll(SRScaleValues, 'disabled') +const scaleModelMapping = { + a: [0, 0, 0], + p: [0, 0, 0], + lite: [0, 1, 0], + gan: [1, 1, 0] +} +var getResizeView = (by, scale, size) => + by === 'scale' ? scale + '倍' : appendText('pixel')(size) +const panels = { + input: { + text: '输入', + description: '选择一张你需要放大的图片,开始体验吧!运行完毕请点击保存', + position: 0, + submit: (opt, data) => opt.file && opt.file[0] && data.set('file', opt.file[0]) && void 0, + view: opt => ({ file: opt.file && opt.file[0] ? opt.file[0].name : '请选择' }), + args: { + file: { + type: 'file', + name: 'file', + text: '图片', + classes: ['inputfile-6', 'imgInp'] + } + } + }, + inputVideo: { + text: '输入', + description: '选择一段需要放大的视频!运行完毕请点击保存', + position: 0, + submit: (opt, data) => opt.file && opt.file[0] && data.set('file', opt.file[0]) && void 0, + view: opt => ({ file: opt.file && opt.file[0] ? opt.file[0].name : '请选择' }), + args: { + file: { + type: 'file', + name: 'file', + text: '视频', + classes: ['inputfile-6'], + notes: [ + '视频会复制一份上传,存放在程序的upload目录下' + ] + } + } + }, + inputBatch: { + text: '批量输入', + description: '将所有需要放大的图片放置到一个文件夹内,并在下方选择路径', + position: 0, + submit: (opt, data) => opt.file && opt.file.length && [...opt.file].forEach(f => data.append('file', f, f.name)) && void 0, + view: opt => ({ file: opt.file && opt.file.length ? [opt.file[0].name, '等', opt.file.length, '个'].join('') : '请选择' }), + args: { + file: { + type: 'file', + name: 'file', + text: '文件', + classes: ['inputfile-6'], + attributes: ['webkitdirectory', 'directory'] + } + } + }, + output: { + text: '输出', + description: '只是占个位置', + position: -1 + }, + SR: { + text: '超分辨率', + description: '以2、3、4倍整数比例放大图像', + draggable: 1, + args: { + scale: { + type: 'radio', + text: '放大倍数', + values: SRScaleValues + }, + model: { + type: 'radio', + text: '超分模型', + change: (_, opt) => { + setScaleDisabled(scaleModelMapping[opt.model]) + if (SRScaleValues[opt.scale - 2].disabled) + for (opt.scale = 2; SRScaleValues[opt.scale - 2].disabled; opt.scale++); + return 1 + }, + values: [ + { value: 'a', text: '动漫', checked: 1 }, + { value: 'p', text: '照片' }, + { + value: 'lite', + text: '快速', + notes: [ + '快速模型仅能放大2倍或4倍,可以在后面添加“缩放”步骤配合使用' + ] + }, + { + value: 'gan', + text: 'GAN', + notes: [ + 'GAN模型仅适用于RGB图像', + 'GAN模型仅能放大4倍,可以在后面添加“缩放”步骤配合使用' + ] + } + ] + } + } + }, + resize: { + text: '缩放', + description: '以插值方法缩放图像,对图像的宽高可以分别设定长度大小或者缩放比例', + draggable: 1, + submit: opt => { + let res = { method: opt.method } + opt.byW === 'scale' ? res.scaleW = opt.scaleW : res.width = opt.width + opt.byH === 'scale' ? res.scaleH = opt.scaleH : res.height = opt.height + return res + }, + view: opt => { + let res = { method: panels.resize.args.method.values.find(item => item.value === opt.method).text } + res.byW = getResizeView(opt.byW, opt.scaleW, opt.width) + res.byH = getResizeView(opt.byH, opt.scaleH, opt.height) + return res + }, + args: { + method: { + type: 'radio', + text: '插值方法', + values: [ + { value: 'nearest', text: '最近邻' }, + { value: 'bilinear', text: '双线性', checked: 1 } + ] + }, + byW: { + type: 'radio', + text: '宽度', + values: [ + { value: 'scale', binds: ['scaleW'] }, + { value: 'pixel', binds: ['width'], checked: 1 } + ] + }, + scaleW: { + type: 'number', + text: '缩放比例', + value: 1, + classes: ['input-number'] + }, + width: { + type: 'number', + text: '大小', + value: 1920, + view: appendText('pixel'), + classes: ['input-number'] + }, + byH: { + type: 'radio', + text: '高度', + values: [ + { value: 'scale', binds: ['scaleH'] }, + { value: 'pixel', binds: ['height'], checked: 1 } + ] + }, + scaleH: { + type: 'number', + text: '缩放比例', + value: 1, + classes: ['input-number'] + }, + height: { + type: 'number', + text: '大小', + value: 1080, + view: appendText('pixel'), + classes: ['input-number'] + } + } + }, + DN: { + text: '降噪', + description: '相对较快的轻量级降噪', + draggable: 1, + args: { + model: { + type: 'radio', + text: '降噪强度', + values: [ + { value: 'lite5', text: '弱', checked: 1 }, + { value: 'lite10', text: '中' }, + { value: 'lite15', text: '强' } + ] + } + } + }, + ednoise: { + op: 'DN', + text: '强力降噪', + description: '这里的降噪非常强,涂抹效果显著,可以试试制作油画风格的照片或者galgame的背景图~', + draggable: 1, + args: { + model: { + type: 'radio', + text: '级别', + values: [ + { value: 'lite5', text: '弱', checked: 1 }, + { value: 'lite10', text: '中' }, + { value: 'lite15', text: '强' } + ] + } + } + }, + dehaze: { + text: '去雾', + description: '去雾AODnet', + draggable: 1 + }, + decode: { + text: '输入解码', + description: '传入ffmpeg的解码参数设定,跟命令行使用是一样的,默认不做额外的解码处理', + position: 1, + submit: copyTruly, + view: copyTruly, + args: { + codec: { + type: 'text', + text: '解码参数', + value: '', + classes: ['input-text'], + notes: [ + '请不要在这里设置颜色格式', + '注意无论下一步的开始于设定为多少,解码都是从视频头开始的' + ] + }, + width: { + type: 'number', + text: '覆盖输入宽度', + value: 0, + view: appendText('pixel'), + classes: ['input-number'] + }, + height: { + type: 'number', + text: '覆盖输入高度', + value: 0, + view: appendText('pixel'), + classes: ['input-number'], + notes: ['我们从输入视频文件里获取画面大小信息,如果这里的解码处理改变了画面大小,请设置上面的两个覆盖值从而告诉后面的处理过程,否则就不要动它们啦'] + } + } + }, + range: { + text: '处理范围', + description: '如果不需要处理整段视频,可以在这里设置', + position: 2, + submit: copyTruly, + view: copyTruly, + args: { + start: { + type: 'number', + text: '开始于', + value: 0, + view: html => `第${html}帧`, + classes: ['input-number'], + notes: [ + '处理范围指的是经过上一步解码处理后的第几帧到第几帧,首帧为第0帧', + '如果想继续上次中止掉的视频处理,上次完成到第几帧这次这里就填多少,其他配置保持一致,最后的多个输出可以用其他工具直接无损拼接', + '注意只有视频画面才有帧的概念,若开始大于0,则除画面外的其余轨道将会被忽略,否则其余轨道将会被直接完整复制到输出中' + ] + }, + stop: { + type: 'number', + text: '结束于', + value: 0, + view: html => `第${html}帧`, + classes: ['input-number'], + notes: [ + '输出不包括上述编号的帧,即输出帧数为结束减开始,若该值小于等于开始则忽略并处理到视频结尾' + ] + } + } + }, + encode: { + text: '输出编码', + description: '传入ffmpeg的编码参数设定,跟命令行使用是一样的,这里通常指定常见的编码器和颜色格式', + position: -1, + submit: copyTruly, + view: copyTruly, + args: { + codec: { + type: 'text', + text: '编码参数', + value: 'libx264 -pix_fmt yuv420p', + classes: ['input-text'], + notes: [ + '传入ffmpeg的编码参数设定,默认设定兼容性最好但是质量和效率都普通', + '一定要以编码器名开头,也一定要指定输出颜色格式(-pix_fmt <格式名>)', + '输出视频以mkv格式封装,因为这啥都能装,有许多免费工具能够无损转换封装格式,我们就不做这事了' + ] + }, + frameRate: { + type: 'number', + text: '覆盖输出帧率', + value: 0, + view: appendText('fps'), + classes: ['input-number'], + notes: [ + '默认的输出帧率就是输入乘上插帧倍数,这样保持时间长度与输入一致,如果您想要看慢动作什么的可以在这里设定', + '注意我们只处理视频画面部分,设置了这个之后声音字幕什么的通常就对不上了' + ] + } + } + }, + slomo: { + text: '插帧', + description: '以可设定的整倍数填充视频画面帧', + draggable: 1, + args: { + sf: { + type: 'number', + text: '倍数', + value: 1, + classes: ['input-number'], + notes: [ + '输出帧数是输入的多少倍,必须是正整数', + '为了方便精确地拼接输出视频,若之前的视频处理开始于设定大于0且这里设置了大于1的倍数,那么开始帧之前的那一帧会被用作参考帧,输出的头几帧将会是它与开始帧之间的插入帧,但这一参考帧本身将不会被输出' + ] + } + } + } +} + +for (let key in panels) addPanel(key, panels[key]) +const setupMain = opt => { + opt.progress = $('#progress') + opt.beforeSend = submit + setup(opt) + initListeners() + context.setFeatures(opt.features) +} +const exportApp = { setup: setupMain, texts, getResource } +if (window.app) + Object.assign(window.app, exportApp) +else + window.app = exportApp \ No newline at end of file diff --git a/src/js/progress.js b/src/js/progress.js index ff0fc6f..b35b041 100644 --- a/src/js/progress.js +++ b/src/js/progress.js @@ -1,6 +1,6 @@ import $ from 'jquery' import { getResource, getSession, texts } from './common.js' -import app from './app.js' +import { setup } from './app.js' const bindProgress = $ele => { var intervalId = 0, remain = 0, bar = $ele.find('.progress-bar'), msgBox = $ele.find('.message'), timeBox = $ele.find('.time'), progress @@ -72,8 +72,7 @@ const bindMessager = ($ele, messager) => { } return progress } -let _setup = app.setup -const setup = opt => { +const setupProgress = opt => { var stopButton = $('#StopButton').hide(), runButton = $('#RunButton'), total = 0 const setPreview = opt.outputImg ? (_ => { let idle = true @@ -98,7 +97,7 @@ const setup = opt => { data.total ? total = data.total : 0 data.gone ? progress.setMessage(opt.onProgress(data.gone, total, data)) : 0 } - const messager = _setup(opt) + const messager = setup(opt) messager.on('message', onMessage).on('open', onMessage) const progress = bindMessager(opt.progress, messager) opt.onErrorMsg = data => progress.setMessage(texts.onBusy(data.gone, total, data)) @@ -137,9 +136,9 @@ const setup = opt => { }) return progress } -const exportApp = { getSession, getResource, setup } +const exportApp = { getSession, getResource, setup: setupProgress } if (window.app) Object.assign(window.app, exportApp) else window.app = exportApp -export default exportApp \ No newline at end of file +export { setupProgress as setup } \ No newline at end of file diff --git a/src/js/steps.js b/src/js/steps.js index c656efb..3d10ab6 100644 --- a/src/js/steps.js +++ b/src/js/steps.js @@ -1,340 +1,10 @@ import $ from 'jquery' -import { texts, appendText, registryLanguageListener } from './common.js' +import { texts, registryLanguageListener } from './common.js' -const setAll = (arr, key) => values => arr.map((o, i) => o[key] = values[i]) -const copyTruly = obj => { - let res = {} - for (let key in obj) obj[key] && (res[key] = obj[key]) - return res -} -var SRScaleValues = [ - { value: 2, text: '2倍', checked: 1 }, - { value: 3, text: '3倍' }, - { value: 4, text: '4倍' } -] -const setScaleDisabled = setAll(SRScaleValues, 'disabled') -const scaleModelMapping = { - a: [0, 0, 0], - p: [0, 0, 0], - lite: [0, 1, 0], - gan: [1, 1, 0] -} -var getResizeView = (by, scale, size) => - by === 'scale' ? scale + '倍' : appendText('pixel')(size) const panels = { index: { text: '请选择一项功能', draggable: 1 - }, - input: { - text: '输入', - description: '选择一张你需要放大的图片,开始体验吧!运行完毕请点击保存', - position: 0, - submit: (opt, data) => opt.file && opt.file[0] && data.set('file', opt.file[0]) && void 0, - view: opt => ({ file: opt.file && opt.file[0] ? opt.file[0].name : '请选择' }), - args: { - file: { - type: 'file', - name: 'file', - text: '图片', - classes: ['inputfile-6', 'imgInp'] - } - } - }, - inputVideo: { - text: '输入', - description: '选择一段需要放大的视频!运行完毕请点击保存', - position: 0, - submit: (opt, data) => opt.file && opt.file[0] && data.set('file', opt.file[0]) && void 0, - view: opt => ({ file: opt.file && opt.file[0] ? opt.file[0].name : '请选择' }), - args: { - file: { - type: 'file', - name: 'file', - text: '视频', - classes: ['inputfile-6'], - notes: [ - '视频会复制一份上传,存放在程序的upload目录下' - ] - } - } - }, - inputBatch: { - text: '批量输入', - description: '将所有需要放大的图片放置到一个文件夹内,并在下方选择路径', - position: 0, - submit: (opt, data) => opt.file && opt.file.length && data.set('file', opt.file) && void 0, - view: opt => ({ file: opt.file && opt.file.length ? [opt.file[0].name, '等', opt.file.length, '个'].join('') : '请选择' }), - args: { - file: { - type: 'file', - name: 'file', - text: '文件', - classes: ['inputfile-6'], - attributes: ['webkitdirectory', 'directory'] - } - } - }, - SR: { - text: '超分辨率', - description: '以2、3、4倍整数比例放大图像', - draggable: 1, - args: { - scale: { - type: 'radio', - text: '放大倍数', - values: SRScaleValues - }, - model: { - type: 'radio', - text: '超分模型', - change: (_, opt) => { - setScaleDisabled(scaleModelMapping[opt.model]) - if (SRScaleValues[opt.scale - 2].disabled) - for (opt.scale = 2; SRScaleValues[opt.scale - 2].disabled; opt.scale++); - return 1 - }, - values: [ - { value: 'a', text: '动漫', checked: 1 }, - { value: 'p', text: '照片' }, - { - value: 'lite', - text: '快速', - notes: [ - '快速模型仅能放大2倍或4倍,可以在后面添加“缩放”步骤配合使用' - ] - }, - { - value: 'gan', - text: 'GAN', - notes: [ - 'GAN模型仅适用于RGB图像', - 'GAN模型仅能放大4倍,可以在后面添加“缩放”步骤配合使用' - ] - } - ] - } - } - }, - resize: { - text: '缩放', - description: '以插值方法缩放图像,对图像的宽高可以分别设定长度大小或者缩放比例', - draggable: 1, - submit: opt => { - let res = { method: opt.method } - opt.byW === 'scale' ? res.scaleW = opt.scaleW : res.width = opt.width - opt.byH === 'scale' ? res.scaleH = opt.scaleH : res.height = opt.height - return res - }, - view: opt => { - let res = { method: panels.resize.args.method.values.find(item => item.value === opt.method).text } - res.byW = getResizeView(opt.byW, opt.scaleW, opt.width) - res.byH = getResizeView(opt.byH, opt.scaleH, opt.height) - return res - }, - args: { - method: { - type: 'radio', - text: '插值方法', - values: [ - { value: 'nearest', text: '最近邻' }, - { value: 'bilinear', text: '双线性', checked: 1 } - ] - }, - byW: { - type: 'radio', - text: '宽度', - values: [ - { value: 'scale', binds: ['scaleW'] }, - { value: 'pixel', binds: ['width'], checked: 1 } - ] - }, - scaleW: { - type: 'number', - text: '缩放比例', - value: 1, - classes: ['input-number'] - }, - width: { - type: 'number', - text: '大小', - value: 1920, - view: appendText('pixel'), - classes: ['input-number'] - }, - byH: { - type: 'radio', - text: '高度', - values: [ - { value: 'scale', binds: ['scaleH'] }, - { value: 'pixel', binds: ['height'], checked: 1 } - ] - }, - scaleH: { - type: 'number', - text: '缩放比例', - value: 1, - classes: ['input-number'] - }, - height: { - type: 'number', - text: '大小', - value: 1080, - view: appendText('pixel'), - classes: ['input-number'] - } - } - }, - DN: { - text: '降噪', - description: '相对较快的轻量级降噪', - draggable: 1, - args: { - model: { - type: 'radio', - text: '降噪强度', - values: [ - { value: 'lite5', text: '弱', checked: 1 }, - { value: 'lite10', text: '中' }, - { value: 'lite15', text: '强' } - ] - } - } - }, - ednoise: { - op: 'DN', - text: '强力降噪', - description: '这里的降噪非常强,涂抹效果显著,可以试试制作油画风格的照片或者galgame的背景图~', - draggable: 1, - args: { - model: { - type: 'radio', - text: '级别', - values: [ - { value: 'lite5', text: '弱', checked: 1 }, - { value: 'lite10', text: '中' }, - { value: 'lite15', text: '强' } - ] - } - } - }, - dehaze: { - text: '去雾', - description: '去雾AODnet', - draggable: 1 - }, - decode: { - text: '输入解码', - description: '传入ffmpeg的解码参数设定,跟命令行使用是一样的,默认不做额外的解码处理', - position: 1, - submit: copyTruly, - view: copyTruly, - args: { - codec: { - type: 'text', - text: '解码参数', - value: '', - classes: ['input-text'], - notes: [ - '请不要在这里设置颜色格式', - '注意无论下一步的开始于设定为多少,解码都是从视频头开始的' - ] - }, - width: { - type: 'number', - text: '覆盖输入宽度', - value: 0, - view: appendText('pixel'), - classes: ['input-number'] - }, - height: { - type: 'number', - text: '覆盖输入高度', - value: 0, - view: appendText('pixel'), - classes: ['input-number'], - notes: ['我们从输入视频文件里获取画面大小信息,如果这里的解码处理改变了画面大小,请设置上面的两个覆盖值从而告诉后面的处理过程,否则就不要动它们啦'] - } - } - }, - range: { - text: '处理范围', - description: '如果不需要处理整段视频,可以在这里设置', - position: 2, - submit: copyTruly, - view: copyTruly, - args: { - start: { - type: 'number', - text: '开始于', - value: 0, - view: html => `第${html}帧`, - classes: ['input-number'], - notes: [ - '处理范围指的是经过上一步解码处理后的第几帧到第几帧,首帧为第0帧', - '如果想继续上次中止掉的视频处理,上次完成到第几帧这次这里就填多少,其他配置保持一致,最后的多个输出可以用其他工具直接无损拼接', - '注意只有视频画面才有帧的概念,若开始大于0,则除画面外的其余轨道将会被忽略,否则其余轨道将会被直接完整复制到输出中' - ] - }, - stop: { - type: 'number', - text: '结束于', - value: 0, - view: html => `第${html}帧`, - classes: ['input-number'], - notes: [ - '输出不包括上述编号的帧,即输出帧数为结束减开始,若该值小于等于开始则忽略并处理到视频结尾' - ] - } - } - }, - encode: { - text: '输出编码', - description: '传入ffmpeg的编码参数设定,跟命令行使用是一样的,这里通常指定常见的编码器和颜色格式', - position: -1, - submit: copyTruly, - view: copyTruly, - args: { - codec: { - type: 'text', - text: '编码参数', - value: 'libx264 -pix_fmt yuv420p', - classes: ['input-text'], - notes: [ - '传入ffmpeg的编码参数设定,默认设定兼容性最好但是质量和效率都普通', - '一定要以编码器名开头,也一定要指定输出颜色格式(-pix_fmt <格式名>)', - '输出视频以mkv格式封装,因为这啥都能装,有许多免费工具能够无损转换封装格式,我们就不做这事了' - ] - }, - frameRate: { - type: 'number', - text: '覆盖输出帧率', - value: 0, - view: appendText('fps'), - classes: ['input-number'], - notes: [ - '默认的输出帧率就是输入乘上插帧倍数,这样保持时间长度与输入一致,如果您想要看慢动作什么的可以在这里设定', - '注意我们只处理视频画面部分,设置了这个之后声音字幕什么的通常就对不上了' - ] - } - } - }, - slomo: { - text: '插帧', - description: '以可设定的整倍数填充视频画面帧', - draggable: 1, - args: { - sf: { - type: 'number', - text: '倍数', - value: 1, - classes: ['input-number'], - notes: [ - '输出帧数是输入的多少倍,必须是正整数', - '为了方便精确地拼接输出视频,若之前的视频处理开始于设定大于0且这里设置了大于1的倍数,那么开始帧之前的那一帧会被用作参考帧,输出的头几帧将会是它与开始帧之间的插入帧,但这一参考帧本身将不会被输出' - ] - } - } } } @@ -613,9 +283,4 @@ const submit = data => data.set('steps', JSON.stringify(steps return opt }).filter(opt => !!opt))) -const app = { addPanel, initListeners, submit, context } -if (window.app) - Object.assign(window.app, app) -else - window.app = app -export default app \ No newline at end of file +export { addPanel, initListeners, submit, context } \ No newline at end of file diff --git a/templates/batch.html b/templates/batch.html index e57dede..b32a066 100644 --- a/templates/batch.html +++ b/templates/batch.html @@ -58,45 +58,8 @@

    批量放大

    属性设置
    -
    -
    -
    - - -
    -
    -
    -
    放大倍数       - 不放大        - 2X        - 3X        - 4X -
    超分模型       - 动漫        - 照片        - 快速 (仅2X,4X放大)
    - GAN (仅适用于RGB图像,仅4X放大) -
    -
    -
    是否降噪       - 不降噪        - 弱        - 中        - 强        -
    降噪顺序       - 先降噪 -        - 后降噪        -
    -
    +
    +

    @@ -125,32 +88,15 @@

    批量放大

    - + diff --git a/templates/index.html b/templates/index.html index e3d20e4..4aa04e8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -197,15 +197,19 @@

    开始吧

    属性设置
    -
    -
    -
    +
    +
    +

    +
    +
    +
    +
    @@ -223,8 +227,7 @@

    开始吧

    - - + diff --git a/webpack.config.js b/webpack.config.js index 2e4acc2..67d597c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,13 +6,12 @@ const ManifestPlugin = require('webpack-manifest-plugin') module.exports = (env, argv) => { return { - mode: 'production', + mode: 'development',//'production', target: 'web', entry: { - app: './src/js/app.js', progress: './src/js/progress.js', about: './src/js/about.js', - steps: './src/js/steps.js' + main: './src/js/main.js' }, output: { filename: 'static/js/[name].js', From 5f61869e98b5fb77d1dbc0c5d5ee5b1ff17c6d23 Mon Sep 17 00:00:00 2001 From: lotress Date: Sat, 2 Mar 2019 00:22:58 +0800 Subject: [PATCH 07/10] 4.5.0 --- python/FIFOcache.py | 56 +++++++++++++++++ python/MoePhoto.py | 8 +-- python/defaultConfig.py | 5 +- python/imageProcess.py | 24 ++++--- python/server.py | 91 ++++++++++++++++----------- python/worker.py | 5 +- src/js/app.js | 21 +++---- src/js/common.js | 13 +++- src/js/lock.js | 41 ++++++++++++ src/js/main.js | 10 +-- src/js/progress.js | 11 ++-- src/js/steps.js | 8 +-- src/js/system.js | 52 +++++++++++++++ static/lock.html | 52 --------------- templates/1-header.html | 6 -- templates/batch.html | 2 +- templates/dehaze.html | 131 -------------------------------------- templates/document.html | 2 +- templates/ednoise.html | 136 ---------------------------------------- templates/gallery.html | 7 +-- templates/index.html | 30 ++------- templates/lock.html | 60 ++++++++++++++++++ templates/system.html | 52 +-------------- templates/video.html | 123 ++++++------------------------------ webpack.config.js | 6 +- 25 files changed, 354 insertions(+), 598 deletions(-) create mode 100644 python/FIFOcache.py create mode 100644 src/js/lock.js create mode 100644 src/js/system.js delete mode 100644 static/lock.html delete mode 100644 templates/dehaze.html delete mode 100644 templates/ednoise.html create mode 100644 templates/lock.html diff --git a/python/FIFOcache.py b/python/FIFOcache.py new file mode 100644 index 0000000..d11a00f --- /dev/null +++ b/python/FIFOcache.py @@ -0,0 +1,56 @@ +import time +from gevent.queue import Queue +from gevent import spawn_later + +def runPeriod(func, period): + flag = True + def f(): + if flag: + spawn_later(period, f) + return func() + spawn_later(period, f) + def stop(): + nonlocal flag + flag = False + return stop + +Null = lambda *_: None + +class Cache(): + def __init__(self, size, lifetime=3600, onExtinct=Null): + self.lifetime = lifetime + self.queue = Queue(size) + self.stop = runPeriod(self.clean, lifetime) + self.out = onExtinct + + def put(self, item): + t = (time.time(), item) + while True: + try: + self.queue.put_nowait(t) + except: + self.clean(True) + else: + break + return self.queue.qsize() + + def clean(self, force=False): + if not hasattr(self, 'queue'): + return 0 + old = time.time() - self.lifetime + count = 0 + while not self.queue.empty(): + t = self.queue.peek_nowait()[0] + if t < old: + self.out(self.queue.get_nowait()[1]) + count += 1 + else: + break + if force and self.queue.qsize() == self.queue.maxsize: + self.out(self.queue.get_nowait()[1]) + return count + + def destroy(self): + self.stop() + del self.queue + del self \ No newline at end of file diff --git a/python/MoePhoto.py b/python/MoePhoto.py index b7f22b0..d4c2a24 100644 --- a/python/MoePhoto.py +++ b/python/MoePhoto.py @@ -30,6 +30,7 @@ def lock(duration): flag.wait(1) flag.clear() node.trace() + node.trace(0, result=duration) return duration def imageEnhance(size, *args): @@ -42,11 +43,10 @@ def imageEnhance(size, *args): return begin(imNode, nodes, True if trace else -1).bindFunc(process)(size, name=name) return getMM(), { - 'lock': lock, + 'lockInterface': lock, 'image_enhance': enhance(imageEnhance), + 'batch': enhance(imageEnhance, False), 'video_enhance': enhance(SR_vid), - 'ednoise_enhance': enhance(imageEnhance), - 'image_dehaze': enhance(imageEnhance), 'systemInfo': enhance(config.system) } @@ -60,7 +60,7 @@ def imageEnhance(size, *args): mp.Process(target=worker, args=(main, taskInReceiver, taskOutSender, notifier, stopEvent), daemon=True).start() from server import runserver from defaultConfig import defaultConfig - run = runserver(taskInSender, taskOutReceiver, noter, stopEvent, notifier, getMM()) + run = runserver(taskInSender, taskOutReceiver, noter, stopEvent, getMM()) host = '127.0.0.1' port = defaultConfig['port'][0] if len(sys.argv) > 1: diff --git a/python/defaultConfig.py b/python/defaultConfig.py index cbc61e9..6616c2f 100644 --- a/python/defaultConfig.py +++ b/python/defaultConfig.py @@ -15,6 +15,9 @@ 'outDir': ('download', '输出目录'), 'uploadDir': ('upload', '上传目录'), 'logPath': ('.user/log.txt', '日志文件路径'), - 'sharedMemSize': (100 * 2 ** 20, '前后台共享的内存文件交换区字节大小,要能装下png格式的一张输入或输出图片'), + 'videoPreview': ('jpeg', '视频预览格式'), + 'maxResultsKept': (1 << 10, '最多缓存几个运行结果'), + 'resultKept': (3600, '运行结果缓存多久'), + 'sharedMemSize': (100 * 2 ** 20, '前后台共享的内存文件交换区字节大小,要能装下一张输入或输出图片'), 'port': (2333, '监听端口') } \ No newline at end of file diff --git a/python/imageProcess.py b/python/imageProcess.py index 63a39e2..3e8eb55 100644 --- a/python/imageProcess.py +++ b/python/imageProcess.py @@ -14,6 +14,7 @@ deviceCPU = torch.device('cpu') outDir = defaultConfig['outDir'][0] +previewFormat = defaultConfig['videoPreview'][0] log = logging.getLogger('Moe') genNameByTime = lambda: '{}/output_{}.png'.format(outDir, int(time.time())) padImageReflect = torch.nn.ReflectionPad2d @@ -259,7 +260,7 @@ def initModel(model, weights=None): trans = [transpose, flip, flip2, combine(flip, transpose), combine(transpose, flip), combine(transpose, flip, transpose), combine(flip2, transpose)] transInv = [transpose, flip, flip2, trans[4], trans[3], trans[5], trans[6]] ensemble = lambda x, es, kwargs: reduce((lambda v, t: v + t[2](doCrop(x=t[1](x), **kwargs))), zip(range(es), trans, transInv), doCrop(x=x, **kwargs)) -previewPath = defaultConfig['outDir'][0] + '/.preview.png' +previewPath = defaultConfig['outDir'][0] + '/.preview.{}'.format(previewFormat if previewFormat else '') def toInt(o, keys): for key in keys: @@ -283,7 +284,7 @@ def appendFuncs(f, node, funcs, wrap=True): toOutput8, (lambda im: im.astype(np.uint8)), 0, - lambda im: writeFile(im, context.shared, 'PNG'), + lambda im: writeFile(im, context.shared, previewFormat), lambda *_: context.root.trace(0, preview=previewPath, fileSize=context.shared.tell())] funcPreview = lambda im: reduce(applyNonNull, fPreview, im) @@ -335,7 +336,7 @@ def procDehaze(opt, out, *_): def procResize(opt, out, nodes): node = Node(dict(op='resize', mode=opt['method']), 1, name=opt['name'] if 'name' in opt else None) - return [node.bindFunc(resize(opt, out, len(nodes), nodes))], [node], out + return [node.bindFunc(NonNullWrap(resize(opt, out, len(nodes), nodes)))], [node], out def procOutput(opt, out, *_): load = out['load'] @@ -348,10 +349,13 @@ def procOutput(opt, out, *_): if out['source']: fPreview[0] = restrictSize(2048) fs1 = [node0.bindFunc(toFloat), fOutput] - def o(im): - res = reduce(applyNonNull, fs1, im) - funcPreview(im) - return [res] + if previewFormat: + def o(im): + res = reduce(applyNonNull, fs1, im) + funcPreview(im) + return [res] + else: + o = lambda im: [reduce(applyNonNull, fs1, im)] fs = [o] if out['channel']: fPreview[4] = BGR2RGB @@ -380,7 +384,11 @@ def genProcess(steps, root=True, outType=None): toInt(opt, ['scale']) opt['opt'] = runSR.getOpt(opt['scale'], opt['model'], config.ensembleSR) for opt in filter((lambda opt: opt['op'] == 'resize'), steps): - toInt(opt, ['scaleW', 'scaleH', 'width', 'height']) + toInt(opt, ['width', 'height']) + if 'scaleW' in opt: + opt['scaleW'] = float(opt['scaleW']) + if 'scaleH' in opt: + opt['scaleH'] = float(opt['scaleH']) for opt in filter((lambda opt: opt['op'] == 'DN'), steps): opt['opt'] = runDN.getOpt(opt['model']) for opt in filter((lambda opt: opt['op'] == 'dehaze'), steps): diff --git a/python/server.py b/python/server.py index fcce978..3664933 100644 --- a/python/server.py +++ b/python/server.py @@ -7,6 +7,7 @@ from flask import Flask, render_template, request, jsonify, send_from_directory, make_response, Response, send_file from gevent import pywsgi, idle, spawn from defaultConfig import defaultConfig +from FIFOcache import Cache staticMaxAge = 86400 app = Flask(__name__, root_path='.') @@ -18,6 +19,9 @@ def current():pass current.path = None current.eta = 0 current.fileSize = 0 +current.result = 0 +results = {} +current.getResult = lambda key: results.pop(key, OK) E403 = ('Not authorized.', 403) E404 = ('Not Found', 404) OK = ('', 200) @@ -26,6 +30,7 @@ def current():pass outDir = defaultConfig['outDir'][0] uploadDir = defaultConfig['uploadDir'][0] logPath = os.path.abspath(defaultConfig['logPath'][0]) +previewFormat = defaultConfig['videoPreview'][0] downDir = os.path.join(app.root_path, outDir) if not os.path.exists(outDir): os.mkdir(outDir) @@ -44,7 +49,7 @@ def acquireSession(request): current.eta = 10 return False if current.session else E403 -def controlPoint(path, fMatch, fUnmatch, resNoCurrent, check=lambda *_: True): +def controlPoint(path, fMatch, fUnmatch, fNoCurrent, check=lambda *_: True): def f(): try: session = request.values['session'] @@ -55,7 +60,7 @@ def f(): if current.session: return spawn(fMatch).get() if current.session == session and check(request) else fUnmatch() else: - return resNoCurrent + return fNoCurrent(session) app.route(path, methods=['GET', 'POST'], endpoint=path)(f) def stopCurrent(): @@ -70,12 +75,15 @@ def checkMsgMatch(request): return path == current.path def onConnect(): - while current.session and not noter.poll(): + while current.session and not (current.result or noter.poll()): idle() - if current.session and noter.poll(): + if current.result or noter.poll(): + res = current.result + current.result = 0 while noter.poll(): res = noter.recv() - current.eta = res['eta'] + if 'eta' in res: + current.eta = res['eta'] if 'fileSize' in res: current.fileSize = res['fileSize'] del res['fileSize'] @@ -91,9 +99,9 @@ def f(): sender.send((name, *prepare(request))) while not receiver.poll(): idle() - result = receiver.recv() - current.session = None - return final(result) + result = final(receiver.recv()) + current.putResult(result) + return OK app.route('/' + name, methods=methods, endpoint=name)(f) def renderPage(item, header=None, footer=None): @@ -176,12 +184,11 @@ def getDynamicInfo(_): ('/', 'index.html', '主页', None), ('/video', 'video.html', 'AI视频', None), ('/batch', 'batch.html', '批量放大', None), - ('/ednoise', 'ednoise.html', '风格化', None), - ('/dehaze', 'dehaze.html', '去雾', None), ('/document', 'document.html', None, None), ('/about', 'about.html', None, about_updater, ['log']), ('/system', 'system.html', None, getDynamicInfo, ['disk_free', 'mem_free', 'session', 'path'], getSystemInfo()), - ('/gallery', 'gallery.html', None, gallery, ['var']) + ('/gallery', 'gallery.html', None, gallery, ['var']), + ('/lock', 'lock.html', None, None) ] for item in routes: @@ -195,20 +202,20 @@ def getDynamicInfo(_): identity = lambda x: x readOpt = lambda req: json.loads(req.values['steps']) -controlPoint('/stop', stopCurrent, lambda: E403, E404) -controlPoint('/msg', onConnect, busy, OK, checkMsgMatch) +controlPoint('/stop', stopCurrent, lambda: E403, lambda *_: E404) +controlPoint('/msg', onConnect, busy, current.getResult, checkMsgMatch) app.route('/log', endpoint='log')(lambda: send_file(logPath, add_etags=False)) app.route('/favicon.ico', endpoint='favicon')(lambda: send_from_directory(app.root_path, 'logo3.ico')) -app.route("/{}/.preview.png".format(outDir), endpoint='preview')(lambda: Response(current.getPreview(), mimetype='image/png')) +if previewFormat: + app.route("/{}/.preview.{}".format(outDir, previewFormat), endpoint='preview')( + lambda: Response(current.getPreview(), mimetype='image/{}'.format(previewFormat))) sendFromDownDir = lambda filename: send_from_directory(downDir, filename, as_attachment=True) app.route("/{}/".format(outDir), endpoint='download')(sendFromDownDir) -lockFinal = lambda result: (jsonify(result='Interrupted', remain=result), 499) if result > 0 else OK -makeHandler('lock', (lambda req: [int(req.values['duration'])]), lockFinal, ['GET', 'POST']) +lockFinal = lambda result: (jsonify(result='Interrupted', remain=result), 200) if result > 0 else OK +makeHandler('lockInterface', (lambda req: [int(float(readOpt(req)[0]['duration']))]), lockFinal, ['GET', 'POST']) makeHandler('systemInfo', (lambda _: []), identity, ['GET', 'POST']) imageEnhancePrep = lambda req: (current.writeFile(req.files['file']), *readOpt(req)) makeHandler('image_enhance', imageEnhancePrep, identity) -makeHandler('ednoise_enhance', imageEnhancePrep, identity) -makeHandler('image_dehaze', lambda req: (current.writeFile(req.files['file']), {'op': 'dehaze'}), identity) def videoEnhancePrep(req): vidfile = req.files['file'] @@ -234,11 +241,11 @@ def batchEnhance(): opt = readOpt(request) total = len(fileList) print('batch total: {}'.format(total)) - current.notifier.send({ + current.result = { 'eta': total, 'gone': 0, 'total': total - }) + } opt[-1]['trace'] = False for image in fileList: if current.stopFlag.is_set(): @@ -247,31 +254,29 @@ def batchEnhance(): name = output_path + os.path.basename(image.filename) start = time.time() opt[-1]['file'] = name - sender.send(('image_enhance', current.writeFile(image), *opt)) + sender.send(('batch', current.writeFile(image), *opt)) while not receiver.poll(): idle() - if receiver.poll(): - output = receiver.recv() - count += 1 - note = { - 'eta': (total - count) * (time.time() - start), - 'gone': count, - 'total': total - } - if output[1] == 200: - note['preview'] = name - else: - fail += 1 - current.notifier.send(note) - current.session = None - return json.dumps({'result': (result, count, fail, output_path)}, ensure_ascii=False) + output = receiver.recv() + count += 1 + note = { + 'eta': (total - count) * (time.time() - start), + 'gone': count, + 'total': total + } + if output[1] == 200: + note['preview'] = name + else: + fail += 1 + current.result = note + current.putResult({'result': (result, count, fail, output_path)}) + return OK -def runserver(taskInSender, taskOutReceiver, noteReceiver, stopEvent, notifier, mm): +def runserver(taskInSender, taskOutReceiver, noteReceiver, stopEvent, mm): global sender, receiver, noter sender = taskInSender receiver = taskOutReceiver noter = noteReceiver - current.notifier = notifier current.stopFlag = stopEvent def preview(): mm.seek(0) @@ -282,6 +287,16 @@ def writeFile(file): mm.seek(0) return file._file.readinto(mm) current.writeFile = writeFile + def putResult(res): + key = str(current.session) + if not (type(res) == tuple and len(res) == 2): + res = (json.dumps(res, ensure_ascii=False), 200) + results[key] = res + resultsIndices.put(key) + current.session = None + current.putResult = putResult + resultsIndices = Cache(defaultConfig['maxResultsKept'][0], defaultConfig['resultKept'][0], + lambda key: results.pop(key, None)) def f(host, port): app.debug = False app.config['SERVER_NAME'] = None diff --git a/python/worker.py b/python/worker.py index 2afbbe0..03cd1d0 100644 --- a/python/worker.py +++ b/python/worker.py @@ -44,7 +44,7 @@ def onProgress(node, kwargs={}): res['stageTotal'] = node.total context.notifier.send(res) -def enhance(f): +def enhance(f, sendResult=True): def g(*args, **kwargs): try: res = { 'result': f(*args, **kwargs) } @@ -58,7 +58,8 @@ def g(*args, **kwargs): code = 400 finally: clean() - onProgress(context.root, res) + if sendResult: + onProgress(context.root, res) return (json.dumps(res, ensure_ascii=False), code) return g diff --git a/src/js/app.js b/src/js/app.js index 51db972..d3706f7 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -40,8 +40,8 @@ const setup = opt => { let result = event.data.result if (result === 'Fail') onError(0, 400, event.data.exception) - else if (result) - onSuccess(result) + else if (result != null) + onSuccess(event.data) else runButton.attr('disabled', true) } @@ -78,30 +78,25 @@ const setup = opt => { console.error(xhr, status, error) clearInterval(intervalId) loading.hide() - opt.error ? opt.error(texts.errorMsg) : alert(texts.errorMsg) + opt.error ? opt.error(texts.errorMsg, xhr) : alert(texts.errorMsg) } if (opt.session) { - runButton.bind('click', function () { + runButton.bind('click', _ => { var fdata = new FormData() opt.beforeSend && opt.beforeSend(fdata) - if (!fdata.get('file').size) return opt.setMessage ? opt.setMessage(texts.noFileMsg) : alert(texts.noFileMsg) - $.ajax({ + if (!(opt.noCheckFile || fdata.get('file').size)) return opt.setMessage ? opt.setMessage(texts.noFileMsg) : alert(texts.noFileMsg) + $.post({ url: `${opt.path}?session=${opt.session}`, - type: "POST", data: fdata, - cache: false, contentType: false, processData: false, - async: true, - dataType: 'json', beforeSend: _ => { loading.show() runButton.attr('disabled', true) intervalId = setInterval(openMessager, 200) - }, - success: onSuccess, - error: onError + openMessager() + } }) }) } else { diff --git a/src/js/common.js b/src/js/common.js index fdc6293..04f5a78 100644 --- a/src/js/common.js +++ b/src/js/common.js @@ -42,6 +42,8 @@ $(document).ready($ => { }) }) const getResource = path => [path, '?', (new Date()).getTime()].join('') +const processingMsg = '处理中' +const genOnProgress = (unit, msg = processingMsg) => (gone, total) => `${msg},共${total}${unit},已处理${gone}${unit}` const texts = { step: '步骤', add: '点击添加...', @@ -49,13 +51,17 @@ const texts = { labelSplitter: ':', pixel: '像素', fps: '帧每秒', + second: '秒', noFileMsg: '缺少输入文件', errorMsg: '出错啦', idle: '空闲中', finish: '完成啦', running: '正在处理您的任务', - processing: '处理中', + processing: processingMsg, stopping: '等待保存已处理部分', + logWritten: '日志已写入浏览器控制台,请按F12查看', + noMoreLog: '没有新的日志', + needRefresh: '请在空闲后刷新', onBusy: gone => '忙碌中' + (gone == null ? '' : `,已经过${gone}秒`), timeFormatter: time => `,预计还需要${time.toFixed(2)}秒`, batchSucc: result => [ @@ -64,7 +70,10 @@ const texts = { result[2] ? `,然而有${result[2]}张失败了` : '', `,请查看这里` ].join(''), - batchRunning: (gone, total) => `处理中,共${total}张,已处理${gone}张` + videoSucc: result => `完成啦,处理到第${result[1]}帧`, + batchRunning: genOnProgress('张'), + videoRunning: genOnProgress('帧'), + lockRunning: genOnProgress('秒', '锁定中') } const appendText = key => text => text + texts[key] const [setLanguage, registryLanguageListener] = (_ => { diff --git a/src/js/lock.js b/src/js/lock.js new file mode 100644 index 0000000..6502914 --- /dev/null +++ b/src/js/lock.js @@ -0,0 +1,41 @@ +import $ from 'jquery' +import { appendText, texts } from './common.js' +import { addPanel, initListeners, submit, context } from './steps.js' +import { setup } from './progress.js' + +const path = '/lockInterface' +const lockPanel = { + text: '锁定设置', + description: '把后台锁起来一段时间内不让用,其他访问会看到“忙碌中”', + position: 0, + args: { + duration: { + type: 'number', + text: '持续时间', + value: 10, + view: appendText('second'), + classes: ['input-number'], + notes: ['只能趁没人在用的空隙提交', '计时精度为整数秒'] + } + } +} +addPanel('lock', lockPanel) +$(document).ready(_ => { + initListeners() + context.setFeatures(['lock']) + const progress = setup({ + onProgress: texts.lockRunning, + progress: $('#progress'), + beforeSend: submit, + path, + noCheckFile: 1, + error: (_, xhr) => { + let busy = xhr ? xhr.responseJSON ? xhr.responseJSON.eta == null ? 0 : 1 : 0 : 0 + if (busy) { + progress.setStatus(+xhr.responseJSON.eta) + } else { + console.error(xhr) + } + } + }) +}) \ No newline at end of file diff --git a/src/js/main.js b/src/js/main.js index f9cede0..e19f3ba 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -23,12 +23,14 @@ const scaleModelMapping = { } var getResizeView = (by, scale, size) => by === 'scale' ? scale + '倍' : appendText('pixel')(size) +const setFile = opt => ({ file: opt.file && opt.file[0] ? opt.file[0].name : '请选择' }) +const submitFile = (opt, data) => opt.file && opt.file[0] && data.set('file', opt.file[0]) && void 0 const panels = { input: { text: '输入', description: '选择一张你需要放大的图片,开始体验吧!运行完毕请点击保存', position: 0, - submit: (opt, data) => opt.file && opt.file[0] && data.set('file', opt.file[0]) && void 0, + submit: submitFile, view: opt => ({ file: opt.file && opt.file[0] ? opt.file[0].name : '请选择' }), args: { file: { @@ -43,14 +45,14 @@ const panels = { text: '输入', description: '选择一段需要放大的视频!运行完毕请点击保存', position: 0, - submit: (opt, data) => opt.file && opt.file[0] && data.set('file', opt.file[0]) && void 0, - view: opt => ({ file: opt.file && opt.file[0] ? opt.file[0].name : '请选择' }), + submit: submitFile, + view: setFile, args: { file: { type: 'file', name: 'file', text: '视频', - classes: ['inputfile-6'], + classes: ['inputfile-6', 'imgInp'], notes: [ '视频会复制一份上传,存放在程序的upload目录下' ] diff --git a/src/js/progress.js b/src/js/progress.js index b35b041..2783a7e 100644 --- a/src/js/progress.js +++ b/src/js/progress.js @@ -110,16 +110,17 @@ const setupProgress = opt => { progress.begin(texts.running) beforeSend && beforeSend(data) } - let success = opt.success + let success = opt.success, error = opt.error opt.success = result => { runButton.show() stopButton.hide() success && success(result, progress) } - opt.error = msg => { + opt.error = (msg, xhr) => { progress.final(msg) runButton.show() stopButton.hide() + error && error(xhr) } stopButton.click(_ => { $.ajax({ @@ -136,9 +137,5 @@ const setupProgress = opt => { }) return progress } -const exportApp = { getSession, getResource, setup: setupProgress } -if (window.app) - Object.assign(window.app, exportApp) -else - window.app = exportApp + export { setupProgress as setup } \ No newline at end of file diff --git a/src/js/steps.js b/src/js/steps.js index 3d10ab6..a611d63 100644 --- a/src/js/steps.js +++ b/src/js/steps.js @@ -197,14 +197,13 @@ const context = (steps => { const compareOp = (a, b) => a.position - b.position const pushNewStep = panel => steps.push(newStep(panel)) const setFeatures = features => { - var arr = ['index'].concat(features) - addibleFeatures = features.filter(name => panels[name].draggable) - let ps = arr.map(name => panels[name]), + addibleFeatures = features.filter(name => panels[name].draggable && name !== 'index') + let ps = features.map(name => panels[name]), tops = ps.filter(panel => panel.position >= 0).sort(compareOp), bottoms = ps.filter(panel => panel.position < 0).sort(compareOp) tops.forEach(pushNewStep) index = steps.length - steps.push(indexStep) + features.includes('index') && steps.push(indexStep) bottoms.forEach(pushNewStep) let listeners = panels.index.listeners addibleFeatures.forEach(name => listeners.click.push({ @@ -225,6 +224,7 @@ const context = (steps => { const refreshSteps = target => { target && (pos = target) index = steps.indexOf(indexStep) + index < 0 && (index = steps.length) refreshPanel(0) $('#steps').html(steps.map(getStepNIndicatorHTML(pos)).join('')) } diff --git a/src/js/system.js b/src/js/system.js new file mode 100644 index 0000000..47ce71f --- /dev/null +++ b/src/js/system.js @@ -0,0 +1,52 @@ +import $ from 'jquery' +import { texts, getSession } from './common.js' +import { setup } from './progress.js' + +const path = '/systemInfo' + +$(document).ready(_ => { + var session = getSession(), GPUfrees = $('.freeGPUMemory'), logButton = $('#logButton') + const progress = setup({ + session, + progress: $('#progress'), + path, + success: freeMems => { + progress.final(texts.idle) + GPUfrees.each((i, elem) => elem.innerHTML = freeMems[i]) + } + }) + session && $.ajax({ + url: path, + data: { session }, + cache: false, + beforeSend: _ => progress.begin(texts.running), + error: (xhr, status, error) => { + let busy = xhr ? xhr.responseJSON ? xhr.responseJSON.eta == null ? 0 : 1 : 0 : 0 + if (busy) { + progress.setStatus(+xhr.responseJSON.eta) + GPUfrees.text(texts.needRefresh) + } else { + console.error(xhr, status, error) + progress.final(texts.errorMsg) + } + } + }) + logButton.attr('href', '#') + logButton.click(_ => { + logButton.attr('disabled', true) + $.get('/log') + .then(data => { + data = data.split('\n').filter(line => line.length) + .map(JSON.parse).map(ev => { + ev.time = new Date(ev.time) + return ev + }) + if (data.length) { + progress.final(texts.logWritten) + data.forEach(item => console.log(item)) + } else progress.final(texts.noMoreLog) + }) + .catch(console.error.bind(console)) + .then(_ => logButton.attr('disabled', false)) + }) +}) \ No newline at end of file diff --git a/static/lock.html b/static/lock.html deleted file mode 100644 index 2683551..0000000 --- a/static/lock.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - Lock - - - - - -
    -

    - -
    -
    -
    -
    - - - - - - - \ No newline at end of file diff --git a/templates/1-header.html b/templates/1-header.html index 22eeb5c..8d6bbd8 100644 --- a/templates/1-header.html +++ b/templates/1-header.html @@ -31,12 +31,6 @@

  • AI视频
  • -
  • - 风格化 -
  • -
  • - 去雾 -
  • 相册
  • diff --git a/templates/batch.html b/templates/batch.html index b32a066..2b486cd 100644 --- a/templates/batch.html +++ b/templates/batch.html @@ -96,7 +96,7 @@

    批量放大

    success: (result, progress) => progress.final(app.texts.batchSucc(result)), path: '/batch_enhance', outputImg: $('#outputImg'), - features: ['inputBatch', 'output', 'SR', 'DN', 'dehaze', 'ednoise', 'resize'] + features: ['index', 'inputBatch', 'output', 'SR', 'DN', 'dehaze', 'ednoise', 'resize'] })) diff --git a/templates/dehaze.html b/templates/dehaze.html deleted file mode 100644 index 387ed97..0000000 --- a/templates/dehaze.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - MoePhoto 去雾 - - - - - - - - - {% autoescape true %} {{header| safe}} {% endautoescape %} - -
    - -
    -
    -
    -
    -

    去雾

    -

    去雾AODnet

    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - 属性设置 -
    -
    -
    -
    -
    - - -
    - -
    -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    - -
    - - {% autoescape true %} {{footer| safe}} {% endautoescape %} - - - - - - - - - \ No newline at end of file diff --git a/templates/document.html b/templates/document.html index 05afd73..b7c1447 100644 --- a/templates/document.html +++ b/templates/document.html @@ -46,7 +46,7 @@

    Moe Photo Image Toolbox - + \ No newline at end of file diff --git a/templates/ednoise.html b/templates/ednoise.html deleted file mode 100644 index 647e706..0000000 --- a/templates/ednoise.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - MoePhoto 强力降噪 - - - - - - - - - {% autoescape true %} {{header| safe}} {% endautoescape %} - -
    - -
    -
    -
    -
    -

    强力降噪

    -

    这里的降噪非常强,涂抹效果显著,可以试试制作油画风格的照片或者galgame的背景图~

    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - 属性设置 -
    -
    -
    -
    -
    - - -
    -
    -
    级别       - 弱        - 中        - 强        -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    - -
    - - {% autoescape true %} {{footer| safe}} {% endautoescape %} - - - - - - - - - \ No newline at end of file diff --git a/templates/gallery.html b/templates/gallery.html index 48e333f..0642ace 100644 --- a/templates/gallery.html +++ b/templates/gallery.html @@ -38,12 +38,7 @@

    Gallery相册

    - - + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 4aa04e8..fcf6a2e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -51,12 +51,6 @@

  • AI视频
  • -
  • - 风格化 -
  • -
  • - 去雾 -
  • 相册
  • @@ -118,7 +112,7 @@

    - @@ -229,12 +212,11 @@

    开始吧

    diff --git a/templates/lock.html b/templates/lock.html new file mode 100644 index 0000000..25b4e57 --- /dev/null +++ b/templates/lock.html @@ -0,0 +1,60 @@ + + + + + MoePhoto Lock + + + + + + + + {% autoescape true %} {{header| safe}} {% endautoescape %} + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +

    + +
    +
    +
    +
    + + {% autoescape true %} {{footer| safe}} {% endautoescape %} + + + + + + + \ No newline at end of file diff --git a/templates/system.html b/templates/system.html index a551a8f..fe981b9 100644 --- a/templates/system.html +++ b/templates/system.html @@ -79,57 +79,7 @@

    Moe Photo Image Toolbox - - + diff --git a/templates/video.html b/templates/video.html index 0df99a1..d83c9b7 100644 --- a/templates/video.html +++ b/templates/video.html @@ -2,7 +2,7 @@ - MoePhoto 视频放大 + MoePhoto AI视频 @@ -27,7 +27,7 @@


    -

    视频放大

    +

    AI视频

    选择一段需要放大的视频!运行完毕请点击保存

    @@ -40,66 +40,8 @@

    视频放大

    属性设置
    -
    -
    -
    - - -
    -
    -
    输入解码设置
    - -
    -
    -
    - 开始于第
    - 结束于第帧 -
    -
    -
    放大倍数       - 不放大        - 2X        - 3X        - 4X -
    超分模型       - 动漫        - 照片        - 快速 (仅2X,4X放大)
    - GAN (仅4X放大) -
    -
    -
    是否降噪       - 不降噪 -        - 弱        - 中        - 强        -
    降噪顺序       - 先降噪 -        - 后降噪        -
    -
    -
    插帧倍数
    - -
    -
    -
    输出编码设置
    - -
    覆盖输出帧率
    - -
    - -
    +
    +

    @@ -155,50 +97,21 @@

    视频放大

    - + diff --git a/webpack.config.js b/webpack.config.js index 67d597c..84098d6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,11 +6,13 @@ const ManifestPlugin = require('webpack-manifest-plugin') module.exports = (env, argv) => { return { - mode: 'development',//'production', + mode: 'production', target: 'web', entry: { - progress: './src/js/progress.js', + system: './src/js/system.js', + lock: './src/js/lock.js', about: './src/js/about.js', + page: './src/js/common.js', main: './src/js/main.js' }, output: { From cd8455f3007c4298c85c96160f7050c9275c7d15 Mon Sep 17 00:00:00 2001 From: lotress Date: Sat, 2 Mar 2019 23:24:31 +0800 Subject: [PATCH 08/10] put fonts on local --- ...00,500italic,700,700italic,900,900italic.css | 6 +++--- src/css/Yanone Kaffeesatz-200,300,400,700.css | 8 ++++---- ...y9-6aknfjLm_3lMKjiMgmUUYBs04YegOv-qPNM.woff2 | Bin 0 -> 18252 bytes ...y9-6aknfjLm_3lMKjiMgmUUYBs04YewPf-qPNM.woff2 | Bin 0 -> 18628 bytes ...y9-6aknfjLm_3lMKjiMgmUUYBs04YfUPv-qPNM.woff2 | Bin 0 -> 17404 bytes .../3y976aknfjLm_3lMKjiMgmUUYBs04Y8bH-o.woff2 | Bin 0 -> 18568 bytes src/fonts/KFOlCnqEu92Fr1MmSU5fBBc4.woff2 | Bin 0 -> 15440 bytes src/fonts/KFOlCnqEu92Fr1MmWUlfBBc4.woff2 | Bin 0 -> 15436 bytes src/fonts/KFOmCnqEu92Fr1Mu4mxK.woff2 | Bin 0 -> 15344 bytes 9 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 src/fonts/3y9-6aknfjLm_3lMKjiMgmUUYBs04YegOv-qPNM.woff2 create mode 100644 src/fonts/3y9-6aknfjLm_3lMKjiMgmUUYBs04YewPf-qPNM.woff2 create mode 100644 src/fonts/3y9-6aknfjLm_3lMKjiMgmUUYBs04YfUPv-qPNM.woff2 create mode 100644 src/fonts/3y976aknfjLm_3lMKjiMgmUUYBs04Y8bH-o.woff2 create mode 100644 src/fonts/KFOlCnqEu92Fr1MmSU5fBBc4.woff2 create mode 100644 src/fonts/KFOlCnqEu92Fr1MmWUlfBBc4.woff2 create mode 100644 src/fonts/KFOmCnqEu92Fr1Mu4mxK.woff2 diff --git a/src/css/Roboto-400,100,100italic,300,300italic,400italic,500,500italic,700,700italic,900,900italic.css b/src/css/Roboto-400,100,100italic,300,300italic,400italic,500,500italic,700,700italic,900,900italic.css index aa3cb3d..c07bd0f 100644 --- a/src/css/Roboto-400,100,100italic,300,300italic,400italic,500,500italic,700,700italic,900,900italic.css +++ b/src/css/Roboto-400,100,100italic,300,300italic,400italic,500,500italic,700,700italic,900,900italic.css @@ -443,7 +443,7 @@ font-family: 'Roboto'; font-style: normal; font-weight: 300; - src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fBBc4.woff2) format('woff2'); + src: local('Roboto Light'), local('Roboto-Light'), url('../fonts/KFOlCnqEu92Fr1MmSU5fBBc4.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @@ -499,7 +499,7 @@ font-family: 'Roboto'; font-style: normal; font-weight: 400; - src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxK.woff2) format('woff2'); + src: local('Roboto'), local('Roboto-Regular'), url('../fonts/KFOmCnqEu92Fr1Mu4mxK.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @@ -611,7 +611,7 @@ font-family: 'Roboto'; font-style: normal; font-weight: 700; - src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfBBc4.woff2) format('woff2'); + src: local('Roboto Bold'), local('Roboto-Bold'), url('../fonts/KFOlCnqEu92Fr1MmWUlfBBc4.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ diff --git a/src/css/Yanone Kaffeesatz-200,300,400,700.css b/src/css/Yanone Kaffeesatz-200,300,400,700.css index 028c240..2aea933 100644 --- a/src/css/Yanone Kaffeesatz-200,300,400,700.css +++ b/src/css/Yanone Kaffeesatz-200,300,400,700.css @@ -27,7 +27,7 @@ font-family: 'Yanone Kaffeesatz'; font-style: normal; font-weight: 200; - src: local('Yanone Kaffeesatz ExtraLight'), local('YanoneKaffeesatz-ExtraLight'), url(https://fonts.gstatic.com/s/yanonekaffeesatz/v9/3y9-6aknfjLm_3lMKjiMgmUUYBs04YfUPv-qPNM.woff2) format('woff2'); + src: local('Yanone Kaffeesatz ExtraLight'), local('YanoneKaffeesatz-ExtraLight'), url('../fonts/3y9-6aknfjLm_3lMKjiMgmUUYBs04YfUPv-qPNM.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic */ @@ -59,7 +59,7 @@ font-family: 'Yanone Kaffeesatz'; font-style: normal; font-weight: 300; - src: local('Yanone Kaffeesatz Light'), local('YanoneKaffeesatz-Light'), url(https://fonts.gstatic.com/s/yanonekaffeesatz/v9/3y9-6aknfjLm_3lMKjiMgmUUYBs04YewPf-qPNM.woff2) format('woff2'); + src: local('Yanone Kaffeesatz Light'), local('YanoneKaffeesatz-Light'), url('../fonts/3y9-6aknfjLm_3lMKjiMgmUUYBs04YewPf-qPNM.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic */ @@ -91,7 +91,7 @@ font-family: 'Yanone Kaffeesatz'; font-style: normal; font-weight: 400; - src: local('Yanone Kaffeesatz Regular'), local('YanoneKaffeesatz-Regular'), url(https://fonts.gstatic.com/s/yanonekaffeesatz/v9/3y976aknfjLm_3lMKjiMgmUUYBs04Y8bH-o.woff2) format('woff2'); + src: local('Yanone Kaffeesatz Regular'), local('YanoneKaffeesatz-Regular'), url('../fonts/3y976aknfjLm_3lMKjiMgmUUYBs04Y8bH-o.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic */ @@ -123,6 +123,6 @@ font-family: 'Yanone Kaffeesatz'; font-style: normal; font-weight: 700; - src: local('Yanone Kaffeesatz Bold'), local('YanoneKaffeesatz-Bold'), url(https://fonts.gstatic.com/s/yanonekaffeesatz/v9/3y9-6aknfjLm_3lMKjiMgmUUYBs04YegOv-qPNM.woff2) format('woff2'); + src: local('Yanone Kaffeesatz Bold'), local('YanoneKaffeesatz-Bold'), url('../fonts/3y9-6aknfjLm_3lMKjiMgmUUYBs04YegOv-qPNM.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } diff --git a/src/fonts/3y9-6aknfjLm_3lMKjiMgmUUYBs04YegOv-qPNM.woff2 b/src/fonts/3y9-6aknfjLm_3lMKjiMgmUUYBs04YegOv-qPNM.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3acf72f3c071d910dd19a4ad8dbea15cdb199bcf GIT binary patch literal 18252 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 P=XG61zsXciguVv=4xKzF literal 0 HcmV?d00001 diff --git a/src/fonts/3y9-6aknfjLm_3lMKjiMgmUUYBs04YewPf-qPNM.woff2 b/src/fonts/3y9-6aknfjLm_3lMKjiMgmUUYBs04YewPf-qPNM.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..747681fe71cf1f25d92bef1634e854ec2dca7856 GIT binary patch literal 18628 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 literal 0 HcmV?d00001 diff --git a/src/fonts/3y976aknfjLm_3lMKjiMgmUUYBs04Y8bH-o.woff2 b/src/fonts/3y976aknfjLm_3lMKjiMgmUUYBs04Y8bH-o.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..27dd09425aab567bd11ff8f23654f47be731b339 GIT binary patch literal 18568 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 T9JpjM?lJWws2Ut&vjG4AIH9qJ literal 0 HcmV?d00001 diff --git a/src/fonts/KFOlCnqEu92Fr1MmSU5fBBc4.woff2 b/src/fonts/KFOlCnqEu92Fr1MmSU5fBBc4.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..52c5845a7c3803f313d8905ee2f83ac20a5bc9b3 GIT binary patch literal 15440 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 CPzPE7 literal 0 HcmV?d00001 diff --git a/src/fonts/KFOmCnqEu92Fr1Mu4mxK.woff2 b/src/fonts/KFOmCnqEu92Fr1Mu4mxK.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..7e854e669b7517d21df4c0b3c51047be86555015 GIT binary patch literal 15344 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 O6y7R}gQBj@I4BA`2c0zl literal 0 HcmV?d00001 From dd2340ed49c29b8c8e7b7ce856307331f7080a43 Mon Sep 17 00:00:00 2001 From: lotress Date: Sat, 2 Mar 2019 23:25:04 +0800 Subject: [PATCH 09/10] update result cache --- package.json | 2 +- python/FIFOcache.py | 76 +++++++++++++++-------------------------- python/MoePhoto.py | 3 +- python/defaultConfig.py | 1 - python/server.py | 55 ++++++++++++++--------------- python/worker.py | 6 ++-- src/js/app.js | 53 ++++++++++++++++------------ src/js/lock.js | 1 + src/js/message.js | 7 ++-- src/js/progress.js | 26 ++++++-------- src/js/system.js | 7 ++-- templates/batch.html | 3 +- templates/index.html | 3 +- templates/lock.html | 27 ++++++++------- templates/system.html | 3 +- templates/video.html | 3 +- 16 files changed, 127 insertions(+), 149 deletions(-) diff --git a/package.json b/package.json index a6f7404..a5a5235 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moephoto", - "version": "4.5.0", + "version": "4.5.2", "description": "MoePhoto Image Toolbox萌图工具箱 一个基于深度学习的AI图像修复软件(更新中...) 感兴趣的加QQ群320785467讨论 ### 基本功能 * 图像降噪 * 超分辨率 * 去雾 * 涂抹及风格化", "private": true, "directories": { diff --git a/python/FIFOcache.py b/python/FIFOcache.py index d11a00f..9cc499d 100644 --- a/python/FIFOcache.py +++ b/python/FIFOcache.py @@ -1,56 +1,34 @@ -import time -from gevent.queue import Queue -from gevent import spawn_later - -def runPeriod(func, period): - flag = True - def f(): - if flag: - spawn_later(period, f) - return func() - spawn_later(period, f) - def stop(): - nonlocal flag - flag = False - return stop +from collections import deque Null = lambda *_: None class Cache(): - def __init__(self, size, lifetime=3600, onExtinct=Null): - self.lifetime = lifetime - self.queue = Queue(size) - self.stop = runPeriod(self.clean, lifetime) - self.out = onExtinct + def __init__(self, size, default=None, onExtinct=Null): + self.cache = {} + self.size = size + self.queue = deque() + self.default = default + self.extinct = onExtinct - def put(self, item): - t = (time.time(), item) - while True: - try: - self.queue.put_nowait(t) - except: - self.clean(True) - else: - break - return self.queue.qsize() + def put(self, key, item): + if len(self.queue) == self.size: + while len(self.queue): + oldKey = self.queue.popleft() + if oldKey in self.cache: + oldItem = self.cache[oldKey] + del self.cache[oldKey] + self.extinct(oldKey, oldItem) + break + self.cache[key] = item + self.queue.append(key) - def clean(self, force=False): - if not hasattr(self, 'queue'): - return 0 - old = time.time() - self.lifetime - count = 0 - while not self.queue.empty(): - t = self.queue.peek_nowait()[0] - if t < old: - self.out(self.queue.get_nowait()[1]) - count += 1 - else: - break - if force and self.queue.qsize() == self.queue.maxsize: - self.out(self.queue.get_nowait()[1]) - return count + def pop(self, key): + if key in self.cache: + res = self.cache[key] + del self.cache[key] + return res + else: + return self.default - def destroy(self): - self.stop() - del self.queue - del self \ No newline at end of file + def peek(self, key): + return key in self.cache \ No newline at end of file diff --git a/python/MoePhoto.py b/python/MoePhoto.py index d4c2a24..0047185 100644 --- a/python/MoePhoto.py +++ b/python/MoePhoto.py @@ -30,7 +30,6 @@ def lock(duration): flag.wait(1) flag.clear() node.trace() - node.trace(0, result=duration) return duration def imageEnhance(size, *args): @@ -45,7 +44,7 @@ def imageEnhance(size, *args): return getMM(), { 'lockInterface': lock, 'image_enhance': enhance(imageEnhance), - 'batch': enhance(imageEnhance, False), + 'batch': enhance(imageEnhance), 'video_enhance': enhance(SR_vid), 'systemInfo': enhance(config.system) } diff --git a/python/defaultConfig.py b/python/defaultConfig.py index 6616c2f..a082374 100644 --- a/python/defaultConfig.py +++ b/python/defaultConfig.py @@ -17,7 +17,6 @@ 'logPath': ('.user/log.txt', '日志文件路径'), 'videoPreview': ('jpeg', '视频预览格式'), 'maxResultsKept': (1 << 10, '最多缓存几个运行结果'), - 'resultKept': (3600, '运行结果缓存多久'), 'sharedMemSize': (100 * 2 ** 20, '前后台共享的内存文件交换区字节大小,要能装下一张输入或输出图片'), 'port': (2333, '监听端口') } \ No newline at end of file diff --git a/python/server.py b/python/server.py index 3664933..3adc552 100644 --- a/python/server.py +++ b/python/server.py @@ -19,12 +19,10 @@ def current():pass current.path = None current.eta = 0 current.fileSize = 0 -current.result = 0 -results = {} -current.getResult = lambda key: results.pop(key, OK) E403 = ('Not authorized.', 403) E404 = ('Not Found', 404) OK = ('', 200) +cache = Cache(defaultConfig['maxResultsKept'][0], OK, lambda *args: print('abandoned', *args)) busy = lambda: (jsonify(result='Busy', eta=current.eta), 503) cwd = os.getcwd() outDir = defaultConfig['outDir'][0] @@ -38,6 +36,8 @@ def current():pass assetMapping = json.load(manifest) vendorsJs = assetMapping['vendors.js'] commonJs = assetMapping['common.js'] if 'common.js' in assetMapping else None +getKey = lambda session, request: request.values['path'] + str(session) if 'path' in request.values else '' +toResponse = lambda obj: (json.dumps(obj, ensure_ascii=False, separators=(',',':')), 200) def acquireSession(request): if current.session: @@ -46,6 +46,7 @@ def acquireSession(request): noter.recv() current.session = request.values['session'] current.path = request.path + current.key = current.path + str(current.session) current.eta = 10 return False if current.session else E403 @@ -60,7 +61,7 @@ def f(): if current.session: return spawn(fMatch).get() if current.session == session and check(request) else fUnmatch() else: - return fNoCurrent(session) + return fNoCurrent(session, request) app.route(path, methods=['GET', 'POST'], endpoint=path)(f) def stopCurrent(): @@ -75,22 +76,30 @@ def checkMsgMatch(request): return path == current.path def onConnect(): - while current.session and not (current.result or noter.poll()): + while current.key and not (noter.poll() or cache.peek(current.key)): idle() - if current.result or noter.poll(): - res = current.result - current.result = 0 - while noter.poll(): - res = noter.recv() + res = None + while current.key and noter.poll(): + res = noter.recv() + if res: if 'eta' in res: current.eta = res['eta'] if 'fileSize' in res: current.fileSize = res['fileSize'] del res['fileSize'] - return (json.dumps(res, ensure_ascii=False), 200) + return toResponse(res) + if cache.peek(current.key): + res = cache.pop(current.key) + return res else: return OK +def endSession(result=None): + cache.put(current.key, result) + current.key = '' + current.session = None + return OK + def makeHandler(name, prepare, final, methods=['POST']): def f(): c = acquireSession(request) @@ -99,9 +108,7 @@ def f(): sender.send((name, *prepare(request))) while not receiver.poll(): idle() - result = final(receiver.recv()) - current.putResult(result) - return OK + return endSession(final(receiver.recv())) app.route('/' + name, methods=methods, endpoint=name)(f) def renderPage(item, header=None, footer=None): @@ -202,8 +209,9 @@ def getDynamicInfo(_): identity = lambda x: x readOpt = lambda req: json.loads(req.values['steps']) +onRequestCache = lambda session, request: cache.pop(getKey(session, request)) controlPoint('/stop', stopCurrent, lambda: E403, lambda *_: E404) -controlPoint('/msg', onConnect, busy, current.getResult, checkMsgMatch) +controlPoint('/msg', onConnect, busy, onRequestCache, checkMsgMatch) app.route('/log', endpoint='log')(lambda: send_file(logPath, add_etags=False)) app.route('/favicon.ico', endpoint='favicon')(lambda: send_from_directory(app.root_path, 'logo3.ico')) if previewFormat: @@ -211,7 +219,7 @@ def getDynamicInfo(_): lambda: Response(current.getPreview(), mimetype='image/{}'.format(previewFormat))) sendFromDownDir = lambda filename: send_from_directory(downDir, filename, as_attachment=True) app.route("/{}/".format(outDir), endpoint='download')(sendFromDownDir) -lockFinal = lambda result: (jsonify(result='Interrupted', remain=result), 200) if result > 0 else OK +lockFinal = lambda result: (jsonify(result='Interrupted', remain=result), 200) if result > 0 else (jsonify(result='Idle'), 200) makeHandler('lockInterface', (lambda req: [int(float(readOpt(req)[0]['duration']))]), lockFinal, ['GET', 'POST']) makeHandler('systemInfo', (lambda _: []), identity, ['GET', 'POST']) imageEnhancePrep = lambda req: (current.writeFile(req.files['file']), *readOpt(req)) @@ -268,9 +276,8 @@ def batchEnhance(): note['preview'] = name else: fail += 1 - current.result = note - current.putResult({'result': (result, count, fail, output_path)}) - return OK + cache.put(current.key, toResponse(note)) + return endSession(toResponse({'result': (result, count, fail, output_path)})) def runserver(taskInSender, taskOutReceiver, noteReceiver, stopEvent, mm): global sender, receiver, noter @@ -287,16 +294,6 @@ def writeFile(file): mm.seek(0) return file._file.readinto(mm) current.writeFile = writeFile - def putResult(res): - key = str(current.session) - if not (type(res) == tuple and len(res) == 2): - res = (json.dumps(res, ensure_ascii=False), 200) - results[key] = res - resultsIndices.put(key) - current.session = None - current.putResult = putResult - resultsIndices = Cache(defaultConfig['maxResultsKept'][0], defaultConfig['resultKept'][0], - lambda key: results.pop(key, None)) def f(host, port): app.debug = False app.config['SERVER_NAME'] = None diff --git a/python/worker.py b/python/worker.py index 03cd1d0..ce59949 100644 --- a/python/worker.py +++ b/python/worker.py @@ -44,7 +44,7 @@ def onProgress(node, kwargs={}): res['stageTotal'] = node.total context.notifier.send(res) -def enhance(f, sendResult=True): +def enhance(f): def g(*args, **kwargs): try: res = { 'result': f(*args, **kwargs) } @@ -58,9 +58,7 @@ def g(*args, **kwargs): code = 400 finally: clean() - if sendResult: - onProgress(context.root, res) - return (json.dumps(res, ensure_ascii=False), code) + return (json.dumps(res, ensure_ascii=False, separators=(',', ':')), code) return g def worker(main, taskIn, taskOut, notifier, stopEvent): diff --git a/src/js/app.js b/src/js/app.js index d3706f7..cf382cf 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -26,27 +26,31 @@ const setup = opt => { options.find('.imgInp')[0].files = e.dataTransfer.files }, false) } - var downloader = $('#downloader'), loading = $('#FG'), runButton = $('#RunButton'), intervalId + var downloader = $('#downloader'), loading = $('#FG'), runButton = $('#RunButton'), intervalId, running = 0 loading.hide() downloader.hide() const messager = newMessager('/msg', opt.session) - .on('message', event => { - if (!event.data) { - messager.abort() - runButton.attr('disabled', false) - } else { - clearInterval(intervalId) - let result = event.data.result - if (result === 'Fail') - onError(0, 400, event.data.exception) - else if (result != null) - onSuccess(event.data) - else - runButton.attr('disabled', true) - } - }).on('error', event => { + const onMessage = event => { + if (!event.data) { + messager.abort() + running && openMessager() && (running = 0) + runButton.attr('disabled', false) + } else { + clearInterval(intervalId) + let result = event.data.result + if (result === 'Fail') + onError(0, 400, event.data.exception) + else if (result != null) + onSuccess(event.data) + else + running || ((running = 1) && runButton.attr('disabled', true)) + } + } + messager.on('message', onMessage).on('open', onMessage) + .on('error', event => { console.error(event) + running = 0 clearInterval(intervalId) runButton.attr('disabled', true) let eta = 0 @@ -67,6 +71,7 @@ const setup = opt => { const onSuccess = result => { console.log(result) + running = 0 clearInterval(intervalId) loading.hide() downloader.show() @@ -76,11 +81,20 @@ const setup = opt => { const onError = (xhr, status, error) => { console.error(xhr, status, error) + running = 0 clearInterval(intervalId) loading.hide() opt.error ? opt.error(texts.errorMsg, xhr) : alert(texts.errorMsg) } + const beforeSend = messager.beforeSend = _ => { + running = 1 + loading.show() + intervalId = setInterval(openMessager, 200) + openMessager() + return messager + } + if (opt.session) { runButton.bind('click', _ => { var fdata = new FormData() @@ -91,12 +105,7 @@ const setup = opt => { data: fdata, contentType: false, processData: false, - beforeSend: _ => { - loading.show() - runButton.attr('disabled', true) - intervalId = setInterval(openMessager, 200) - openMessager() - } + beforeSend: _ => beforeSend() && runButton.attr('disabled', true) }) }) } else { diff --git a/src/js/lock.js b/src/js/lock.js index 6502914..599b6f5 100644 --- a/src/js/lock.js +++ b/src/js/lock.js @@ -29,6 +29,7 @@ $(document).ready(_ => { beforeSend: submit, path, noCheckFile: 1, + success: _ => progress.final(''), error: (_, xhr) => { let busy = xhr ? xhr.responseJSON ? xhr.responseJSON.eta == null ? 0 : 1 : 0 : 0 if (busy) { diff --git a/src/js/message.js b/src/js/message.js index d6d5b91..440707d 100644 --- a/src/js/message.js +++ b/src/js/message.js @@ -21,10 +21,9 @@ export const newMessager = (url, session, opt = {}) => { } const onError = (xhr, _, error) => m.fire({ type: 'error', data: formatData(xhr.responseJSON), error }) const pend = res => { - if (m.status) { - m.xhr = $.ajax(getOpt()) - .then((data => m.fire({ type: 'message', data: formatData(data) })), onError) - } else m.xhr = null + m.status ? m.xhr = $.ajax(getOpt()) + .then((data => m.fire({ type: 'message', data: formatData(data) })), onError) + : m.xhr = null return res } m.on = (type, listener) => { diff --git a/src/js/progress.js b/src/js/progress.js index 2783a7e..42a848f 100644 --- a/src/js/progress.js +++ b/src/js/progress.js @@ -2,7 +2,7 @@ import $ from 'jquery' import { getResource, getSession, texts } from './common.js' import { setup } from './app.js' const bindProgress = $ele => { - var intervalId = 0, remain = 0, bar = $ele.find('.progress-bar'), + var intervalId = 0, remain = 0, bar = $ele.find('.progress-bar'), statusBox = $ele.find('.status'), msgBox = $ele.find('.message'), timeBox = $ele.find('.time'), progress const elapse = _ => { bar[0].value += 1 @@ -27,34 +27,29 @@ const bindProgress = $ele => { return progress } const setMessage = data => { - if (typeof data === 'string') { + if (typeof data === 'string') msgBox.html(data) - } else if (data && data.result) { - msgBox.html(data.result) - } return progress } - const setStatus = eta => { + const setStatus = str => statusBox.html(str) && progress + const setTime = eta => { intervalId || show() bar[0].max = eta + +bar[0].value remain = eta timeBox.text(texts.timeFormatter(remain)) return progress } - return progress = { show, hide, setMessage, setStatus } + return progress = { show, hide, setMessage, setStatus, setTime } } const bindMessager = ($ele, messager) => { const progress = bindProgress($ele) const onMessage = event => { if (!event.data) { - progress.hide().setMessage(texts.idle) + progress.hide().setStatus(texts.idle) progress.status || messager.abort() } else { let data = event.data - if (data) { - progress.setMessage(data) - if (data.eta) progress.setStatus(+data.eta) - } + data && data.eta && progress.setTime(+data.eta) } } messager.on('message', onMessage) @@ -70,6 +65,7 @@ const bindMessager = ($ele, messager) => { progress.status = 1 return progress.show().setMessage(msg) } + progress.beforeSend = messager.beforeSend return progress } const setupProgress = opt => { @@ -95,13 +91,13 @@ const setupProgress = opt => { } data.preview ? setPreview(getResource(data.preview)) : 0 data.total ? total = data.total : 0 - data.gone ? progress.setMessage(opt.onProgress(data.gone, total, data)) : 0 + data.gone ? progress.setStatus(opt.onProgress(data.gone, total, data)) : 0 } const messager = setup(opt) messager.on('message', onMessage).on('open', onMessage) const progress = bindMessager(opt.progress, messager) - opt.onErrorMsg = data => progress.setMessage(texts.onBusy(data.gone, total, data)) - opt.setStatus = progress.setStatus + opt.onErrorMsg = data => progress.setStatus(texts.onBusy(data.gone, total, data)) + opt.setStatus = progress.setTime opt.setMessage = progress.setMessage let beforeSend = opt.beforeSend opt.beforeSend = data => { diff --git a/src/js/system.js b/src/js/system.js index 47ce71f..9bc4162 100644 --- a/src/js/system.js +++ b/src/js/system.js @@ -10,16 +10,13 @@ $(document).ready(_ => { session, progress: $('#progress'), path, - success: freeMems => { - progress.final(texts.idle) - GPUfrees.each((i, elem) => elem.innerHTML = freeMems[i]) - } + success: freeMems => progress.final('') && GPUfrees.each((i, elem) => elem.innerHTML = freeMems[i]) }) session && $.ajax({ url: path, data: { session }, cache: false, - beforeSend: _ => progress.begin(texts.running), + beforeSend: _ => progress.begin(texts.running).beforeSend(), error: (xhr, status, error) => { let busy = xhr ? xhr.responseJSON ? xhr.responseJSON.eta == null ? 0 : 1 : 0 : 0 if (busy) { diff --git a/templates/batch.html b/templates/batch.html index 2b486cd..afae460 100644 --- a/templates/batch.html +++ b/templates/batch.html @@ -76,7 +76,8 @@

    批量放大

    -

    +

    +


    diff --git a/templates/index.html b/templates/index.html index fcf6a2e..b39b711 100644 --- a/templates/index.html +++ b/templates/index.html @@ -198,7 +198,8 @@

    开始吧

    -

    +

    +


    diff --git a/templates/lock.html b/templates/lock.html index 25b4e57..568d95b 100644 --- a/templates/lock.html +++ b/templates/lock.html @@ -5,7 +5,7 @@ MoePhoto Lock - - - + + {% autoescape true %} {{footer| safe}} {% endautoescape %} + + + + \ No newline at end of file diff --git a/templates/system.html b/templates/system.html index fe981b9..d3a3d52 100644 --- a/templates/system.html +++ b/templates/system.html @@ -67,7 +67,8 @@

    Moe Photo Image Toolbox

    -

    +

    +


    diff --git a/templates/video.html b/templates/video.html index d83c9b7..fa3c5f0 100644 --- a/templates/video.html +++ b/templates/video.html @@ -85,7 +85,8 @@

    AI视频

    -

    +

    +


    From 7a6186bb2d9244cbd56c2d7b15a04dbe5277214e Mon Sep 17 00:00:00 2001 From: lotress Date: Tue, 5 Mar 2019 21:25:36 +0800 Subject: [PATCH 10/10] 4.5.4 --- package.json | 2 +- python/video.py | 23 +++++++---------------- server.bat | 6 ++++++ src/js/main.js | 3 ++- templates/1-header.html | 9 ++++++--- templates/index.html | 9 ++++++--- 6 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 server.bat diff --git a/package.json b/package.json index a5a5235..7e66cb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moephoto", - "version": "4.5.2", + "version": "4.5.4", "description": "MoePhoto Image Toolbox萌图工具箱 一个基于深度学习的AI图像修复软件(更新中...) 感兴趣的加QQ群320785467讨论 ### 基本功能 * 图像降噪 * 超分辨率 * 去雾 * 涂抹及风格化", "private": true, "directories": { diff --git a/python/video.py b/python/video.py index 32092e9..be6d274 100644 --- a/python/video.py +++ b/python/video.py @@ -105,26 +105,17 @@ def prepare(video, steps): width = optDecode['width'] if 'height' in optDecode: height = optDecode['height'] - scaleW = scaleH = 1 outWidth, outHeight = (width, height) for opt in filter((lambda opt: opt['op'] == 'SR' or opt['op'] == 'resize'), procSteps): if opt['op'] == 'SR': - scaleW *= opt['scale'] - scaleH *= opt['scale'] - else: - if 'scaleW' in opt: - scaleW = opt['scaleW'] - else: - scaleW = 1 - outWidth = opt['width'] - if 'scaleH' in opt: - scaleH = opt['scaleH'] - else: - scaleH = 1 - outHeight = opt['height'] + outWidth *= opt['scale'] + outHeight *= opt['scale'] + else: # resize + outWidth = round(outWidth * opt['scaleW']) if 'scaleW' in opt else opt['width'] + outHeight = round(outHeight * opt['scaleH']) if 'scaleH' in opt else opt['height'] if start < 0: start = 0 - if start and len(slomos): + if start and len(slomos): # should generate intermediate frames between start-1 and start start -= 1 for opt in slomos: opt['opt'].firstTime = 0 @@ -157,7 +148,7 @@ def prepare(video, steps): '-y', '-f', 'rawvideo', '-pix_fmt', pix_fmt, - '-s', '{}x{}'.format(outWidth * scaleW, outHeight * scaleH), + '-s', '{}x{}'.format(outWidth, outHeight), '-r', str(frameRate), '-i', '-', '-i', video, diff --git a/server.bat b/server.bat new file mode 100644 index 0000000..d90fd47 --- /dev/null +++ b/server.bat @@ -0,0 +1,6 @@ +@echo off +setlocal +%~d0 +cd %~dp0 +ipconfig +MoePhoto.exe -g \ No newline at end of file diff --git a/src/js/main.js b/src/js/main.js index e19f3ba..385823c 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -152,7 +152,8 @@ const panels = { values: [ { value: 'scale', binds: ['scaleW'] }, { value: 'pixel', binds: ['width'], checked: 1 } - ] + ], + notes: ['按比例缩放图像长宽的小数部分四舍五入为整数'] }, scaleW: { type: 'number', diff --git a/templates/1-header.html b/templates/1-header.html index 8d6bbd8..b1f3060 100644 --- a/templates/1-header.html +++ b/templates/1-header.html @@ -34,6 +34,9 @@

  • 相册
  • +
  • + 系统信息 +
  • diff --git a/templates/index.html b/templates/index.html index b39b711..396cd3e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -54,6 +54,9 @@

  • 相册
  • +
  • + 系统信息 +