Send svg image to MTer, park pen on start, no-plotter option, max task time dynamic, fix mt_task.xml click
This commit is contained in:
parent
a2c20b533c
commit
3111d2cc85
7 changed files with 69 additions and 17 deletions
|
@ -38,7 +38,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><label>Drawing link:</label></td>
|
<td><label>Drawing link:</label></td>
|
||||||
<td><a href="http://here.rubenvandeven.com:8888/draw?id={HIT_NR}">drawing page</a></td>
|
<td><a target="_blank" href="http://here.rubenvandeven.com:8888/draw?id={HIT_NR}">drawing page</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -16,6 +16,11 @@ if __name__ == '__main__':
|
||||||
type=str,
|
type=str,
|
||||||
help='The yaml config file to load'
|
help='The yaml config file to load'
|
||||||
)
|
)
|
||||||
|
argParser.add_argument(
|
||||||
|
'--no-plotter',
|
||||||
|
action='store_true',
|
||||||
|
help='Skip attempt to connect to plotter'
|
||||||
|
)
|
||||||
argParser.add_argument(
|
argParser.add_argument(
|
||||||
'--verbose',
|
'--verbose',
|
||||||
'-v',
|
'-v',
|
||||||
|
@ -49,5 +54,5 @@ if __name__ == '__main__':
|
||||||
logger.info("Start server")
|
logger.info("Start server")
|
||||||
|
|
||||||
command = CentralManagement(debug_mode=args.verbose > 0)
|
command = CentralManagement(debug_mode=args.verbose > 0)
|
||||||
command.loadConfig(args.config)
|
command.loadConfig(args.config, args)
|
||||||
command.start()
|
command.start()
|
||||||
|
|
|
@ -66,6 +66,9 @@ class HIT(Base):
|
||||||
|
|
||||||
def getSvgImageUrl(self):
|
def getSvgImageUrl(self):
|
||||||
return f"scans/{self.id:06d}.svg"
|
return f"scans/{self.id:06d}.svg"
|
||||||
|
|
||||||
|
def getSvgImagePath(self):
|
||||||
|
return os.path.join('www', self.getSvgImageUrl())
|
||||||
|
|
||||||
def getStatus(self):
|
def getStatus(self):
|
||||||
if self.scanned_at:
|
if self.scanned_at:
|
||||||
|
@ -155,6 +158,9 @@ class Store:
|
||||||
return int(2.5*60)
|
return int(2.5*60)
|
||||||
return int(sum(durations) / len(durations))
|
return int(sum(durations) / len(durations))
|
||||||
|
|
||||||
|
def getEstimatedHitDuration(self):
|
||||||
|
return self.getAvgDurationOfPreviousNHits(5)
|
||||||
|
|
||||||
def getHITs(self, n = 100):
|
def getHITs(self, n = 100):
|
||||||
return self.session.query(HIT).\
|
return self.session.query(HIT).\
|
||||||
filter(HIT.submit_hit_at != None).\
|
filter(HIT.submit_hit_at != None).\
|
||||||
|
|
|
@ -40,10 +40,13 @@ class CentralManagement():
|
||||||
self.scanLock = threading.Lock()
|
self.scanLock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def loadConfig(self, filename):
|
def loadConfig(self, filename, args):
|
||||||
with open(filename, 'r') as fp:
|
with open(filename, 'r') as fp:
|
||||||
self.logger.debug('Load config from {}'.format(filename))
|
self.logger.debug('Load config from {}'.format(filename))
|
||||||
self.config = yaml.safe_load(fp)
|
self.config = yaml.safe_load(fp)
|
||||||
|
|
||||||
|
if args.no_plotter:
|
||||||
|
self.config['dummy_plotter'] = True
|
||||||
|
|
||||||
varDb = os.path.join(
|
varDb = os.path.join(
|
||||||
# self.config['storage_dir'],
|
# self.config['storage_dir'],
|
||||||
|
@ -242,7 +245,7 @@ class CentralManagement():
|
||||||
self.logger.info(f"Make HIT {self.currentHit.id}")
|
self.logger.info(f"Make HIT {self.currentHit.id}")
|
||||||
|
|
||||||
question = open(self.config['amazon']['task_xml'], mode='r').read().replace("{HIT_NR}",str(self.currentHit.id))
|
question = open(self.config['amazon']['task_xml'], mode='r').read().replace("{HIT_NR}",str(self.currentHit.id))
|
||||||
estimatedHitDuration = self.store.getAvgDurationOfPreviousNHits(5)
|
estimatedHitDuration = self.store.getEstimatedHitDuration()
|
||||||
|
|
||||||
fee = (self.config['hour_rate_aim']/3600.) * estimatedHitDuration
|
fee = (self.config['hour_rate_aim']/3600.) * estimatedHitDuration
|
||||||
self.currentHit.fee = fee
|
self.currentHit.fee = fee
|
||||||
|
@ -254,7 +257,7 @@ class CentralManagement():
|
||||||
Reward = "{:.2f}".format(fee),
|
Reward = "{:.2f}".format(fee),
|
||||||
MaxAssignments = 1,
|
MaxAssignments = 1,
|
||||||
LifetimeInSeconds = self.config['hit_lifetime'],
|
LifetimeInSeconds = self.config['hit_lifetime'],
|
||||||
AssignmentDurationInSeconds = self.config['hit_assignment_duration'],
|
AssignmentDurationInSeconds = estimatedHitDuration * 2, # give people twice as long as we expect them to take
|
||||||
AutoApprovalDelayInSeconds = self.config['hit_autoapprove_delay'],
|
AutoApprovalDelayInSeconds = self.config['hit_autoapprove_delay'],
|
||||||
Question = question,
|
Question = question,
|
||||||
)
|
)
|
||||||
|
@ -317,6 +320,7 @@ class CentralManagement():
|
||||||
|
|
||||||
def reset(self) -> str:
|
def reset(self) -> str:
|
||||||
# TODO: for returns & abandons
|
# TODO: for returns & abandons
|
||||||
|
self.plotter.park()
|
||||||
scan = threading.Thread(target=self.cleanDrawing, name='reset')
|
scan = threading.Thread(target=self.cleanDrawing, name='reset')
|
||||||
scan.start()
|
scan.start()
|
||||||
|
|
||||||
|
|
|
@ -53,10 +53,9 @@ class Plotter:
|
||||||
self.ad.options.speed_pendown = 100
|
self.ad.options.speed_pendown = 100
|
||||||
self.ad.options.model = 1 # 2 for A3, 1 for A4
|
self.ad.options.model = 1 # 2 for A3, 1 for A4
|
||||||
|
|
||||||
self.ad.moveto(0,0)
|
|
||||||
|
|
||||||
# plotterWidth = 25
|
self.park()
|
||||||
# plotterHeight = 21
|
# self.ad.moveto(0,0)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.ad = None
|
self.ad = None
|
||||||
|
@ -66,11 +65,15 @@ class Plotter:
|
||||||
finally:
|
finally:
|
||||||
self.logger.warning("Close Axidraw connection")
|
self.logger.warning("Close Axidraw connection")
|
||||||
if self.ad:
|
if self.ad:
|
||||||
with self.scannerLock:
|
try:
|
||||||
self.ad.moveto(0,0)
|
with self.scannerLock:
|
||||||
self.ad.disconnect()
|
self.ad.moveto(0,0)
|
||||||
|
self.ad.disconnect()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning("Error on closing axidraw:")
|
||||||
|
self.logger.exception(e)
|
||||||
|
|
||||||
self.logger.critical("Clear running Event")
|
self.logger.info("Clear running Event")
|
||||||
# send shutdown signal (if not already set)
|
# send shutdown signal (if not already set)
|
||||||
self.isRunning.clear()
|
self.isRunning.clear()
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import httpagentparser
|
||||||
import geoip2.database
|
import geoip2.database
|
||||||
import queue
|
import queue
|
||||||
import datetime
|
import datetime
|
||||||
|
import html
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("sorteerhoed").getChild("webserver")
|
logger = logging.getLogger("sorteerhoed").getChild("webserver")
|
||||||
|
@ -62,6 +63,8 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
|
|
||||||
self.hit = self.store.currentHit
|
self.hit = self.store.currentHit
|
||||||
|
|
||||||
|
self.timeout = datetime.datetime.now() + datetime.timedelta(seconds=self.store.getEstimatedHitDuration() * 2)
|
||||||
|
|
||||||
if self.hit.submit_hit_at:
|
if self.hit.submit_hit_at:
|
||||||
raise Exception("Opening websocket for already submitted hit")
|
raise Exception("Opening websocket for already submitted hit")
|
||||||
|
|
||||||
|
@ -80,6 +83,12 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
# the client sent the message
|
# the client sent the message
|
||||||
def on_message(self, message):
|
def on_message(self, message):
|
||||||
logger.debug(f"recieve: {message}")
|
logger.debug(f"recieve: {message}")
|
||||||
|
|
||||||
|
if datetime.datetime.now() > self.timeout:
|
||||||
|
logger.critical("Close websocket after timeout (abandon?)")
|
||||||
|
self.close()
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg = json.loads(message)
|
msg = json.loads(message)
|
||||||
# TODO: sanitize input: min/max, limit strokes
|
# TODO: sanitize input: min/max, limit strokes
|
||||||
|
@ -100,6 +109,19 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
if not id:
|
if not id:
|
||||||
self.write_message(json.dumps('error'))
|
self.write_message(json.dumps('error'))
|
||||||
return
|
return
|
||||||
|
#store svg:
|
||||||
|
d = html.escape(msg['d'])
|
||||||
|
svg = f"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.0" viewBox="0 0 {self.config['scanner']['width']}0 {self.config['scanner']['height']}0" width="{self.config['scanner']['width']}mm" height="{self.config['scanner']['height']}mm" preserveAspectRatio="none">
|
||||||
|
<path d="{d}" style='stroke:gray;stroke-width:1mm;fill:none;' id="stroke" />
|
||||||
|
</svg>
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(self.store.currentHit.getSvgImagePath(), 'w') as fp:
|
||||||
|
fp.write(svg)
|
||||||
|
|
||||||
self.write_message(json.dumps({
|
self.write_message(json.dumps({
|
||||||
'action': 'submitted',
|
'action': 'submitted',
|
||||||
|
|
|
@ -13,6 +13,17 @@
|
||||||
width:100%;
|
width:100%;
|
||||||
height:100%;
|
height:100%;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
z-index:2;
|
||||||
|
}
|
||||||
|
img{
|
||||||
|
position:absolute;
|
||||||
|
top:0;
|
||||||
|
bottom:0;
|
||||||
|
right:0;
|
||||||
|
left:0;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
z-index:1;
|
||||||
}
|
}
|
||||||
|
|
||||||
path {
|
path {
|
||||||
|
@ -81,11 +92,10 @@
|
||||||
background:gray;
|
background:gray;
|
||||||
}
|
}
|
||||||
#interface{
|
#interface{
|
||||||
background:white;
|
background:black;
|
||||||
height: 0;
|
height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-top: calc({HEIGHT}/{WIDTH} * 100%);
|
padding-top: calc({HEIGHT}/{WIDTH} * 100%);
|
||||||
background: white;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
|
@ -117,17 +127,18 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id='interface' style="background-image:url('{IMAGE_URL}')">
|
<div id='interface'">
|
||||||
<div id='innerface'>
|
<div id='innerface'>
|
||||||
<div id='wrapper'>
|
<div id='wrapper'>
|
||||||
<!-- <img src="{IMAGE_URL}" id='sample'>-->
|
<!-- <img src="{IMAGE_URL}" id='sample'>-->
|
||||||
<svg id="canvas" viewBox="0 0 {WIDTH}0 {HEIGHT}0" width="{WIDTH}mm" height="{HEIGHT}mm" preserveAspectRatio="none">
|
<svg id="canvas" viewBox="0 0 {WIDTH}0 {HEIGHT}0" width="{WIDTH}mm" height="{HEIGHT}mm" preserveAspectRatio="none">
|
||||||
<path d="" id="stroke" />
|
<path d="" id="stroke" />
|
||||||
</svg>
|
</svg>
|
||||||
|
<img src='{IMAGE_URL}'>
|
||||||
</div>
|
</div>
|
||||||
<div id='info'>
|
<div id='info'>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Drag the mouse to trace the <strong>clearest lines</strong> drawing above</li>
|
<li>Drag the mouse to trace the lines drawing above</li>
|
||||||
<li>Follow the lines as precise as possible</li>
|
<li>Follow the lines as precise as possible</li>
|
||||||
<li>Press submit when you're done.</li>
|
<li>Press submit when you're done.</li>
|
||||||
<li>You'll receive a submission token, to fill in at Mechanical Turk</li>
|
<li>You'll receive a submission token, to fill in at Mechanical Turk</li>
|
||||||
|
@ -304,7 +315,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.send(JSON.stringify({
|
socket.send(JSON.stringify({
|
||||||
'action': 'submit'
|
'action': 'submit',
|
||||||
|
'd': strokeEl.getAttribute('d')
|
||||||
}));
|
}));
|
||||||
|
|
||||||
document.body.removeEventListener('mousemove', draw);
|
document.body.removeEventListener('mousemove', draw);
|
||||||
|
|
Loading…
Reference in a new issue