From 93bdac797ed3cf04a427e276823f9896dcf19a2d Mon Sep 17 00:00:00 2001 From: Patrick Simianer Date: Tue, 25 Jul 2017 02:17:07 +0200 Subject: lfpe 2017 (neural edition) --- external/apply_bpe.py | 1 + external/de-bpe | 8 + external/de-bpe.rb | 4 + inc/help.inc.php | 6 +- index.php | 4 +- interface.php | 28 +- js/interface.js | 143 +++- js/nouislider.js | 2146 +++++++++++++++++++++++++++++++++++++++++++++++ server-neural.rb | 434 ++++++++++ server.rb | 11 + static/nouislider.css | 257 ++++++ util/kill_all | 2 +- util/nanomsg_wrapper.rb | 21 +- util/run_all | 2 +- util/run_session | 4 +- views/summary.haml | 97 ++- 16 files changed, 3085 insertions(+), 83 deletions(-) create mode 120000 external/apply_bpe.py create mode 100755 external/de-bpe create mode 100755 external/de-bpe.rb create mode 100644 js/nouislider.js create mode 100755 server-neural.rb create mode 100644 static/nouislider.css diff --git a/external/apply_bpe.py b/external/apply_bpe.py new file mode 120000 index 0000000..5b15a8b --- /dev/null +++ b/external/apply_bpe.py @@ -0,0 +1 @@ +../../subword-nmt/apply_bpe.py \ No newline at end of file diff --git a/external/de-bpe b/external/de-bpe new file mode 100755 index 0000000..fde1ef1 --- /dev/null +++ b/external/de-bpe @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby + +STDOUT.sync = true + +while line = STDIN.gets + puts line.gsub(/@@ /, "") +end + diff --git a/external/de-bpe.rb b/external/de-bpe.rb new file mode 100755 index 0000000..65e1506 --- /dev/null +++ b/external/de-bpe.rb @@ -0,0 +1,4 @@ +#!/bin/zsh + +sed "s|@@ ||g" + diff --git a/inc/help.inc.php b/inc/help.inc.php index 121c087..149c0ea 100644 --- a/inc/help.inc.php +++ b/inc/help.inc.php @@ -6,7 +6,7 @@

Please use only a single browser window per session at the same time. Going back to earlier examples is not possible, please take great care when interacting with the system.

-

Instructions for the graphical interface:

+ -

While editing text, you can press CTRL+<left/right arrows> to move the curose word-wise.

+

While editing text, you can press CTRL+<left/right arrows> to move the cursor word-wise.

The interface was tested with Firefox 31, 38, 43 and 45.

diff --git a/index.php b/index.php index 5b6f33f..b2a810f 100644 --- a/index.php +++ b/index.php @@ -28,8 +28,8 @@     Session type:     diff --git a/interface.php b/interface.php index 9c5531c..c1cb221 100644 --- a/interface.php +++ b/interface.php @@ -3,12 +3,14 @@ Post-Editing Interface (Session: #<?php echo $_GET["key"]; ?>) + + @@ -52,10 +54,6 @@ Note that the source word may be distorted. - - - -
@@ -66,14 +64,31 @@ Note that the source word may be distorted.
+ + + + + + + +

Support: Mail

-

Session: | - Debug +

Session: #

@@ -114,5 +129,6 @@ Note that the source word may be distorted. + diff --git a/js/interface.js b/js/interface.js index 6c32063..53f19e2 100644 --- a/js/interface.js +++ b/js/interface.js @@ -10,6 +10,10 @@ var TEXT_count_click=0, var rules_orig = {}; +var NEXT_MODE = "get"; // or "rate" +var SLIDER = null; +var DONE = false; + /* * cross-site request * @@ -57,6 +61,40 @@ var Timer = { } } +var TimerRating = { + start_t: 0, + pause_start_t: 0, + pause_acc_t: 0, + paused: false, + value: -1, + + start: function () { + this.start_t = Date.now(); + this.pause_start_t = 0; + this.pause_acc_t = 0; + this.paused = false; + }, + pause: function () { + this.paused = true; + this.pause_start_t = Date.now(); + }, + unpause: function () { + this.paused = false; + this.pause_acc_t += Date.now()-this.pause_start_t; + this.pause_start_t = 0; + }, + stop: function() { + this.value = (Date.now()-this.start_t)-this.pause_acc_t; + }, + get: function () { + if (this.value < 0) { + return (Date.now()-this.start_t)-this.pause_acc_t; + } else { + return this.value; + } + } +} + /* * pause/unpause timer * @@ -76,6 +114,7 @@ var pause = function () next_button.setAttribute("disabled", "disabled"); reset_button.setAttribute("disabled", "disabled"); Timer.pause(); + TimerRating.pause(); if (ui_type=='g') { $("#derivation_editor").fadeTo(200,0.1); DE_ui_lock=true; @@ -85,8 +124,10 @@ var pause = function () button.innerHTML = "Pause"; paused.value = 0; next_button.removeAttribute("disabled"); - reset_button.removeAttribute("disabled", "disabled"); + if (NEXT_MODE != "rate") + reset_button.removeAttribute("disabled", "disabled"); Timer.unpause(); + TimerRating.unpause(); if (ui_type=='g') { $("#derivation_editor").fadeTo(200,1); DE_ui_lock=false; @@ -196,17 +237,17 @@ function not_working(fadein=true) * polling the server * */ -var poll = function (url_prefix) +var poll = function (url_prefix,rate=false,callback=function(){alert("Z");return false}) { setTimeout(function(){ $.get(url_prefix+"/status").done(function(response){ $("#status_detail").text(response); if (response == "Ready") { ready = true; - request_and_process_next(); + request_and_process_next(rate,callback); return; } else { - poll(url_prefix); + poll(url_prefix,rate,callback); } }); }, 1000); @@ -236,7 +277,38 @@ var safe_str = function (s) * next button * */ -var next = function () +var next = function() +{ + if (NEXT_MODE=="get") { + nextX(true,next_callback); + NEXT_MODE="rate"; + } else if (NEXT_MODE=="rate" && !DONE) { + TimerRating.stop(); + Timer.start(); + $("#slider_wrapper").slideUp(); + $("#target_textarea").prop("disabled",false); + //$("#pause_button").prop("disabled",false); + $("#reset_button").prop("disabled",false); + $("#next").html("Next"); + NEXT_MODE="get"; + } + + return false; +} + +var next_callback = function (done=false) +{ + if (done) return false; + NEXT_MODE = "rate"; + $("#slider_wrapper").slideDown(); + $("#original_mt_cmp").html($("#original_mt").val()); + SLIDER.set(50); + TimerRating.start(); + + return false; +} + +var nextX = function (rate=false,callback=function(){alert("X");return false}) { // elements var button = document.getElementById("next"); @@ -299,11 +371,13 @@ var next = function () send_data["key"] = key; send_data["name"] = safe_str($.trim($("#name").val())); + send_data["rating"] = $("#rating").val(); // send data if (oov_correct.value=="false" && post_edit != "") { send_data["EDIT"] = true; send_data["duration"] = Timer.get(); + send_data["duration_rating"] = TimerRating.get(); send_data["source_value"] = safe_str(source.value); // compose request // no change? @@ -332,7 +406,7 @@ var next = function () for (var i=0; i 0) { $("#seg_"+(id-1)).removeClass("bold"); @@ -525,11 +608,13 @@ var request_and_process_next = function () } // start timer - Timer.start(); + //Timer.start(); + + return callback(false); } }; - return; + return false; } /* @@ -540,6 +625,8 @@ var init_text_editor = function () { document.getElementById("target_textarea").value = ""; document.getElementById("target_textarea").setAttribute("disabled", "disabled"); + $("#pause_button").prop("disabled", true); + $("#reset_button").prop("disabled", true); TEXT_count_click = 0; TEXT_count_kbd = 0; @@ -599,8 +686,30 @@ $().ready(function() document.getElementById("textboxes").style.display = "block"; } + initSlider(); }); +var initSlider = function () +{ + var slider = document.getElementById("slider"); + SLIDER = noUiSlider.create(slider, { + start: 50, + range: { + min: [0,1], + max: [100] + }, + pips: { + mode: 'values', + values: [], + density: 25 + } + }); + SLIDER.on("update", function (values, handle) + { + $("#rating").val(values[0]); + }); +} + var explore = function (o,src,tgt,s2t,t2s,done) { if (done[o["id"]]) return; diff --git a/js/nouislider.js b/js/nouislider.js new file mode 100644 index 0000000..2e394aa --- /dev/null +++ b/js/nouislider.js @@ -0,0 +1,2146 @@ +/*! nouislider - 9.2.0 - 2017-01-11 10:35:34 */ + +(function (factory) { + + if ( typeof define === 'function' && define.amd ) { + + // AMD. Register as an anonymous module. + define([], factory); + + } else if ( typeof exports === 'object' ) { + + // Node/CommonJS + module.exports = factory(); + + } else { + + // Browser globals + window.noUiSlider = factory(); + } + +}(function( ){ + + 'use strict'; + + var VERSION = '9.2.0'; + + + // Creates a node, adds it to target, returns the new node. + function addNodeTo ( target, className ) { + var div = document.createElement('div'); + addClass(div, className); + target.appendChild(div); + return div; + } + + // Removes duplicates from an array. + function unique ( array ) { + return array.filter(function(a){ + return !this[a] ? this[a] = true : false; + }, {}); + } + + // Round a value to the closest 'to'. + function closest ( value, to ) { + return Math.round(value / to) * to; + } + + // Current position of an element relative to the document. + function offset ( elem, orientation ) { + + var rect = elem.getBoundingClientRect(), + doc = elem.ownerDocument, + docElem = doc.documentElement, + pageOffset = getPageOffset(); + + // getBoundingClientRect contains left scroll in Chrome on Android. + // I haven't found a feature detection that proves this. Worst case + // scenario on mis-match: the 'tap' feature on horizontal sliders breaks. + if ( /webkit.*Chrome.*Mobile/i.test(navigator.userAgent) ) { + pageOffset.x = 0; + } + + return orientation ? (rect.top + pageOffset.y - docElem.clientTop) : (rect.left + pageOffset.x - docElem.clientLeft); + } + + // Checks whether a value is numerical. + function isNumeric ( a ) { + return typeof a === 'number' && !isNaN( a ) && isFinite( a ); + } + + // Sets a class and removes it after [duration] ms. + function addClassFor ( element, className, duration ) { + if (duration > 0) { + addClass(element, className); + setTimeout(function(){ + removeClass(element, className); + }, duration); + } + } + + // Limits a value to 0 - 100 + function limit ( a ) { + return Math.max(Math.min(a, 100), 0); + } + + // Wraps a variable as an array, if it isn't one yet. + // Note that an input array is returned by reference! + function asArray ( a ) { + return Array.isArray(a) ? a : [a]; + } + + // Counts decimals + function countDecimals ( numStr ) { + numStr = String(numStr); + var pieces = numStr.split("."); + return pieces.length > 1 ? pieces[1].length : 0; + } + + // http://youmightnotneedjquery.com/#add_class + function addClass ( el, className ) { + if ( el.classList ) { + el.classList.add(className); + } else { + el.className += ' ' + className; + } + } + + // http://youmightnotneedjquery.com/#remove_class + function removeClass ( el, className ) { + if ( el.classList ) { + el.classList.remove(className); + } else { + el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); + } + } + + // https://plainjs.com/javascript/attributes/adding-removing-and-testing-for-classes-9/ + function hasClass ( el, className ) { + return el.classList ? el.classList.contains(className) : new RegExp('\\b' + className + '\\b').test(el.className); + } + + // https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY#Notes + function getPageOffset ( ) { + + var supportPageOffset = window.pageXOffset !== undefined, + isCSS1Compat = ((document.compatMode || "") === "CSS1Compat"), + x = supportPageOffset ? window.pageXOffset : isCSS1Compat ? document.documentElement.scrollLeft : document.body.scrollLeft, + y = supportPageOffset ? window.pageYOffset : isCSS1Compat ? document.documentElement.scrollTop : document.body.scrollTop; + + return { + x: x, + y: y + }; + } + + // we provide a function to compute constants instead + // of accessing window.* as soon as the module needs it + // so that we do not compute anything if not needed + function getActions ( ) { + + // Determine the events to bind. IE11 implements pointerEvents without + // a prefix, which breaks compatibility with the IE10 implementation. + return window.navigator.pointerEnabled ? { + start: 'pointerdown', + move: 'pointermove', + end: 'pointerup' + } : window.navigator.msPointerEnabled ? { + start: 'MSPointerDown', + move: 'MSPointerMove', + end: 'MSPointerUp' + } : { + start: 'mousedown touchstart', + move: 'mousemove touchmove', + end: 'mouseup touchend' + }; + } + + +// Value calculation + + // Determine the size of a sub-range in relation to a full range. + function subRangeRatio ( pa, pb ) { + return (100 / (pb - pa)); + } + + // (percentage) How many percent is this value of this range? + function fromPercentage ( range, value ) { + return (value * 100) / ( range[1] - range[0] ); + } + + // (percentage) Where is this value on this range? + function toPercentage ( range, value ) { + return fromPercentage( range, range[0] < 0 ? + value + Math.abs(range[0]) : + value - range[0] ); + } + + // (value) How much is this percentage on this range? + function isPercentage ( range, value ) { + return ((value * ( range[1] - range[0] )) / 100) + range[0]; + } + + +// Range conversion + + function getJ ( value, arr ) { + + var j = 1; + + while ( value >= arr[j] ){ + j += 1; + } + + return j; + } + + // (percentage) Input a value, find where, on a scale of 0-100, it applies. + function toStepping ( xVal, xPct, value ) { + + if ( value >= xVal.slice(-1)[0] ){ + return 100; + } + + var j = getJ( value, xVal ), va, vb, pa, pb; + + va = xVal[j-1]; + vb = xVal[j]; + pa = xPct[j-1]; + pb = xPct[j]; + + return pa + (toPercentage([va, vb], value) / subRangeRatio (pa, pb)); + } + + // (value) Input a percentage, find where it is on the specified range. + function fromStepping ( xVal, xPct, value ) { + + // There is no range group that fits 100 + if ( value >= 100 ){ + return xVal.slice(-1)[0]; + } + + var j = getJ( value, xPct ), va, vb, pa, pb; + + va = xVal[j-1]; + vb = xVal[j]; + pa = xPct[j-1]; + pb = xPct[j]; + + return isPercentage([va, vb], (value - pa) * subRangeRatio (pa, pb)); + } + + // (percentage) Get the step that applies at a certain value. + function getStep ( xPct, xSteps, snap, value ) { + + if ( value === 100 ) { + return value; + } + + var j = getJ( value, xPct ), a, b; + + // If 'snap' is set, steps are used as fixed points on the slider. + if ( snap ) { + + a = xPct[j-1]; + b = xPct[j]; + + // Find the closest position, a or b. + if ((value - a) > ((b-a)/2)){ + return b; + } + + return a; + } + + if ( !xSteps[j-1] ){ + return value; + } + + return xPct[j-1] + closest( + value - xPct[j-1], + xSteps[j-1] + ); + } + + +// Entry parsing + + function handleEntryPoint ( index, value, that ) { + + var percentage; + + // Wrap numerical input in an array. + if ( typeof value === "number" ) { + value = [value]; + } + + // Reject any invalid input, by testing whether value is an array. + if ( Object.prototype.toString.call( value ) !== '[object Array]' ){ + throw new Error("noUiSlider (" + VERSION + "): 'range' contains invalid value."); + } + + // Covert min/max syntax to 0 and 100. + if ( index === 'min' ) { + percentage = 0; + } else if ( index === 'max' ) { + percentage = 100; + } else { + percentage = parseFloat( index ); + } + + // Check for correct input. + if ( !isNumeric( percentage ) || !isNumeric( value[0] ) ) { + throw new Error("noUiSlider (" + VERSION + "): 'range' value isn't numeric."); + } + + // Store values. + that.xPct.push( percentage ); + that.xVal.push( value[0] ); + + // NaN will evaluate to false too, but to keep + // logging clear, set step explicitly. Make sure + // not to override the 'step' setting with false. + if ( !percentage ) { + if ( !isNaN( value[1] ) ) { + that.xSteps[0] = value[1]; + } + } else { + that.xSteps.push( isNaN(value[1]) ? false : value[1] ); + } + + that.xHighestCompleteStep.push(0); + } + + function handleStepPoint ( i, n, that ) { + + // Ignore 'false' stepping. + if ( !n ) { + return true; + } + + // Factor to range ratio + that.xSteps[i] = fromPercentage([ + that.xVal[i] + ,that.xVal[i+1] + ], n) / subRangeRatio ( + that.xPct[i], + that.xPct[i+1] ); + + var totalSteps = (that.xVal[i+1] - that.xVal[i]) / that.xNumSteps[i]; + var highestStep = Math.ceil(Number(totalSteps.toFixed(3)) - 1); + var step = that.xVal[i] + (that.xNumSteps[i] * highestStep); + + that.xHighestCompleteStep[i] = step; + } + + +// Interface + + // The interface to Spectrum handles all direction-based + // conversions, so the above values are unaware. + + function Spectrum ( entry, snap, direction, singleStep ) { + + this.xPct = []; + this.xVal = []; + this.xSteps = [ singleStep || false ]; + this.xNumSteps = [ false ]; + this.xHighestCompleteStep = []; + + this.snap = snap; + this.direction = direction; + + var index, ordered = [ /* [0, 'min'], [1, '50%'], [2, 'max'] */ ]; + + // Map the object keys to an array. + for ( index in entry ) { + if ( entry.hasOwnProperty(index) ) { + ordered.push([entry[index], index]); + } + } + + // Sort all entries by value (numeric sort). + if ( ordered.length && typeof ordered[0][0] === "object" ) { + ordered.sort(function(a, b) { return a[0][0] - b[0][0]; }); + } else { + ordered.sort(function(a, b) { return a[0] - b[0]; }); + } + + + // Convert all entries to subranges. + for ( index = 0; index < ordered.length; index++ ) { + handleEntryPoint(ordered[index][1], ordered[index][0], this); + } + + // Store the actual step values. + // xSteps is sorted in the same order as xPct and xVal. + this.xNumSteps = this.xSteps.slice(0); + + // Convert all numeric steps to the percentage of the subrange they represent. + for ( index = 0; index < this.xNumSteps.length; index++ ) { + handleStepPoint(index, this.xNumSteps[index], this); + } + } + + Spectrum.prototype.getMargin = function ( value ) { + + var step = this.xNumSteps[0]; + + if ( step && ((value / step) % 1) !== 0 ) { + throw new Error("noUiSlider (" + VERSION + "): 'limit', 'margin' and 'padding' must be divisible by step."); + } + + return this.xPct.length === 2 ? fromPercentage(this.xVal, value) : false; + }; + + Spectrum.prototype.toStepping = function ( value ) { + + value = toStepping( this.xVal, this.xPct, value ); + + return value; + }; + + Spectrum.prototype.fromStepping = function ( value ) { + + return fromStepping( this.xVal, this.xPct, value ); + }; + + Spectrum.prototype.getStep = function ( value ) { + + value = getStep(this.xPct, this.xSteps, this.snap, value ); + + return value; + }; + + Spectrum.prototype.getNearbySteps = function ( value ) { + + var j = getJ(value, this.xPct); + + return { + stepBefore: { startValue: this.xVal[j-2], step: this.xNumSteps[j-2], highestStep: this.xHighestCompleteStep[j-2] }, + thisStep: { startValue: this.xVal[j-1], step: this.xNumSteps[j-1], highestStep: this.xHighestCompleteStep[j-1] }, + stepAfter: { startValue: this.xVal[j-0], step: this.xNumSteps[j-0], highestStep: this.xHighestCompleteStep[j-0] } + }; + }; + + Spectrum.prototype.countStepDecimals = function () { + var stepDecimals = this.xNumSteps.map(countDecimals); + return Math.max.apply(null, stepDecimals); + }; + + // Outside testing + Spectrum.prototype.convert = function ( value ) { + return this.getStep(this.toStepping(value)); + }; + +/* Every input option is tested and parsed. This'll prevent + endless validation in internal methods. These tests are + structured with an item for every option available. An + option can be marked as required by setting the 'r' flag. + The testing function is provided with three arguments: + - The provided value for the option; + - A reference to the options object; + - The name for the option; + + The testing function returns false when an error is detected, + or true when everything is OK. It can also modify the option + object, to make sure all values can be correctly looped elsewhere. */ + + var defaultFormatter = { 'to': function( value ){ + return value !== undefined && value.toFixed(2); + }, 'from': Number }; + + function testStep ( parsed, entry ) { + + if ( !isNumeric( entry ) ) { + throw new Error("noUiSlider (" + VERSION + "): 'step' is not numeric."); + } + + // The step option can still be used to set stepping + // for linear sliders. Overwritten if set in 'range'. + parsed.singleStep = entry; + } + + function testRange ( parsed, entry ) { + + // Filter incorrect input. + if ( typeof entry !== 'object' || Array.isArray(entry) ) { + throw new Error("noUiSlider (" + VERSION + "): 'range' is not an object."); + } + + // Catch missing start or end. + if ( entry.min === undefined || entry.max === undefined ) { + throw new Error("noUiSlider (" + VERSION + "): Missing 'min' or 'max' in 'range'."); + } + + // Catch equal start or end. + if ( entry.min === entry.max ) { + throw new Error("noUiSlider (" + VERSION + "): 'range' 'min' and 'max' cannot be equal."); + } + + parsed.spectrum = new Spectrum(entry, parsed.snap, parsed.dir, parsed.singleStep); + } + + function testStart ( parsed, entry ) { + + entry = asArray(entry); + + // Validate input. Values aren't tested, as the public .val method + // will always provide a valid location. + if ( !Array.isArray( entry ) || !entry.length ) { + throw new Error("noUiSlider (" + VERSION + "): 'start' option is incorrect."); + } + + // Store the number of handles. + parsed.handles = entry.length; + + // When the slider is initialized, the .val method will + // be called with the start options. + parsed.start = entry; + } + + function testSnap ( parsed, entry ) { + + // Enforce 100% stepping within subranges. + parsed.snap = entry; + + if ( typeof entry !== 'boolean' ){ + throw new Error("noUiSlider (" + VERSION + "): 'snap' option must be a boolean."); + } + } + + function testAnimate ( parsed, entry ) { + + // Enforce 100% stepping within subranges. + parsed.animate = entry; + + if ( typeof entry !== 'boolean' ){ + throw new Error("noUiSlider (" + VERSION + "): 'animate' option must be a boolean."); + } + } + + function testAnimationDuration ( parsed, entry ) { + + parsed.animationDuration = entry; + + if ( typeof entry !== 'number' ){ + throw new Error("noUiSlider (" + VERSION + "): 'animationDuration' option must be a number."); + } + } + + function testConnect ( parsed, entry ) { + + var connect = [false]; + var i; + + // Map legacy options + if ( entry === 'lower' ) { + entry = [true, false]; + } + + else if ( entry === 'upper' ) { + entry = [false, true]; + } + + // Handle boolean options + if ( entry === true || entry === false ) { + + for ( i = 1; i < parsed.handles; i++ ) { + connect.push(entry); + } + + connect.push(false); + } + + // Reject invalid input + else if ( !Array.isArray( entry ) || !entry.length || entry.length !== parsed.handles + 1 ) { + throw new Error("noUiSlider (" + VERSION + "): 'connect' option doesn't match handle count."); + } + + else { + connect = entry; + } + + parsed.connect = connect; + } + + function testOrientation ( parsed, entry ) { + + // Set orientation to an a numerical value for easy + // array selection. + switch ( entry ){ + case 'horizontal': + parsed.ort = 0; + break; + case 'vertical': + parsed.ort = 1; + break; + default: + throw new Error("noUiSlider (" + VERSION + "): 'orientation' option is invalid."); + } + } + + function testMargin ( parsed, entry ) { + + if ( !isNumeric(entry) ){ + throw new Error("noUiSlider (" + VERSION + "): 'margin' option must be numeric."); + } + + // Issue #582 + if ( entry === 0 ) { + return; + } + + parsed.margin = parsed.spectrum.getMargin(entry); + + if ( !parsed.margin ) { + throw new Error("noUiSlider (" + VERSION + "): 'margin' option is only supported on linear sliders."); + } + } + + function testLimit ( parsed, entry ) { + + if ( !isNumeric(entry) ){ + throw new Error("noUiSlider (" + VERSION + "): 'limit' option must be numeric."); + } + + parsed.limit = parsed.spectrum.getMargin(entry); + + if ( !parsed.limit || parsed.handles < 2 ) { + throw new Error("noUiSlider (" + VERSION + "): 'limit' option is only supported on linear sliders with 2 or more handles."); + } + } + + function testPadding ( parsed, entry ) { + + if ( !isNumeric(entry) ){ + throw new Error("noUiSlider (" + VERSION + "): 'padding' option must be numeric."); + } + + if ( entry === 0 ) { + return; + } + + parsed.padding = parsed.spectrum.getMargin(entry); + + if ( !parsed.padding ) { + throw new Error("noUiSlider (" + VERSION + "): 'padding' option is only supported on linear sliders."); + } + + if ( parsed.padding < 0 ) { + throw new Error("noUiSlider (" + VERSION + "): 'padding' option must be a positive number."); + } + + if ( parsed.padding >= 50 ) { + throw new Error("noUiSlider (" + VERSION + "): 'padding' option must be less than half the range."); + } + } + + function testDirection ( parsed, entry ) { + + // Set direction as a numerical value for easy parsing. + // Invert connection for RTL sliders, so that the proper + // handles get the connect/background classes. + switch ( entry ) { + case 'ltr': + parsed.dir = 0; + break; + case 'rtl': + parsed.dir = 1; + break; + default: + throw new Error("noUiSlider (" + VERSION + "): 'direction' option was not recognized."); + } + } + + function testBehaviour ( parsed, entry ) { + + // Make sure the input is a string. + if ( typeof entry !== 'string' ) { + throw new Error("noUiSlider (" + VERSION + "): 'behaviour' must be a string containing options."); + } + + // Check if the string contains any keywords. + // None are required. + var tap = entry.indexOf('tap') >= 0; + var drag = entry.indexOf('drag') >= 0; + var fixed = entry.indexOf('fixed') >= 0; + var snap = entry.indexOf('snap') >= 0; + var hover = entry.indexOf('hover') >= 0; + + if ( fixed ) { + + if ( parsed.handles !== 2 ) { + throw new Error("noUiSlider (" + VERSION + "): 'fixed' behaviour must be used with 2 handles"); + } + + // Use margin to enforce fixed state + testMargin(parsed, parsed.start[1] - parsed.start[0]); + } + + parsed.events = { + tap: tap || snap, + drag: drag, + fixed: fixed, + snap: snap, + hover: hover + }; + } + + function testTooltips ( parsed, entry ) { + + if ( entry === false ) { + return; + } + + else if ( entry === true ) { + + parsed.tooltips = []; + + for ( var i = 0; i < parsed.handles; i++ ) { + parsed.tooltips.push(true); + } + } + + else { + + parsed.tooltips = asArray(entry); + + if ( parsed.tooltips.length !== parsed.handles ) { + throw new Error("noUiSlider (" + VERSION + "): must pass a formatter for all handles."); + } + + parsed.tooltips.forEach(function(formatter){ + if ( typeof formatter !== 'boolean' && (typeof formatter !== 'object' || typeof formatter.to !== 'function') ) { + throw new Error("noUiSlider (" + VERSION + "): 'tooltips' must be passed a formatter or 'false'."); + } + }); + } + } + + function testFormat ( parsed, entry ) { + + parsed.format = entry; + + // Any object with a to and from method is supported. + if ( typeof entry.to === 'function' && typeof entry.from === 'function' ) { + return true; + } + + throw new Error("noUiSlider (" + VERSION + "): 'format' requires 'to' and 'from' methods."); + } + + function testCssPrefix ( parsed, entry ) { + + if ( entry !== undefined && typeof entry !== 'string' && entry !== false ) { + throw new Error("noUiSlider (" + VERSION + "): 'cssPrefix' must be a string or `false`."); + } + + parsed.cssPrefix = entry; + } + + function testCssClasses ( parsed, entry ) { + + if ( entry !== undefined && typeof entry !== 'object' ) { + throw new Error("noUiSlider (" + VERSION + "): 'cssClasses' must be an object."); + } + + if ( typeof parsed.cssPrefix === 'string' ) { + parsed.cssClasses = {}; + + for ( var key in entry ) { + if ( !entry.hasOwnProperty(key) ) { continue; } + + parsed.cssClasses[key] = parsed.cssPrefix + entry[key]; + } + } else { + parsed.cssClasses = entry; + } + } + + function testUseRaf ( parsed, entry ) { + if ( entry === true || entry === false ) { + parsed.useRequestAnimationFrame = entry; + } else { + throw new Error("noUiSlider (" + VERSION + "): 'useRequestAnimationFrame' option should be true (default) or false."); + } + } + + // Test all developer settings and parse to assumption-safe values. + function testOptions ( options ) { + + // To prove a fix for #537, freeze options here. + // If the object is modified, an error will be thrown. + // Object.freeze(options); + + var parsed = { + margin: 0, + limit: 0, + padding: 0, + animate: true, + animationDuration: 300, + format: defaultFormatter + }; + + // Tests are executed in the order they are presented here. + var tests = { + 'step': { r: false, t: testStep }, + 'start': { r: true, t: testStart }, + 'connect': { r: true, t: testConnect }, + 'direction': { r: true, t: testDirection }, + 'snap': { r: false, t: testSnap }, + 'animate': { r: false, t: testAnimate }, + 'animationDuration': { r: false, t: testAnimationDuration }, + 'range': { r: true, t: testRange }, + 'orientation': { r: false, t: testOrientation }, + 'margin': { r: false, t: testMargin }, + 'limit': { r: false, t: testLimit }, + 'padding': { r: false, t: testPadding }, + 'behaviour': { r: true, t: testBehaviour }, + 'format': { r: false, t: testFormat }, + 'tooltips': { r: false, t: testTooltips }, + 'cssPrefix': { r: false, t: testCssPrefix }, + 'cssClasses': { r: false, t: testCssClasses }, + 'useRequestAnimationFrame': { r: false, t: testUseRaf } + }; + + var defaults = { + 'connect': false, + 'direction': 'ltr', + 'behaviour': 'tap', + 'orientation': 'horizontal', + 'cssPrefix' : 'noUi-', + 'cssClasses': { + target: 'target', + base: 'base', + origin: 'origin', + handle: 'handle', + handleLower: 'handle-lower', + handleUpper: 'handle-upper', + horizontal: 'horizontal', + vertical: 'vertical', + background: 'background', + connect: 'connect', + ltr: 'ltr', + rtl: 'rtl', + draggable: 'draggable', + drag: 'state-drag', + tap: 'state-tap', + active: 'active', + tooltip: 'tooltip', + pips: 'pips', + pipsHorizontal: 'pips-horizontal', + pipsVertical: 'pips-vertical', + marker: 'marker', + markerHorizontal: 'marker-horizontal', + markerVertical: 'marker-vertical', + markerNormal: 'marker-normal', + markerLarge: 'marker-large', + markerSub: 'marker-sub', + value: 'value', + valueHorizontal: 'value-horizontal', + valueVertical: 'value-vertical', + valueNormal: 'value-normal', + valueLarge: 'value-large', + valueSub: 'value-sub' + }, + 'useRequestAnimationFrame': true + }; + + // Run all options through a testing mechanism to ensure correct + // input. It should be noted that options might get modified to + // be handled properly. E.g. wrapping integers in arrays. + Object.keys(tests).forEach(function( name ){ + + // If the option isn't set, but it is required, throw an error. + if ( options[name] === undefined && defaults[name] === undefined ) { + + if ( tests[name].r ) { + throw new Error("noUiSlider (" + VERSION + "): '" + name + "' is required."); + } + + return true; + } + + tests[name].t( parsed, options[name] === undefined ? defaults[name] : options[name] ); + }); + + // Forward pips options + parsed.pips = options.pips; + + var styles = [['left', 'top'], ['right', 'bottom']]; + + // Pre-define the styles. + parsed.style = styles[parsed.dir][parsed.ort]; + parsed.styleOposite = styles[parsed.dir?0:1][parsed.ort]; + + return parsed; + } + + +function closure ( target, options, originalOptions ){ + + var actions = getActions( ); + + // All variables local to 'closure' are prefixed with 'scope_' + var scope_Target = target; + var scope_Locations = []; + var scope_Base; + var scope_Handles; + var scope_HandleNumbers = []; + var scope_ActiveHandle = false; + var scope_Connects; + var scope_Spectrum = options.spectrum; + var scope_Values = []; + var scope_Events = {}; + var scope_Self; + + + // Append a origin to the base + function addOrigin ( base, handleNumber ) { + + var origin = addNodeTo(base, options.cssClasses.origin); + var handle = addNodeTo(origin, options.cssClasses.handle); + + handle.setAttribute('data-handle', handleNumber); + + if ( handleNumber === 0 ) { + addClass(handle, options.cssClasses.handleLower); + } + + else if ( handleNumber === options.handles - 1 ) { + addClass(handle, options.cssClasses.handleUpper); + } + + return origin; + } + + // Insert nodes for connect elements + function addConnect ( base, add ) { + + if ( !add ) { + return false; + } + + return addNodeTo(base, options.cssClasses.connect); + } + + // Add handles to the slider base. + function addElements ( connectOptions, base ) { + + scope_Handles = []; + scope_Connects = []; + + scope_Connects.push(addConnect(base, connectOptions[0])); + + // [::::O====O====O====] + // connectOptions = [0, 1, 1, 1] + + for ( var i = 0; i < options.handles; i++ ) { + // Keep a list of all added handles. + scope_Handles.push(addOrigin(base, i)); + scope_HandleNumbers[i] = i; + scope_Connects.push(addConnect(base, connectOptions[i + 1])); + } + } + + // Initialize a single slider. + function addSlider ( target ) { + + // Apply classes and data to the target. + addClass(target, options.cssClasses.target); + + if ( options.dir === 0 ) { + addClass(target, options.cssClasses.ltr); + } else { + addClass(target, options.cssClasses.rtl); + } + + if ( options.ort === 0 ) { + addClass(target, options.cssClasses.horizontal); + } else { + addClass(target, options.cssClasses.vertical); + } + + scope_Base = addNodeTo(target, options.cssClasses.base); + } + + + function addTooltip ( handle, handleNumber ) { + + if ( !options.tooltips[handleNumber] ) { + return false; + } + + return addNodeTo(handle.firstChild, options.cssClasses.tooltip); + } + + // The tooltips option is a shorthand for using the 'update' event. + function tooltips ( ) { + + // Tooltips are added with options.tooltips in original order. + var tips = scope_Handles.map(addTooltip); + + bindEvent('update', function(values, handleNumber, unencoded) { + + if ( !tips[handleNumber] ) { + return; + } + + var formattedValue = values[handleNumber]; + + if ( options.tooltips[handleNumber] !== true ) { + formattedValue = options.tooltips[handleNumber].to(unencoded[handleNumber]); + } + + tips[handleNumber].innerHTML = formattedValue; + }); + } + + + function getGroup ( mode, values, stepped ) { + + // Use the range. + if ( mode === 'range' || mode === 'steps' ) { + return scope_Spectrum.xVal; + } + + if ( mode === 'count' ) { + + if ( !values ) { + throw new Error("noUiSlider (" + VERSION + "): 'values' required for mode 'count'."); + } + + // Divide 0 - 100 in 'count' parts. + var spread = ( 100 / (values - 1) ); + var v; + var i = 0; + + values = []; + + // List these parts and have them handled as 'positions'. + while ( (v = i++ * spread) <= 100 ) { + values.push(v); + } + + mode = 'positions'; + } + + if ( mode === 'positions' ) { + + // Map all percentages to on-range values. + return values.map(function( value ){ + return scope_Spectrum.fromStepping( stepped ? scope_Spectrum.getStep( value ) : value ); + }); + } + + if ( mode === 'values' ) { + + // If the value must be stepped, it needs to be converted to a percentage first. + if ( stepped ) { + + return values.map(function( value ){ + + // Convert to percentage, apply step, return to value. + return scope_Spectrum.fromStepping( scope_Spectrum.getStep( scope_Spectrum.toStepping( value ) ) ); + }); + + } + + // Otherwise, we can simply use the values. + return values; + } + } + + function generateSpread ( density, mode, group ) { + + function safeIncrement(value, increment) { + // Avoid floating point variance by dropping the smallest decimal places. + return (value + increment).toFixed(7) / 1; + } + + var indexes = {}; + var firstInRange = scope_Spectrum.xVal[0]; + var lastInRange = scope_Spectrum.xVal[scope_Spectrum.xVal.length-1]; + var ignoreFirst = false; + var ignoreLast = false; + var prevPct = 0; + + // Create a copy of the group, sort it and filter away all duplicates. + group = unique(group.slice().sort(function(a, b){ return a - b; })); + + // Make sure the range starts with the first element. + if ( group[0] !== firstInRange ) { + group.unshift(firstInRange); + ignoreFirst = true; + } + + // Likewise for the last one. + if ( group[group.length - 1] !== lastInRange ) { + group.push(lastInRange); + ignoreLast = true; + } + + group.forEach(function ( current, index ) { + + // Get the current step and the lower + upper positions. + var step; + var i; + var q; + var low = current; + var high = group[index+1]; + var newPct; + var pctDifference; + var pctPos; + var type; + var steps; + var realSteps; + var stepsize; + + // When using 'steps' mode, use the provided steps. + // Otherwise, we'll step on to the next subrange. + if ( mode === 'steps' ) { + step = scope_Spectrum.xNumSteps[ index ]; + } + + // Default to a 'full' step. + if ( !step ) { + step = high-low; + } + + // Low can be 0, so test for false. If high is undefined, + // we are at the last subrange. Index 0 is already handled. + if ( low === false || high === undefined ) { + return; + } + + // Make sure step isn't 0, which would cause an infinite loop (#654) + step = Math.max(step, 0.0000001); + + // Find all steps in the subrange. + for ( i = low; i <= high; i = safeIncrement(i, step) ) { + + // Get the percentage value for the current step, + // calculate the size for the subrange. + newPct = scope_Spectrum.toStepping( i ); + pctDifference = newPct - prevPct; + + steps = pctDifference / density; + realSteps = Math.round(steps); + + // This ratio represents the ammount of percentage-space a point indicates. + // For a density 1 the points/percentage = 1. For density 2, that percentage needs to be re-devided. + // Round the percentage offset to an even number, then divide by two + // to spread the offset on both sides of the range. + stepsize = pctDifference/realSteps; + + // Divide all points evenly, adding the correct number to this subrange. + // Run up to <= so that 100% gets a point, event if ignoreLast is set. + for ( q = 1; q <= realSteps; q += 1 ) { + + // The ratio between the rounded value and the actual size might be ~1% off. + // Correct the percentage offset by the number of points + // per subrange. density = 1 will result in 100 points on the + // full range, 2 for 50, 4 for 25, etc. + pctPos = prevPct + ( q * stepsize ); + indexes[pctPos.toFixed(5)] = ['x', 0]; + } + + // Determine the point type. + type = (group.indexOf(i) > -1) ? 1 : ( mode === 'steps' ? 2 : 0 ); + + // Enforce the 'ignoreFirst' option by overwriting the type for 0. + if ( !index && ignoreFirst ) { + type = 0; + } + + if ( !(i === high && ignoreLast)) { + // Mark the 'type' of this point. 0 = plain, 1 = real value, 2 = step value. + indexes[newPct.toFixed(5)] = [i, type]; + } + + // Update the percentage count. + prevPct = newPct; + } + }); + + return indexes; + } + + function addMarking ( spread, filterFunc, formatter ) { + + var element = document.createElement('div'); + var out = ''; + var valueSizeClasses = [ + options.cssClasses.valueNormal, + options.cssClasses.valueLarge, + options.cssClasses.valueSub + ]; + var markerSizeClasses = [ + options.cssClasses.markerNormal, + options.cssClasses.markerLarge, + options.cssClasses.markerSub + ]; + var valueOrientationClasses = [ + options.cssClasses.valueHorizontal, + options.cssClasses.valueVertical + ]; + var markerOrientationClasses = [ + options.cssClasses.markerHorizontal, + options.cssClasses.markerVertical + ]; + + addClass(element, options.cssClasses.pips); + addClass(element, options.ort === 0 ? options.cssClasses.pipsHorizontal : options.cssClasses.pipsVertical); + + function getClasses( type, source ){ + var a = source === options.cssClasses.value; + var orientationClasses = a ? valueOrientationClasses : markerOrientationClasses; + var sizeClasses = a ? valueSizeClasses : markerSizeClasses; + + return source + ' ' + orientationClasses[options.ort] + ' ' + sizeClasses[type]; + } + + function getTags( offset, source, values ) { + return 'class="' + getClasses(values[1], source) + '" style="' + options.style + ': ' + offset + '%"'; + } + + function addSpread ( offset, values ){ + + // Apply the filter function, if it is set. + values[1] = (values[1] && filterFunc) ? filterFunc(values[0], values[1]) : values[1]; + + // Add a marker for every point + out += '
'; + + // Values are only appended for points marked '1' or '2'. + if ( values[1] ) { + out += '
' + formatter.to(values[0]) + '
'; + } + } + + // Append all points. + Object.keys(spread).forEach(function(a){ + addSpread(a, spread[a]); + }); + + element.innerHTML = out; + + return element; + } + + function pips ( grid ) { + + var mode = grid.mode; + var density = grid.density || 1; + var filter = grid.filter || false; + var values = grid.values || false; + var stepped = grid.stepped || false; + var group = getGroup( mode, values, stepped ); + var spread = generateSpread( density, mode, group ); + var format = grid.format || { + to: Math.round + }; + + return scope_Target.appendChild(addMarking( + spread, + filter, + format + )); + } + + + // Shorthand for base dimensions. + function baseSize ( ) { + var rect = scope_Base.getBoundingClientRect(), alt = 'offset' + ['Width', 'Height'][options.ort]; + return options.ort === 0 ? (rect.width||scope_Base[alt]) : (rect.height||scope_Base[alt]); + } + + // Handler for attaching events trough a proxy. + function attachEvent ( events, element, callback, data ) { + + // This function can be used to 'filter' events to the slider. + // element is a node, not a nodeList + + var method = function ( e ){ + + if ( scope_Target.hasAttribute('disabled') ) { + return false; + } + + // Stop if an active 'tap' transition is taking place. + if ( hasClass(scope_Target, options.cssClasses.tap) ) { + return false; + } + + e = fixEvent(e, data.pageOffset); + + // Handle reject of multitouch + if ( !e ) { + return false; + } + + // Ignore right or middle clicks on start #454 + if ( events === actions.start && e.buttons !== undefined && e.buttons > 1 ) { + return false; + } + + // Ignore right or middle clicks on start #454 + if ( data.hover && e.buttons ) { + return false; + } + + e.calcPoint = e.points[ options.ort ]; + + // Call the event handler with the event [ and additional data ]. + callback ( e, data ); + }; + + var methods = []; + + // Bind a closure on the target for every event type. + events.split(' ').forEach(function( eventName ){ + element.addEventListener(eventName, method, false); + methods.push([eventName, method]); + }); + + return methods; + } + + // Provide a clean event with standardized offset values. + function fixEvent ( e, pageOffset ) { + + // Prevent scrolling and panning on touch events, while + // attempting to slide. The tap event also depends on this. + e.preventDefault(); + + // Filter the event to register the type, which can be + // touch, mouse or pointer. Offset changes need to be + // made on an event specific basis. + var touch = e.type.indexOf('touch') === 0; + var mouse = e.type.indexOf('mouse') === 0; + var pointer = e.type.indexOf('pointer') === 0; + var x; + var y; + + // IE10 implemented pointer events with a prefix; + if ( e.type.indexOf('MSPointer') === 0 ) { + pointer = true; + } + + if ( touch ) { + + // Fix bug when user touches with two or more fingers on mobile devices. + // It's useful when you have two or more sliders on one page, + // that can be touched simultaneously. + // #649, #663, #668 + if ( e.touches.length > 1 ) { + return false; + } + + // noUiSlider supports one movement at a time, + // so we can select the first 'changedTouch'. + x = e.changedTouches[0].pageX; + y = e.changedTouches[0].pageY; + } + + pageOffset = pageOffset || getPageOffset(); + + if ( mouse || pointer ) { + x = e.clientX + pageOffset.x; + y = e.clientY + pageOffset.y; + } + + e.pageOffset = pageOffset; + e.points = [x, y]; + e.cursor = mouse || pointer; // Fix #435 + + return e; + } + + // Translate a coordinate in the document to a percentage on the slider + function calcPointToPercentage ( calcPoint ) { + var location = calcPoint - offset(scope_Base, options.ort); + var proposal = ( location * 100 ) / baseSize(); + return options.dir ? 100 - proposal : proposal; + } + + // Find handle closest to a certain percentage on the slider + function getClosestHandle ( proposal ) { + + var closest = 100; + var handleNumber = false; + + scope_Handles.forEach(function(handle, index){ + + // Disabled handles are ignored + if ( handle.hasAttribute('disabled') ) { + return; + } + + var pos = Math.abs(scope_Locations[index] - proposal); + + if ( pos < closest ) { + handleNumber = index; + closest = pos; + } + }); + + return handleNumber; + } + + // Moves handle(s) by a percentage + // (bool, % to move, [% where handle started, ...], [index in scope_Handles, ...]) + function moveHandles ( upward, proposal, locations, handleNumbers ) { + + var proposals = locations.slice(); + + var b = [!upward, upward]; + var f = [upward, !upward]; + + // Copy handleNumbers so we don't change the dataset + handleNumbers = handleNumbers.slice(); + + // Check to see which handle is 'leading'. + // If that one can't move the second can't either. + if ( upward ) { + handleNumbers.reverse(); + } + + // Step 1: get the maximum percentage that any of the handles can move + if ( handleNumbers.length > 1 ) { + + handleNumbers.forEach(function(handleNumber, o) { + + var to = checkHandlePosition(proposals, handleNumber, proposals[handleNumber] + proposal, b[o], f[o]); + + // Stop if one of the handles can't move. + if ( to === false ) { + proposal = 0; + } else { + proposal = to - proposals[handleNumber]; + proposals[handleNumber] = to; + } + }); + } + + // If using one handle, check backward AND forward + else { + b = f = [true]; + } + + var state = false; + + // Step 2: Try to set the handles with the found percentage + handleNumbers.forEach(function(handleNumber, o) { + state = setHandle(handleNumber, locations[handleNumber] + proposal, b[o], f[o]) || state; + }); + + // Step 3: If a handle moved, fire events + if ( state ) { + handleNumbers.forEach(function(handleNumber){ + fireEvent('update', handleNumber); + fireEvent('slide', handleNumber); + }); + } + } + + // External event handling + function fireEvent ( eventName, handleNumber, tap ) { + + Object.keys(scope_Events).forEach(function( targetEvent ) { + + var eventType = targetEvent.split('.')[0]; + + if ( eventName === eventType ) { + scope_Events[targetEvent].forEach(function( callback ) { + + callback.call( + // Use the slider public API as the scope ('this') + scope_Self, + // Return values as array, so arg_1[arg_2] is always valid. + scope_Values.map(options.format.to), + // Handle index, 0 or 1 + handleNumber, + // Unformatted slider values + scope_Values.slice(), + // Event is fired by tap, true or false + tap || false, + // Left offset of the handle, in relation to the slider + scope_Locations.slice() + ); + }); + } + }); + } + + + // Fire 'end' when a mouse or pen leaves the document. + function documentLeave ( event, data ) { + if ( event.type === "mouseout" && event.target.nodeName === "HTML" && event.relatedTarget === null ){ + eventEnd (event, data); + } + } + + // Handle movement on document for handle and range drag. + function eventMove ( event, data ) { + + // Fix #498 + // Check value of .buttons in 'start' to work around a bug in IE10 mobile (data.buttonsProperty). + // https://connect.microsoft.com/IE/feedback/details/927005/mobile-ie10-windows-phone-buttons-property-of-pointermove-event-always-zero + // IE9 has .buttons and .which zero on mousemove. + // Firefox breaks the spec MDN defines. + if ( navigator.appVersion.indexOf("MSIE 9") === -1 && event.buttons === 0 && data.buttonsProperty !== 0 ) { + return eventEnd(event, data); + } + + // Check if we are moving up or down + var movement = (options.dir ? -1 : 1) * (event.calcPoint - data.startCalcPoint); + + // Convert the movement into a percentage of the slider width/height + var proposal = (movement * 100) / data.baseSize; + + moveHandles(movement > 0, proposal, data.locations, data.handleNumbers); + } + + // Unbind move events on document, call callbacks. + function eventEnd ( event, data ) { + + // The handle is no longer active, so remove the class. + if ( scope_ActiveHandle ) { + removeClass(scope_ActiveHandle, options.cssClasses.active); + scope_ActiveHandle = false; + } + + // Remove cursor styles and text-selection events bound to the body. + if ( event.cursor ) { + document.body.style.cursor = ''; + document.body.removeEventListener('selectstart', document.body.noUiListener); + } + + // Unbind the move and end events, which are added on 'start'. + document.documentElement.noUiListeners.forEach(function( c ) { + document.documentElement.removeEventListener(c[0], c[1]); + }); + + // Remove dragging class. + removeClass(scope_Target, options.cssClasses.drag); + + setZindex(); + + data.handleNumbers.forEach(function(handleNumber){ + fireEvent('set', handleNumber); + fireEvent('change', handleNumber); + fireEvent('end', handleNumber); + }); + } + + // Bind move events on document. + function eventStart ( event, data ) { + + if ( data.handleNumbers.length === 1 ) { + + var handle = scope_Handles[data.handleNumbers[0]]; + + // Ignore 'disabled' handles + if ( handle.hasAttribute('disabled') ) { + return false; + } + + // Mark the handle as 'active' so it can be styled. + scope_ActiveHandle = handle.children[0]; + addClass(scope_ActiveHandle, options.cssClasses.active); + } + + // Fix #551, where a handle gets selected instead of dragged. + event.preventDefault(); + + // A drag should never propagate up to the 'tap' event. + event.stopPropagation(); + + // Attach the move and end events. + var moveEvent = attachEvent(actions.move, document.documentElement, eventMove, { + startCalcPoint: event.calcPoint, + baseSize: baseSize(), + pageOffset: event.pageOffset, + handleNumbers: data.handleNumbers, + buttonsProperty: event.buttons, + locations: scope_Locations.slice() + }); + + var endEvent = attachEvent(actions.end, document.documentElement, eventEnd, { + handleNumbers: data.handleNumbers + }); + + var outEvent = attachEvent("mouseout", document.documentElement, documentLeave, { + handleNumbers: data.handleNumbers + }); + + document.documentElement.noUiListeners = moveEvent.concat(endEvent, outEvent); + + // Text selection isn't an issue on touch devices, + // so adding cursor styles can be skipped. + if ( event.cursor ) { + + // Prevent the 'I' cursor and extend the range-drag cursor. + document.body.style.cursor = getComputedStyle(event.target).cursor; + + // Mark the target with a dragging state. + if ( scope_Handles.length > 1 ) { + addClass(scope_Target, options.cssClasses.drag); + } + + var f = function(){ + return false; + }; + + document.body.noUiListener = f; + + // Prevent text selection when dragging the handles. + document.body.addEventListener('selectstart', f, false); + } + + data.handleNumbers.forEach(function(handleNumber){ + fireEvent('start', handleNumber); + }); + } + + // Move closest handle to tapped location. + function eventTap ( event ) { + + // The tap event shouldn't propagate up + event.stopPropagation(); + + var proposal = calcPointToPercentage(event.calcPoint); + var handleNumber = getClosestHandle(proposal); + + // Tackle the case that all handles are 'disabled'. + if ( handleNumber === false ) { + return false; + } + + // Flag the slider as it is now in a transitional state. + // Transition takes a configurable amount of ms (default 300). Re-enable the slider after that. + if ( !options.events.snap ) { + addClassFor(scope_Target, options.cssClasses.tap, options.animationDuration); + } + + setHandle(handleNumber, proposal, true, true); + + setZindex(); + + fireEvent('slide', handleNumber, true); + fireEvent('set', handleNumber, true); + fireEvent('change', handleNumber, true); + fireEvent('update', handleNumber, true); + + if ( options.events.snap ) { + eventStart(event, { handleNumbers: [handleNumber] }); + } + } + + // Fires a 'hover' event for a hovered mouse/pen position. + function eventHover ( event ) { + + var proposal = calcPointToPercentage(event.calcPoint); + + var to = scope_Spectrum.getStep(proposal); + var value = scope_Spectrum.fromStepping(to); + + Object.keys(scope_Events).forEach(function( targetEvent ) { + if ( 'hover' === targetEvent.split('.')[0] ) { + scope_Events[targetEvent].forEach(function( callback ) { + callback.call( scope_Self, value ); + }); + } + }); + } + + // Attach events to several slider parts. + function bindSliderEvents ( behaviour ) { + + // Attach the standard drag event to the handles. + if ( !behaviour.fixed ) { + + scope_Handles.forEach(function( handle, index ){ + + // These events are only bound to the visual handle + // element, not the 'real' origin element. + attachEvent ( actions.start, handle.children[0], eventStart, { + handleNumbers: [index] + }); + }); + } + + // Attach the tap event to the slider base. + if ( behaviour.tap ) { + attachEvent (actions.start, scope_Base, eventTap, {}); + } + + // Fire hover events + if ( behaviour.hover ) { + attachEvent (actions.move, scope_Base, eventHover, { hover: true }); + } + + // Make the range draggable. + if ( behaviour.drag ){ + + scope_Connects.forEach(function( connect, index ){ + + if ( connect === false || index === 0 || index === scope_Connects.length - 1 ) { + return; + } + + var handleBefore = scope_Handles[index - 1]; + var handleAfter = scope_Handles[index]; + var eventHolders = [connect]; + + addClass(connect, options.cssClasses.draggable); + + // When the range is fixed, the entire range can + // be dragged by the handles. The handle in the first + // origin will propagate the start event upward, + // but it needs to be bound manually on the other. + if ( behaviour.fixed ) { + eventHolders.push(handleBefore.children[0]); + eventHolders.push(handleAfter.children[0]); + } + + eventHolders.forEach(function( eventHolder ) { + attachEvent ( actions.start, eventHolder, eventStart, { + handles: [handleBefore, handleAfter], + handleNumbers: [index - 1, index] + }); + }); + }); + } + } + + + // Split out the handle positioning logic so the Move event can use it, too + function checkHandlePosition ( reference, handleNumber, to, lookBackward, lookForward ) { + + // For sliders with multiple handles, limit movement to the other handle. + // Apply the margin option by adding it to the handle positions. + if ( scope_Handles.length > 1 ) { + + if ( lookBackward && handleNumber > 0 ) { + to = Math.max(to, reference[handleNumber - 1] + options.margin); + } + + if ( lookForward && handleNumber < scope_Handles.length - 1 ) { + to = Math.min(to, reference[handleNumber + 1] - options.margin); + } + } + + // The limit option has the opposite effect, limiting handles to a + // maximum distance from another. Limit must be > 0, as otherwise + // handles would be unmoveable. + if ( scope_Handles.length > 1 && options.limit ) { + + if ( lookBackward && handleNumber > 0 ) { + to = Math.min(to, reference[handleNumber - 1] + options.limit); + } + + if ( lookForward && handleNumber < scope_Handles.length - 1 ) { + to = Math.max(to, reference[handleNumber + 1] - options.limit); + } + } + + // The padding option keeps the handles a certain distance from the + // edges of the slider. Padding must be > 0. + if ( options.padding ) { + + if ( handleNumber === 0 ) { + to = Math.max(to, options.padding); + } + + if ( handleNumber === scope_Handles.length - 1 ) { + to = Math.min(to, 100 - options.padding); + } + } + + to = scope_Spectrum.getStep(to); + + // Limit percentage to the 0 - 100 range + to = limit(to); + + // Return false if handle can't move + if ( to === reference[handleNumber] ) { + return false; + } + + return to; + } + + function toPct ( pct ) { + return pct + '%'; + } + + // Updates scope_Locations and scope_Values, updates visual state + function updateHandlePosition ( handleNumber, to ) { + + // Update locations. + scope_Locations[handleNumber] = to; + + // Convert the value to the slider stepping/range. + scope_Values[handleNumber] = scope_Spectrum.fromStepping(to); + + // Called synchronously or on the next animationFrame + var stateUpdate = function() { + scope_Handles[handleNumber].style[options.style] = toPct(to); + updateConnect(handleNumber); + updateConnect(handleNumber + 1); + }; + + // Set the handle to the new position. + // Use requestAnimationFrame for efficient painting. + // No significant effect in Chrome, Edge sees dramatic performace improvements. + // Option to disable is useful for unit tests, and single-step debugging. + if ( window.requestAnimationFrame && options.useRequestAnimationFrame ) { + window.requestAnimationFrame(stateUpdate); + } else { + stateUpdate(); + } + } + + function setZindex ( ) { + + scope_HandleNumbers.forEach(function(handleNumber){ + // Handles before the slider middle are stacked later = higher, + // Handles after the middle later is lower + // [[7] [8] .......... | .......... [5] [4] + var dir = (scope_Locations[handleNumber] > 50 ? -1 : 1); + var zIndex = 3 + (scope_Handles.length + (dir * handleNumber)); + scope_Handles[handleNumber].childNodes[0].style.zIndex = zIndex; + }); + } + + // Test suggested values and apply margin, step. + function setHandle ( handleNumber, to, lookBackward, lookForward ) { + + to = checkHandlePosition(scope_Locations, handleNumber, to, lookBackward, lookForward); + + if ( to === false ) { + return false; + } + + updateHandlePosition(handleNumber, to); + + return true; + } + + // Updates style attribute for connect nodes + function updateConnect ( index ) { + + // Skip connects set to false + if ( !scope_Connects[index] ) { + return; + } + + var l = 0; + var h = 100; + + if ( index !== 0 ) { + l = scope_Locations[index - 1]; + } + + if ( index !== scope_Connects.length - 1 ) { + h = scope_Locations[index]; + } + + scope_Connects[index].style[options.style] = toPct(l); + scope_Connects[index].style[options.styleOposite] = toPct(100 - h); + } + + // ... + function setValue ( to, handleNumber ) { + + // Setting with null indicates an 'ignore'. + // Inputting 'false' is invalid. + if ( to === null || to === false ) { + return; + } + + // If a formatted number was passed, attemt to decode it. + if ( typeof to === 'number' ) { + to = String(to); + } + + to = options.format.from(to); + + // Request an update for all links if the value was invalid. + // Do so too if setting the handle fails. + if ( to !== false && !isNaN(to) ) { + setHandle(handleNumber, scope_Spectrum.toStepping(to), false, false); + } + } + + // Set the slider value. + function valueSet ( input, fireSetEvent ) { + + var values = asArray(input); + var isInit = scope_Locations[0] === undefined; + + // Event fires by default + fireSetEvent = (fireSetEvent === undefined ? true : !!fireSetEvent); + + values.forEach(setValue); + + // Animation is optional. + // Make sure the initial values were set before using animated placement. + if ( options.animate && !isInit ) { + addClassFor(scope_Target, options.cssClasses.tap, options.animationDuration); + } + + // Now that all base values are set, apply constraints + scope_HandleNumbers.forEach(function(handleNumber){ + setHandle(handleNumber, scope_Locations[handleNumber], true, false); + }); + + setZindex(); + + scope_HandleNumbers.forEach(function(handleNumber){ + + fireEvent('update', handleNumber); + + // Fire the event only for handles that received a new value, as per #579 + if ( values[handleNumber] !== null && fireSetEvent ) { + fireEvent('set', handleNumber); + } + }); + } + + // Reset slider to initial values + function valueReset ( fireSetEvent ) { + valueSet(options.start, fireSetEvent); + } + + // Get the slider value. + function valueGet ( ) { + + var values = scope_Values.map(options.format.to); + + // If only one handle is used, return a single value. + if ( values.length === 1 ){ + return values[0]; + } + + return values; + } + + // Removes classes from the root and empties it. + function destroy ( ) { + + for ( var key in options.cssClasses ) { + if ( !options.cssClasses.hasOwnProperty(key) ) { continue; } + removeClass(scope_Target, options.cssClasses[key]); + } + + while (scope_Target.firstChild) { + scope_Target.removeChild(scope_Target.firstChild); + } + + delete scope_Target.noUiSlider; + } + + // Get the current step size for the slider. + function getCurrentStep ( ) { + + // Check all locations, map them to their stepping point. + // Get the step point, then find it in the input list. + return scope_Locations.map(function( location, index ){ + + var nearbySteps = scope_Spectrum.getNearbySteps( location ); + var value = scope_Values[index]; + var increment = nearbySteps.thisStep.step; + var decrement = null; + + // If the next value in this step moves into the next step, + // the increment is the start of the next step - the current value + if ( increment !== false ) { + if ( value + increment > nearbySteps.stepAfter.startValue ) { + increment = nearbySteps.stepAfter.startValue - value; + } + } + + + // If the value is beyond the starting point + if ( value > nearbySteps.thisStep.startValue ) { + decrement = nearbySteps.thisStep.step; + } + + else if ( nearbySteps.stepBefore.step === false ) { + decrement = false; + } + + // If a handle is at the start of a step, it always steps back into the previous step first + else { + decrement = value - nearbySteps.stepBefore.highestStep; + } + + + // Now, if at the slider edges, there is not in/decrement + if ( location === 100 ) { + increment = null; + } + + else if ( location === 0 ) { + decrement = null; + } + + // As per #391, the comparison for the decrement step can have some rounding issues. + var stepDecimals = scope_Spectrum.countStepDecimals(); + + // Round per #391 + if ( increment !== null && increment !== false ) { + increment = Number(increment.toFixed(stepDecimals)); + } + + if ( decrement !== null && decrement !== false ) { + decrement = Number(decrement.toFixed(stepDecimals)); + } + + return [decrement, increment]; + }); + } + + // Attach an event to this slider, possibly including a namespace + function bindEvent ( namespacedEvent, callback ) { + scope_Events[namespacedEvent] = scope_Events[namespacedEvent] || []; + scope_Events[namespacedEvent].push(callback); + + // If the event bound is 'update,' fire it immediately for all handles. + if ( namespacedEvent.split('.')[0] === 'update' ) { + scope_Handles.forEach(function(a, index){ + fireEvent('update', index); + }); + } + } + + // Undo attachment of event + function removeEvent ( namespacedEvent ) { + + var event = namespacedEvent && namespacedEvent.split('.')[0]; + var namespace = event && namespacedEvent.substring(event.length); + + Object.keys(scope_Events).forEach(function( bind ){ + + var tEvent = bind.split('.')[0], + tNamespace = bind.substring(tEvent.length); + + if ( (!event || event === tEvent) && (!namespace || namespace === tNamespace) ) { + delete scope_Events[bind]; + } + }); + } + + // Updateable: margin, limit, padding, step, range, animate, snap + function updateOptions ( optionsToUpdate, fireSetEvent ) { + + // Spectrum is created using the range, snap, direction and step options. + // 'snap' and 'step' can be updated, 'direction' cannot, due to event binding. + // If 'snap' and 'step' are not passed, they should remain unchanged. + var v = valueGet(); + + var updateAble = ['margin', 'limit', 'padding', 'range', 'animate', 'snap', 'step', 'format']; + + // Only change options that we're actually passed to update. + updateAble.forEach(function(name){ + if ( optionsToUpdate[name] !== undefined ) { + originalOptions[name] = optionsToUpdate[name]; + } + }); + + var newOptions = testOptions(originalOptions); + + // Load new options into the slider state + updateAble.forEach(function(name){ + if ( optionsToUpdate[name] !== undefined ) { + options[name] = newOptions[name]; + } + }); + + // Save current spectrum direction as testOptions in testRange call + // doesn't rely on current direction + newOptions.spectrum.direction = scope_Spectrum.direction; + scope_Spectrum = newOptions.spectrum; + + // Limit, margin and padding depend on the spectrum but are stored outside of it. (#677) + options.margin = newOptions.margin; + options.limit = newOptions.limit; + options.padding = newOptions.padding; + + // Invalidate the current positioning so valueSet forces an update. + scope_Locations = []; + valueSet(optionsToUpdate.start || v, fireSetEvent); + } + + // Throw an error if the slider was already initialized. + if ( scope_Target.noUiSlider ) { + throw new Error("noUiSlider (" + VERSION + "): Slider was already initialized."); + } + + // Create the base element, initialise HTML and set classes. + // Add handles and connect elements. + addSlider(scope_Target); + addElements(options.connect, scope_Base); + + scope_Self = { + destroy: destroy, + steps: getCurrentStep, + on: bindEvent, + off: removeEvent, + get: valueGet, + set: valueSet, + reset: valueReset, + // Exposed for unit testing, don't use this in your application. + __moveHandles: function(a, b, c) { moveHandles(a, b, scope_Locations, c); }, + options: originalOptions, // Issue #600, #678 + updateOptions: updateOptions, + target: scope_Target, // Issue #597 + pips: pips // Issue #594 + }; + + // Attach user events. + bindSliderEvents(options.events); + + // Use the public value method to set the start values. + valueSet(options.start); + + if ( options.pips ) { + pips(options.pips); + } + + if ( options.tooltips ) { + tooltips(); + } + + return scope_Self; + +} + + + // Run the standard initializer + function initialize ( target, originalOptions ) { + + if ( !target.nodeName ) { + throw new Error("noUiSlider (" + VERSION + "): create requires a single element."); + } + + // Test the options and create the slider environment; + var options = testOptions( originalOptions, target ); + var api = closure( target, options, originalOptions ); + + target.noUiSlider = api; + + return api; + } + + // Use an object instead of a function for future expansibility; + return { + version: VERSION, + create: initialize + }; + +})); \ No newline at end of file diff --git a/server-neural.rb b/server-neural.rb new file mode 100755 index 0000000..e8915b4 --- /dev/null +++ b/server-neural.rb @@ -0,0 +1,434 @@ +#!/usr/bin/env ruby + +require 'sinatra' +require 'sinatra/cross_origin' +require 'sinatra/reloader' +require 'nanomsg' +require 'zipf' +require 'json' +require 'tilt/haml' +require 'uri' +require 'rack' + +# ############################################################################# +# Load configuration file and setup global variables +# ############################################################################# +require_relative "#{ARGV[0]}" # load configuration for this session +$lock = false # lock if currently learning/translating +$last_reply = nil # cache last reply +$last_processed_postedit = "" # to show to the user +$confirmed = true # client received translation? +if !FileTest.exist? LOCK_FILE # locked? + $db = {} # data file (JSON format) + $env = {} # environment variables (socket connections to daemons) +end +$status = "Idle" # current server status + +# ############################################################################# +# Daemons +# ############################################################################# +DIR="/srv/postedit" +$daemons = { + :tokenizer => "#{DIR}/lfpe/util/nanomsg_wrapper.rb -a tokenize -S '__ADDR__' -e #{EXTERNAL} -l #{TARGET_LANG}", + :tokenizer_src => "#{DIR}/lfpe/util/nanomsg_wrapper.rb -a tokenize -S '__ADDR__' -e #{EXTERNAL} -l #{SOURCE_LANG}", + :detokenizer => "#{DIR}/lfpe/util/nanomsg_wrapper.rb -a detokenize -S '__ADDR__' -e #{EXTERNAL} -l #{TARGET_LANG}", + :detokenizer_src => "#{DIR}/lfpe/util/nanomsg_wrapper.rb -a detokenize -S '__ADDR__' -e #{EXTERNAL} -l #{SOURCE_LANG}", + :bpe => "#{DIR}/lfpe/util/nanomsg_wrapper.rb -a bpe -S '__ADDR__' -e #{EXTERNAL} -b #{SESSION_DIR}/bpe", #-B #{SESSION_DIR}/#{TARGET_LANG}", + #:debpe => "#{DIR}/lfpe/util/nanomsg_wrapper.rb -a de-bpe -S '__ADDR__' -e #{EXTERNAL}", + :truecaser => "#{DIR}/lfpe/util/nanomsg_wrapper.rb -a truecase -S '__ADDR__' -e #{EXTERNAL} -t #{SESSION_DIR}/truecase.model", + #:lamtram => "#{DIR}/online-lamtram/src/lamtram/lamtram-train-online --master_addr '__ADDR__' --model_in #{SESSION_DIR}/model --model_out #{SESSION_DIR}/model.out --dropout 0.5 --learning_rate 0.5 --beam_size 1 --size_limit 100 --unk_penalty 1.0 --word_penalty 2.5" +} + +$remote_daemons = { + :lamtram => true +} + +# ############################################################################# +# Set-up Sinatra +# ############################################################################# +set :server, 'thin' +set :bind, SERVER_IP +set :port, WEB_PORT +set :allow_origin, :any +set :allow_methods, [:get, :post] +set :allow_credentials, true +set :max_age, "1728000" +set :expose_headers, ['Content-Type'] +set :public_folder, File.dirname(__FILE__) + '/static' + +Rack::Utils.key_space_limit = 68719476736 + +# ############################################################################# +# Helper functions +# ############################################################################# +def logmsg name, msg + STDERR.write "[#{name}] #{msg}\n" +end + +def start_daemon cmd, name, addr + logmsg :server, "starting #{name} daemon" + cmd.gsub! '__ADDR__', addr + + sock = NanoMsg::PairSocket.new + sock.connect addr + pid = spawn(cmd) + Process.detach pid + logmsg :server, "#{name} detached" + logmsg :server, "< got #{sock.recv} from #{name}" + + return sock, pid +end + +def stop_daemon key + $env[key][:socket].send "shutdown" # every daemon shuts down + # after receiving this keyword + logmsg :server, "< #{key} is #{$env[key][:socket].recv}" +end + +def stop_all_daemons + $status = "Shutting down" # status + logmsg :server, "shutting down all daemons" + $env.each_key { |k| + stop_daemon k + } + + $status = "Ready to shutdown" # status +end + +def update_database reset=false + $status = "Updating database" # status + if !reset + $db['progress'] += 1 + else + $db['progress'] = 0 + end + j = JSON.generate $db + f = WriteFile.new DB_FILE + f.write j.to_s + f.close + + $status = "Updated database" # status +end + +def init + $status = "Initialization" # status + $db = JSON.parse ReadFile.read DB_FILE # data from JSON file + # working directory + `mkdir -p #{WORK_DIR}/` + `mkdir #{WORK_DIR}/g` + + if OLM + `mkfifo #{WORK_DIR}/refp` + end + + # setup environment, start daemons + port = BEGIN_PORT_RANGE + $daemons.each { |name,cmd| + logmsg :server, "starting #{name} daemon" + sock, pid = start_daemon cmd, name, "tcp://147.142.207.34:#{port}" + $env[name] = { :socket => sock, :pid => pid } + port += 1 + logmsg :server, "starting #{name} daemon done" + } + $remote_daemons.each { |name,_| + addr = "tcp://147.142.207.34:#{port}" + sock = NanoMsg::PairSocket.new + sock.bind addr + logmsg :server, "waiting for remote daemon #{name} on #{addr}" + logmsg :server, "< got #{sock.recv} from remote #{name}" + $env[name] = { :socket => sock, :pid => nil } + port += 1 + } + + send_recv :truecaser, "lOaD iT" + send_recv :bpe, "blablablubbbla" + #send_recv :debpe, "asdf@@ asd" + + # lock file + `touch #{LOCK_FILE}` + $status = "Initialized" # status +end + +def send_recv daemon, msg # simple pair communcation + socket = $env[daemon][:socket] # query -> answer + logmsg daemon, "> sending message: '#{msg}'" + socket.send msg + logmsg daemon, "waiting ..." + ans = socket.recv.force_encoding("UTF-8").strip + logmsg daemon, "< received answer: '#{ans}'" + + return ans +end + +# ############################################################################# +# Run init() [just once] +# ############################################################################# +init if !FileTest.exist?(LOCK_FILE) + +# ############################################################################# +# Routes +# ############################################################################# +get '/' do + cross_origin # enable Cross-Origin Resource Sharing + + return "" # return +end + +post '/next' do + cross_origin + + $status = "Received request" # status + reply = request.body.read + Thread.new { process_next reply } +end + +def process_next reply + $status = "Processing request" # status + data = JSON.parse(URI.decode(reply)) + if $lock + $status = "Locked" # status + return + end + $lock = true # lock + if !data['name'] || data['name'] == "" + $status = "Error: Name not given." + return + end + if data['key'] != SESSION_KEY + $status = "Error: Key mismatch (#{data['key']}, #{SESSION_KEY})" + return + end + + if data["EDIT"] + $status = "Processing post-edit" # status + logmsg :server, "received post-edit" + # 0. save raw post-edit + source = data["source_value"] + post_edit = data["post_edit"] + $status = "Processing post-edit ..." # status + post_edit.strip! + post_edit.lstrip! + # fill db + $db['feedback'] << reply + $db['post_edits_raw'] << post_edit + $db['durations'] << data['duration'].to_f + $db['durations_rating'] << data['duration_rating'].to_f + $db['count_click'] << data['count_click'].to_i + $db['count_kbd'] << data['count_kbd'].to_i + $db['post_edits_display'] << send_recv(:detokenizer, post_edit) + $db['ratings'] << data['rating'].to_f + $last_processed_postedit = $db['post_edits_display'].last + # 1. tokenize + $status = "Tokenizing post-edit" # status + logmsg :server, "tokenizing post-edit" + post_edit = send_recv :tokenizer, post_edit + # 2. truecase + $status = "Truecasing post-edit" # status + logmsg :server, "truecasing post-edit" + post_edit = send_recv :truecaser, post_edit + # 3. bpe + $status = "BPE'ing post-edit" + logmsg :server, "bpe'ing post-edit" + post_edit = send_recv :bpe, post_edit + # 4. save processed post-edits + $status = "saving processed post-edit" # status + logmsg :db, "saving processed post-edit" + $db['post_edits'] << post_edit.strip + if data['nochange'] + logmsg :server, "no change" + end + if !NOLEARN && !NOMT + $status = "Updating model" # status + logmsg :server, "updating ..." + $status = "Learning from post-edit" # status + send_recv :lamtram, "act:learn ||| #{source} ||| #{post_edit}" + $db['updated'] << true + end + logmsg :db, "updating database" + update_database + end + $status = "Getting next data to translate" # status + source = $db['source_segments'][$db['progress']] + raw_source = $db['raw_source_segments'][$db['progress']] + if !source # input is done + begin + stop_daemon :lamtram + rescue + end + logmsg :server, "end of input, sending 'fin'" + $lock = false + $status = "Ready" # status + $last_reply = {'fin'=>true, + 'processed_postedit'=>$last_processed_postedit + }.to_json + return # return + elsif !$confirmed \ + || ($confirmed && $last_reply && $last_reply!="" \ + && !data["EDIT"]) # send last reply + logmsg :server, "locked, re-sending last reply" + logmsg :server, "last_reply: '#{$last_reply}'" + $lock = false + $status = "Ready" # status + return + else + source = source.strip.lstrip + raw_source = raw_source.strip.lstrip + + if NOMT # 0. no mt? + $lock = false + logmsg :server, "no mt" + obj = Hash.new + obj["progress"] = $db["progress"] + obj["source"] = source + obj["raw_source"] = raw_source + obj["processed_postedit"] = $last_processed_postedit + $last_reply = obj.to_json + $status = "Ready" # status + return + end + + $status = "Translating" # status + msg = "act:translate ||| #{source}" + obj = Hash.new + obj["transl_bpe"] = send_recv :lamtram, msg + obj["transl"] = send_recv(:truecaser, obj["transl_bpe"].gsub(/((@@ )|@@$)/, "")).strip + $status = "Processing raw translation" # status + obj["transl_detok"] = send_recv(:detokenizer, obj["transl"]).strip + obj["source"] = source + obj["progress"] = $db['progress'] + obj["raw_source"] = raw_source + $db["mt_raw_bpe"] << obj["transl_bpe"] + $db["mt_raw"] << obj["transl"] + $db["mt"] << obj["transl_detok"] + obj["processed_postedit"] = $last_processed_postedit + # 5. reply + $last_reply = obj.to_json + $lock = false + $confirmed = false + logmsg :server, "response: '#{$last_reply}'" + $status = "Ready" # status + return # return + end +end + +get '/fetch' do # fetch next + cross_origin + return "Locked" if $locked + return $last_reply +end + +get '/fetch_processed_postedit/:i' do # processed post-edit + cross_origin + i = params[:i].to_i + return $db['post_edits_display'][i] +end + +get '/progress' do # processed post-edit + cross_origin + return $db['progress'] +end + +get '/status' do # check status + cross_origin + logmsg :server, "status: #{$status}" + return "Locked" if $locked + return $status +end + +get '/status_debug' do # check status + cross_origin + logmsg :server, "status: #{$status}" + return "[##{$db["progress"]}] Locked" if $locked + return "[##{$db["progress"]}] #{$status}" +end + +get '/confirm' do # client confirms received translation + cross_origin + $confirmed = true + logmsg :server, "confirmed = #{$confirmed}" + + return "#{$confirmed}" +end + +get '/reset_progress' do # reset current session + return "locked" if $lock + $db = JSON.parse ReadFile.read DB_FILE + $db['post_edits'].clear + $db['post_edits_raw'].clear + $db['post_edits_display'].clear + $db['mt'].clear + $db['mt_raw'].clear + $db['updated'].clear + $db['durations'].clear + $db['durations_rating'].clear + $db['feedback'].clear + $db['progress'] = -1 + $db['count_kbd'].clear + $db['count_click'].clear + $db['ratings'].clear + update_database true + $confirmed = true + $last_reply = nil + + return "progress reset: done" +end + +get '/shutdown' do # stop daemons and shut down server + logmsg :server, "shutting down daemons" + stop_all_daemons + + return "stopped all daemons, ready to shutdown" +end + +get '/summary' do + logmsg :server, "showing summary" + + data = JSON.parse ReadFile.read(DB_FILE).force_encoding("UTF-8") + + bleu_scores = [] + data["post_edits"].each_with_index { |pe,j| + f = Tempfile.new "lfpe-summary-pe" + g = Tempfile.new "lfpe-summary-ref" + f.write pe+"\n" + g.write data["references"][j]+"\n" + f.close + g.close + bleu_scores << [1.0, (`#{CDEC}/mteval/fast_score -i #{f.path} -r #{g.path} -m ibm_bleu 2>/dev/null`.to_f).round(2)].min + f.unlink + g.unlink + } + + ter_scores = [] + data["post_edits"].each_with_index { |pe,j| + f = Tempfile.new "lfpe-summary-pe" + g = Tempfile.new "lfpe-summary-ref" + f.write pe.gsub("@@ ","")+"\n" + g.write data["references"][j]+"\n" + f.close + g.close + ter_scores << [1.0, (`#{CDEC}/mteval/fast_score -i #{f.path} -r #{g.path} -m ter 2>/dev/null`.to_f).round(2)].min + f.unlink + g.unlink + } + + hter_scores = [] + data["post_edits"].each_with_index { |pe,j| + f = Tempfile.new "lfpe-summary-mt" + g = Tempfile.new "lfpe-summary-pe" + f.write data["mt_raw"][j]+"\n" + g.write pe.gsub("@@ ", "")+"\n" + f.close + g.close + hter_scores << [1.0, (`#{CDEC}/mteval/fast_score -i #{f.path} -r #{g.path} -m ter 2>/dev/null`.to_f).round(2)].min + f.unlink + g.unlink + } + + haml :summary, :locals => { :session_key => SESSION_KEY, + :data => data, + :bleu_scores => bleu_scores, + :ter_scores => ter_scores, + :hter_scores => hter_scores } + +end + diff --git a/server.rb b/server.rb index 38987de..edc7d35 100755 --- a/server.rb +++ b/server.rb @@ -127,9 +127,11 @@ def init # setup environment, start daemons port = BEGIN_PORT_RANGE $daemons.each { |name,cmd| + logmsg :server, "starting #{name} daemon" sock, pid = start_daemon cmd, name, "tcp://127.0.0.1:#{port}" $env[name] = { :socket => sock, :pid => pid } port += 1 + logmsg :server, "starting #{name} daemon done" } if OLM @@ -372,9 +374,11 @@ def process_next reply $db['svg'] << data['svg'] $db['original_svg'] << data['original_svg'] $db['durations'] << data['duration'].to_f + $db['durations_rating'] << data['duration_rating'].to_f $db['count_click'] << data['count_click'].to_i $db['count_kbd'] << data['count_kbd'].to_i $db['post_edits_display'] << send_recv(:detokenizer, post_edit) + $db['ratings'] << data['rating'].to_f $last_processed_postedit = $db['post_edits_display'].last # 1. tokenize $status = "Tokenizing post-edit" # status @@ -437,6 +441,7 @@ def process_next reply `cp #{WORK_DIR}/dtrain.debug.json \ #{WORK_DIR}/#{$db['progress']}.dtrain.debug.json.pass1` else + logmsg :server, "no NOLOO" send_recv :dtrain, "act:learn ||| #{annotated_source} ||| #{post_edit}" `cp #{WORK_DIR}/dtrain.debug.json \ #{WORK_DIR}/#{$db['progress']}.dtrain.debug.json.pass0` @@ -658,6 +663,9 @@ get '/debug' do # debug view if data["durations"].size == 0 data["durations"] << -1 end + if data["durations_rating"].size == 0 + data["durations_rating"] << -1 + end fn = "#{WORK_DIR}/#{$db["progress"]-1}.dtrain.debug.json.pass" pass = 0 @@ -807,6 +815,7 @@ get '/reset_progress' do # reset current session $db['mt_raw'].clear $db['updated'].clear $db['durations'].clear + $db['durations_rating'].clear $db['derivations'].clear $db['derivations_proc'].clear $db['svg'].clear @@ -815,6 +824,7 @@ get '/reset_progress' do # reset current session $db['progress'] = -1 $db['count_kbd'].clear $db['count_click'].clear + $db['ratings'].clear update_database true $confirmed = true $last_reply = nil @@ -891,6 +901,7 @@ get '/summary' do haml :summary, :locals => { :session_key => SESSION_KEY, :data => data, :ter_scores => ter_scores, + :bleu_scores => bleu_scores, :hter_scores => hter_scores } end diff --git a/static/nouislider.css b/static/nouislider.css new file mode 100644 index 0000000..4e6206b --- /dev/null +++ b/static/nouislider.css @@ -0,0 +1,257 @@ +/*! nouislider - 9.2.0 - 2017-01-11 10:35:35 */ +/* Functional styling; + * These styles are required for noUiSlider to function. + * You don't need to change these rules to apply your design. + */ +.noUi-target, +.noUi-target * { + -webkit-touch-callout: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-user-select: none; + -ms-touch-action: none; + touch-action: none; + -ms-user-select: none; + -moz-user-select: none; + user-select: none; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.noUi-target { + position: relative; + direction: ltr; +} +.noUi-base { + width: 100%; + height: 100%; + position: relative; + z-index: 1; + /* Fix 401 */ +} +.noUi-connect { + position: absolute; + right: 0; + top: 0; + left: 0; + bottom: 0; +} +.noUi-origin { + position: absolute; + height: 0; + width: 0; +} +.noUi-handle { + position: relative; + z-index: 1; +} +.noUi-state-tap .noUi-connect, +.noUi-state-tap .noUi-origin { + -webkit-transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s; + transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s; +} +.noUi-state-drag * { + cursor: inherit !important; +} +/* Painting and performance; + * Browsers can paint handles in their own layer. + */ +.noUi-base, +.noUi-handle { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} +/* Slider size and handle placement; + */ +.noUi-horizontal { + height: 18px; +} +.noUi-horizontal .noUi-handle { + width: 34px; + height: 28px; + left: -17px; + top: -6px; +} +.noUi-vertical { + width: 18px; +} +.noUi-vertical .noUi-handle { + width: 28px; + height: 34px; + left: -6px; + top: -17px; +} +/* Styling; + */ +.noUi-target { + background: #FAFAFA; + border-radius: 4px; + border: 1px solid #D3D3D3; + box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB; +} +.noUi-connect { + background: #3FB8AF; + box-shadow: inset 0 0 3px rgba(51, 51, 51, 0.45); + -webkit-transition: background 450ms; + transition: background 450ms; +} +/* Handles and cursors; + */ +.noUi-draggable { + cursor: ew-resize; +} +.noUi-vertical .noUi-draggable { + cursor: ns-resize; +} +.noUi-handle { + border: 1px solid #D9D9D9; + border-radius: 3px; + background: #FFF; + cursor: default; + box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #EBEBEB, 0 3px 6px -3px #BBB; +} +.noUi-active { + box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #DDD, 0 3px 6px -3px #BBB; +} +/* Handle stripes; + */ +.noUi-handle:before, +.noUi-handle:after { + content: ""; + display: block; + position: absolute; + height: 14px; + width: 1px; + background: #E8E7E6; + left: 14px; + top: 6px; +} +.noUi-handle:after { + left: 17px; +} +.noUi-vertical .noUi-handle:before, +.noUi-vertical .noUi-handle:after { + width: 14px; + height: 1px; + left: 6px; + top: 14px; +} +.noUi-vertical .noUi-handle:after { + top: 17px; +} +/* Disabled state; + */ +[disabled] .noUi-connect { + background: #B8B8B8; +} +[disabled].noUi-target, +[disabled].noUi-handle, +[disabled] .noUi-handle { + cursor: not-allowed; +} +/* Base; + * + */ +.noUi-pips, +.noUi-pips * { + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.noUi-pips { + position: absolute; + color: #999; +} +/* Values; + * + */ +.noUi-value { + position: absolute; + text-align: center; +} +.noUi-value-sub { + color: #ccc; + font-size: 10px; +} +/* Markings; + * + */ +.noUi-marker { + position: absolute; + background: #CCC; +} +.noUi-marker-sub { + background: #AAA; +} +.noUi-marker-large { + background: #AAA; +} +/* Horizontal layout; + * + */ +.noUi-pips-horizontal { + padding: 10px 0; + height: 80px; + top: 100%; + left: 0; + width: 100%; +} +.noUi-value-horizontal { + -webkit-transform: translate3d(-50%, 50%, 0); + transform: translate3d(-50%, 50%, 0); +} +.noUi-marker-horizontal.noUi-marker { + margin-left: -1px; + width: 2px; + height: 5px; +} +.noUi-marker-horizontal.noUi-marker-sub { + height: 10px; +} +.noUi-marker-horizontal.noUi-marker-large { + height: 15px; +} +/* Vertical layout; + * + */ +.noUi-pips-vertical { + padding: 0 10px; + height: 100%; + top: 0; + left: 100%; +} +.noUi-value-vertical { + -webkit-transform: translate3d(0, 50%, 0); + transform: translate3d(0, 50%, 0); + padding-left: 25px; +} +.noUi-marker-vertical.noUi-marker { + width: 5px; + height: 2px; + margin-top: -1px; +} +.noUi-marker-vertical.noUi-marker-sub { + width: 10px; +} +.noUi-marker-vertical.noUi-marker-large { + width: 15px; +} +.noUi-tooltip { + display: block; + position: absolute; + border: 1px solid #D9D9D9; + border-radius: 3px; + background: #fff; + color: #000; + padding: 5px; + text-align: center; +} +.noUi-horizontal .noUi-tooltip { + -webkit-transform: translate(-50%, 0); + transform: translate(-50%, 0); + left: 50%; + bottom: 120%; +} +.noUi-vertical .noUi-tooltip { + -webkit-transform: translate(0, -50%); + transform: translate(0, -50%); + top: 50%; + right: 120%; +} diff --git a/util/kill_all b/util/kill_all index e82f822..00c0a65 100755 --- a/util/kill_all +++ b/util/kill_all @@ -1,4 +1,4 @@ #!/bin/bash -for i in {1..6}; do ps ax | grep -P "(server.rb|wrapper.rb|atools|net_fa|sa.extract|dtrain|truecase.perl)" | grep -v vim | grep -v -P "^\s\+$" | cut -d " " -f $i | xargs kill -9 &>/dev/null; done +for i in {1..6}; do ps ax | grep -P "(server.rb|server-neural.rb|wrapper.rb|atools|net_fa|sa.extract|dtrain|lamtram|truecase.perl)" | grep -v vim | grep -v -P "^\s\+$" | cut -d " " -f $i | xargs kill -9 &>/dev/null; done diff --git a/util/nanomsg_wrapper.rb b/util/nanomsg_wrapper.rb index d0e6ca7..fbdaafa 100755 --- a/util/nanomsg_wrapper.rb +++ b/util/nanomsg_wrapper.rb @@ -5,17 +5,15 @@ require 'open3' require 'trollop' conf = Trollop::options do - opt :action, "tokenize, detokenize, truecase, or lowercase", :short => "-a", :type => :string, :required => true + opt :action, "tokenize, detokenize, truecase, lowercase, [de-]bpe", :short => "-a", :type => :string, :required => true opt :addr, "socket address", :short => "-S", :type => :string, :required => true opt :ext, "path to externals", :short => "-e", :type => :string, :required => true opt :lang, "language", :short => "-l", :type => :string opt :truecase_model, "model file for truecaser", :short => "-t", :type => :string + opt :bpe, "codes", :short => "-b", :type => :string + opt :bpe_vocab, "BPE vocab", :short => "-B", :type => :string end -sock = NanoMsg::PairSocket.new -sock.bind conf[:addr] -sock.send "hello" - if conf[:action] == "detokenize" cmd = "#{conf[:ext]}/detokenizer.perl -q -b -u -l #{conf[:lang]}" if !conf[:lang] @@ -33,10 +31,23 @@ elsif conf[:action] == "truecase" end elsif conf[:action] == "lowercase" cmd = "#{conf[:ext]}/lowercase.perl" +elsif conf[:action] == "bpe" + cmd = "#{conf[:ext]}/apply_bpe.py -c #{conf[:bpe]}" # --vocabulary #{conf[:bpe_vocab]} --vocabulary-threshold 1" +elsif conf[:action] == "de-bpe" + cmd = "#{conf[:ext]}/de-bpe" else STDERR.write "[wrapper] Unknown action #{conf[:action]}, exiting!\n"; exit end +STDERR.write "[wrapper] will run cmd '#{cmd}'\n" + +sock = NanoMsg::PairSocket.new +STDERR.write "[wrapper] addr: #{conf[:addr]}\n" +sock.bind conf[:addr] +sock.send "hello" +STDERR.write "[wrapper] sent hello\n" + pin, pout, perr = Open3.popen3(cmd) + while true inp = sock.recv.strip break if !inp||inp=="shutdown" diff --git a/util/run_all b/util/run_all index e82ced9..e98c4a7 100755 --- a/util/run_all +++ b/util/run_all @@ -4,6 +4,6 @@ for i in `cat ../sessions/sessions | cut -f 1`; do echo $i ./util/run_session $i & - sleep 10 + sleep 1 done diff --git a/util/run_session b/util/run_session index a4b7a6c..4eecc17 100755 --- a/util/run_session +++ b/util/run_session @@ -12,6 +12,6 @@ rm $SESSION_DIR/work/lockfile rm -r $SESSION_DIR/work/ mkdir -p $SESSION_DIR/work cp $SESSION_DIR/data.json.original $SESSION_DIR/data.json -cp $SESSION_DIR/g/original/* $SESSION_DIR/g/ -$UTIL/../server.rb $SESSION_DIR/conf.rb &>$SESSION_DIR/work/session.out +#cp $SESSION_DIR/g/original/* $SESSION_DIR/g/ +$UTIL/../server-neural.rb $SESSION_DIR/conf.rb &>$SESSION_DIR/work/session.out diff --git a/views/summary.haml b/views/summary.haml index 671f1b5..7339367 100644 --- a/views/summary.haml +++ b/views/summary.haml @@ -11,51 +11,56 @@ %h1 Summary %p.small (Session #{session_key}) %p Data is shown in the MT system's formatting. BLEU is calculated without smoothing. TER capped at 1.0. - %table - %tr - %td - #{"#"} - %td - %strong Source - %td - %strong - Post-Edit - %td - %strong - Reference - %td - %strong - BLEU - %td - %strong - TER - %td - %strong - MT - %td - %strong - HTER - %td - %strong - Key. - %td - %strong - M.a. - %td - %strong - Dur. - - data["post_edits"].each_with_index do |pe,j| - %tr - %td.center #{j+1}. - %td #{data["source_segments"][j]} - %td #{pe} - %td #{data["references"][j]} - %td.center #{(BLEU::per_sentence_bleu(pe, [data["references"][j]], 4, 0)*100).round 2}% - %td.center #{ter_scores[j]} - %td #{data["mt_raw"][j]} - %td.center #{hter_scores[j]} - %td.center #{data["count_kbd"][j]} - %td.center #{data["count_click"][j]} - %td.center #{((data["durations"][j]/1000.0)/60.0).round 1} min + -if data["source_segments"].size == data["mt_raw"].size + %table + %tr + %td + #{"#"} + %td + %strong Source + %td + %strong + Post-Edit + %td + %strong + Reference + %td + %strong + BLEU + %td + %strong + TER + %td + %strong + MT + %td + %strong + HTER + %td + %strong + Rating + %td + %strong + Key. + %td + %strong + M.a. + %td + %strong + Dur. + - data["post_edits"].each_with_index do |pe,j| + %tr + %td.center #{j+1}. + %td #{data["source_segments"][j].gsub(/((@@ )|(@@$))/, "")} + %td #{pe.gsub("@@ ", "")} + %td #{data["references"][j]} + %td.center #{bleu_scores[j]} + %td.center #{ter_scores[j]} + %td #{data["mt_raw"][j].gsub(/((@@ )|(@@$))/, "")} + %td.center #{hter_scores[j]} + %td.center #{data["ratings"][j]} + %td.center #{data["count_kbd"][j]} + %td.center #{data["count_click"][j]} + %td.center #{((data["durations"][j]/1000.0)/60.0).round 1} min [#{((data["durations_rating"][j]/1000.0)/60.0).round 1} min] -- cgit v1.2.3