Commits

nathbot committed ceb9fe0

Réorganisation des dossiers

Comments (0)

Files changed (20)

jsplugin/_bugs & changes.txt

+BUGS
+
+texte de la status bar n'est pas maj qd une jsaction le modifie (mais les colonnes supplémentaires, si)
+
+CHANGES
+
+Fenêtre globale des paramètres, avec paramètres custom typés (bool, string, number) + validation et explications.
+
+réorganisation de la liste des erreurs:
+- possibilité de balancer des messages plus circonstanciés (explication détaillée d'une erreur, avec résultat d'un fix éventuel, le tout en HTML + CSS)
+- possibilité de trier les messages d'erreur du 1er au dernier sous-titre, avec groupage par sous-titre
+
+mode "revue des changements de plan"
+- marquer certains changements de plan comme changement de scène, les montrer en rouge (chevauchement interdit), donner accès à cette propriété dans les plug-ins
+- possibilité de changer la sélection, et d'afficher des frames (avance frame par frame)
+
+possibilité de runner tous les plug-ins juste sur la sélection
+
+fix => re-run checks on current, prev and next + MAJ liste des erreurs

jsplugin/_hotkeys.txt

+Legend:
+(Win)	Standard Windows shortcut, must not be modified
+(VSS)	Default VSS shortcut
+(JST)	Default JS Tool shortcut
+(Thy)	Thyresias binding
+
+Ctrl+A (Win) Select All
+Ctrl+B
+Ctrl+C (Win) Copy
+Ctrl+D (VSS) Delay
+Ctrl+E (Thy) Check Errors
+Ctrl+F (Win) Find
+Ctrl+G (Win) Goto
+Ctrl+H
+Ctrl+I (VSS) Text in Italic
+Ctrl+J
+Ctrl+K
+Ctrl+L
+Ctrl+M
+Ctrl+N (Win) New Project
+Ctrl+O (Win) Open Project
+Ctrl+P (Thy) Preferences
+Ctrl+Q (JST) Quick Stats
+Ctrl+R (JST) Reading Speed Stats
+Ctrl+S (Win) Save
+Ctrl+T (VSS) Set Subtitle Time
+Ctrl+U (JST) Make Duration Ideal
+Ctrl+V (Win) Paste
+Ctrl+W (VSS) Add Subtitle
+Ctrl+X (Win) Cut
+Ctrl+Y (Win) Redo
+Ctrl+Z (Win) Undo
+
+F1 (VSS)    Play
+F2 (VSS)    Loop
+F3 (Win)    Find Next
+F4 (VSS)    Show/Hide Video
+F5 (VSS)    Play Previous Sub
+F6 (VSS)    Play Next Sub
+F7 (VSS)    Clear Selection
+
+Esc (VSS)   Stop

jsplugin/_readme.txt

+This directory contains JavaScript plugins for VisualSubSync.
+
+For more information about JavaScript check this links:
+http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Guide
+http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference
+http://developer.mozilla.org/en/docs/A_re-introduction_to_JavaScript
+
+
+VSS specific functions and properties:
+
+ScriptLog(Message) : Display a message in the log window
+SetStatusBarText(Message) : Display a message in the status bar
+LoadScript(Filename) : Load an external javascript file
+
+
+VSSCore.INDEX_COL_IDX : Index of the subtitle index column
+VSSCore.START_COL_IDX : Index of the start time column
+VSSCore.STOP_COL_IDX  : Index of the stop time colum
+VSSCore.STYLE_COL_IDX : Index of the style column (SSA/ASS only)
+VSSCore.TEXT_COL_IDX  : Index of the text column
+VSSCore.LAST_CORE_COL_IDX : Index of the last column of VSS core
+
+VSSCore.CpsTarget : Characters per second target (value set in the preferences dialog)
+VSSCore.MinimumDuration : Minimum subtitle duration (value set in the preferences dialog)
+VSSCore.MaximumDuration : Maximum subtitle duration (value set in the preferences dialog)
+VSSCore.MinimumBlank : Minimum blank between subtitles (value set in the preferences dialog)
+
+VSSCore.VideoWidth : The video width in pixel (0 if no video)
+VSSCore.VideoHeight : The video height in pixel (0 if no video)
+
+VSSCore.RegisterJavascriptAction(Name, Description, DefaultShortcut) : Register a javascript action
+VSSCore.GetSubCount() : Return the total number of subtitles
+VSSCore.GetSubAt(Index) : Return the subtitle at the specified index
+VSSCore.GetFirst() : Return the first subtitle
+VSSCore.GetNext(Subtitle) : Return the subtitle next to the specified one
+VSSCore.GetPrevious(Subtitle) : Return the subtitle previous to the specified one
+VSSCore.GetFirstSelected() : Return the first selected subtitle
+VSSCore.GetNextSelected(Subtitle) : Return the next selected subtitle after the specified one
+VSSCore.MeasureStringWidth(FontName, FontSize, IsBold, Text) : Return the width of the text in pixels.
+
+Subtitle.Index : Index of the subtitle
+Subtitle.Start : Start time in ms of the subtitle
+Subtitle.Stop : Stop time in ms of the subtitle
+Subtitle.Text : Subtitle's text
+Subtitle.StrippedText : Subtitle's text without any tags
+
+
+SceneChange.StartOffset : The offset in ms to keep before a scene change (value set in the preferences dialog)
+SceneChange.StopOffset : The offset in ms to keep after a scene change (value set in the preferences dialog)
+SceneChange.FilterOffset : The offset in ms from subtitle start and stop where scene change are filtered (value set in the preferences dialog)
+SceneChange.Visible : True if scene change are currenlty visible in VSS (value set in the preferences dialog)
+SceneChange.GetCount() : Get the total number of scene change
+SceneChange.GetAt(Index) : Get the time of the scene change at the specified index in ms. Index is between 0 and GetCount()-1
+SceneChange.GetNext(TimeMs) : Get the time in ms of the next scene change superior or equal to TimeMs
+SceneChange.GetPrevious(TimeMs) : Get the time in ms of the next scene change inferior or equal to TimeMs
+SceneChange.Contains(Start,Stop) : Check if there is a scene change between [Start,Stop]
+
+
+/*
+  Case control
+  thyresias at gmail dot com (www.calorifix.net)
+  26 Nov 2005
+  22 Dec 2008   Added ErrorOnAcronymPlural (request from linwelin dot sg at gmail dot com)
+*/
+
+var debugMode = false;
+
+function checkSub(CurrentSub, PreviousSub, errorOnAcronymPlural) {
+
+  var tagText = CurrentSub.Text;
+
+  // --- prepare untagged version, remember tags
+
+  // split at tags
+  var chunks = tagText.split(/<\/?\w>/g);
+
+  // get the tags
+  // tags[i] is the tag between chunks[i-1] and chunks[i]
+  var tags = new Array(chunks.length);
+  var index = chunks[0].length;
+  for (i = 1; i < chunks.length; i++) {
+    tags[i] = tagText.substr(index).replace(/(<\/?\w>)(.|[\r\n])*/m, "$1");
+    index = index + tags[i].length + chunks[i].length;
+  }
+
+  // stripped version
+  var text = tagText.replace(/<\/?\w>/g, "");
+
+  // debug
+  if (debugMode) {
+    ScriptLog("");
+    ScriptLog("tagText=[" + tagText.replace(/\r\n/mg, "|") + "]");
+    ScriptLog("   text=[" + text.replace(/\r\n/mg, "|") + "]");
+    for (i=0; i<chunks.length; i++)
+      ScriptLog("   " + i + " = " + (i>0 ? tags[i] : "") + "[" + chunks[i].replace(/\r\n/mg, "|") + "]");
+  }
+
+  // --- correct UPpercase errors like this
+
+  var re = errorOnAcronymPlural
+    ? /([A-Z��-��-�])([A-Z��-��-�]+)([a-z��-��-�]+)/mg
+    : /([A-Z��-��-�])([A-Z��-��-�]+)([a-rt-z��-��-�]|[a-z��-��-�]{2,})/mg
+  ;
+  var match = re.exec(text);
+  while (match) {
+    text =
+      text.substr(0, match.index) // before the match
+      + match[1]
+      + match[2].toLowerCase()
+      + match[3]
+      + text.substr(match.index + match[0].length) // after the match
+    ;
+    match = re.exec();
+  }
+
+  // --- uppercase after . ? !, except after ... and acronyms, bypassing double quotes
+
+  re = /([^.A-Z][.?!]"?\s+"?)([a-z��-��-�])/mg;
+
+  // prepend the last characters of previous subtitle
+  var prevText = PreviousSub == null ? "" : PreviousSub.StrippedText;
+  var prevLen = prevText.length;
+  var addText = "";
+  if (prevLen==0)     addText = "aaa. ";
+  else if (prevLen<3) addText = ("aaaa").substr(prevLen) + prevText + " ";
+  else                addText = "a" + prevText.substr(prevLen-3) + " ";
+  text = addText + text;
+
+  // debug: display matches
+  if (debugMode)
+    if (re.test(text)) {
+      ScriptLog("   text=[" + text.replace(/\r\n/mg, "|") + "]");
+      ScriptLog("   >>> [" + text.replace(re, "$1{$2}").replace(/\r\n/mg, "|") + "]");
+    }
+
+  // correct uppercase
+  match = re.exec(text);
+  while (match) {
+    text =
+      text.substr(0, match.index) // before the match
+      + match[1]
+      + match[2].toUpperCase()
+      + text.substr(match.index + match[0].length) // after the match
+    ;
+    match = re.exec();
+  }
+
+  // remove previous subtitle text
+  text = text.substr(addText.length);
+
+  // --- restore tags
+
+  index = chunks[0].length;
+  tagText = text.substr(0,index);
+  for (i = 1; i < chunks.length; i++) {
+    tagText += tags[i] + text.substr(index, chunks[i].length);
+    index = index + chunks[i].length;
+  }
+
+  if (debugMode)
+    ScriptLog("   tagText=[" + tagText.replace(/\r\n/mg, "|") + "]");
+
+  return tagText;
+
+}
+
+VSSPlugin = {
+
+  Name: "Case errors",
+  Color: 0x00D900, // green
+  Message: "Case",
+  Description: "Check uppercase versus punctuation.",
+
+  // --- parameters
+
+  Param_ErrorOnAcronymPlural: {
+    Value: 1, Unit: "1=Yes 0=No", Description:
+      "Consider trailing 's' on acronyms as case errors? (e.g., PAs > Pas)\r\n" +
+      "1: Yes (recommended).\r\n" +
+      "0: No (English only, tolerance)."
+  },
+
+  // --- error detection/fix
+
+  HasError: function(CurrentSub, PreviousSub, NextSub) {
+    var fix = checkSub(CurrentSub, PreviousSub, this.Param_ErrorOnAcronymPlural.Value != 0);
+    return fix == CurrentSub.Text ? "" : ">> " + fix.replace(/[\r\n]+/gm,"|");
+  },
+
+  FixError: function(CurrentSub, PreviousSub, NextSub) {
+    CurrentSub.Text = checkSub(CurrentSub, PreviousSub, this.Param_ErrorOnAcronymPlural.Value != 0);
+  }
+
+}

jsplugin/french_punctuation.js

+/*
+  French punctuation
+  original version by christophe dot paris at free dot fr
+  all changes below by thyresias at gmail dot com (www.calorifix.net)
+  26 Nov 2005  refactoring, tune rules for Calorifix
+  25 Feb 2007  Param_AllAtOnce=1 by default
+*/
+
+VSSPlugin = {
+
+  Name: " French punctuation", // space: check this first
+  Color: 0x008000, // dark green
+  Message: "Typo",
+  Description: "Check text against French punctuation rules.",
+
+  // --- parameters
+
+  // If AllAtOnce=true, this will look for all typo errors, and fix all of them at once.
+  // However, the message will not give all reasons.
+  Param_AllAtOnce: {
+    Value: 1, Unit: "1=Yes 0=No", Description:
+      "Report/fix all errors in a subtitle?\r\n" +
+      "0: Only the first detected error will be reported/fixed.\r\n" +
+      "1: All errors found will be reported/fixed."
+  },
+
+  // We use a table of rules to define the typography
+  // Each rule is defined by those field:
+  //   - re: a regular expression
+  //   - msg: a message to display when the text match
+  //   - replaceby (optional): a replace expression used to fix the error
+  //   - exception (optional): if set to true the processing will stop on
+  //     this rule and no replacement will be made (msg can be used for debugging)
+  Rules: new Array(
+    // espaces
+    { re: /[\t\v\f]/mg, msg: "Caract�re d'espacement interdit (Tab, VT, FF)", replaceby: " "},
+    { re: /^[ \u00A0\u2028\u2029]+/mg, msg: "Pas d'espace en d�but ligne", replaceby: ""},
+    { re: /[ \u00A0\u2028\u2029]+$/mg, msg: "Pas d'espace en fin de ligne", replaceby: ""},
+    { re: /[ \u00A0\u2028\u2029]{2,}/mg, msg: "Pas plus d'un espace cons�cutif", replaceby: " "},
+    // ponctuation multiple
+    { re: /([!:;.,]*\?[!:;?.,])|([!:;.,]+\?[!:;?.,]*)/mg, msg: "Pas de ponctuation multiple", replaceby: "?"}, // privil�gie le ?
+    { re: /([?!:;])[?!:;.,]+/mg, msg: "Pas de ponctuation multiple", replaceby: "$1"},
+    // points de suspension
+    { re: /([^.])\.\.([^.])/mg, msg: "Manque un point de suspension", replaceby: "$1...$2"},
+    { re: /([^.])\.\.$/mg, msg: "Manque un point de suspension", replaceby: "$1..."},
+    { re: /\.{4,}/mg, msg: "Trop de points de suspension", replaceby: "..."},
+    { re: /\.{3}\b/mg, msg: "Un espace apr�s ...", replaceby: "... "},
+    { re: /^\.[.]+[ \u00A0\u2028\u2029]*/mg, msg: "Pas de points de suspension en d�but de ligne", replaceby: ""},
+    // espaces et ponctuation
+    { re: /\s+([,.])/mg, msg: "Pas d'espace avant , et .", replaceby: "$1"},
+    { re: /(\w)([?!:;]+)/mg, msg: "Un espace avant ? : ; !", replaceby: "$1 $2"},
+    { re: /^-(\S)/mg, msg: "Un espace apr�s - en d�but de ligne", replaceby: "- $1"},
+    // apostrophe
+    { re: /''/mg, msg: "Double apostrophe", replaceby: "\""},
+    // apostrophes comme guillemets:
+    // - la 1e pr�c�d�e d'un espace ou en d�but de ligne, suivie d'une lettre ou d'un chiffre
+    // - la 2e non suivie d'une lettre ou en fin de ligne
+    { re: /(\s|^)'([A-Z����-��-�0-9a-z����-��-�][^'"]*)'([^A-Z����-��-�0-9a-z����-��-�]|$)/mg,
+        msg: "Pas d'apostrophe comme guillemet", replaceby: "$1\"$2\"$3"},
+    { re: /(\s')|('\s)/mg, msg: "Pas d'espace avant ni apr�s une apostrophe", replaceby: "'"}, // '
+    // guillemets
+    { re: /( |^)" +([^"]+) +"/mg, msg: "Pas d'espace apr�s et avant guillemets", replaceby: "$1\"$2\""},
+    { re: /(^" +)|( +"$)/mg, msg: "Pas d'espace apr�s et avant guillemets", replaceby: "\""},
+    { re: / +" +/mg, msg: "Pas d'espace apr�s et avant guillemets", replaceby: " \""},  // '
+    // nombres
+    { re: /([0-9]+)[,.]([0-9]{3})/mg, msg: "Espace comme s�parateur des milliers", replaceby: "$1\u00A0$2"},
+    { re: /([0-9]+)\.([0-9]{1,2})/mg, msg: "Virgule comme s�parateur d�cimal", replaceby: "$1,$2"},
+    // espaces apr�s . et ,
+    { re: /(http:\/\/[^\s\)]+)/mg, msg: "Ignorer les point dans les URL (1)", replaceby: "[url1=$1]", exception: true, },
+    { re: /(www.[^\s)]+)/mg, msg: "Ignorer les points dans les URL (2)", replaceby: "[url2=$1]", exception: true},
+    { re: /\b(([A-Z]\.){2,})\B/mg, msg: "Ignorer les points dans les acronymes", replaceby: "[acro=$1]", exception: true},
+    { re: /([0-9]+[.,][0-9]+)/mg, msg: "Ignorer points et virgules dans les nombres", replaceby: "[nombre=$1]", exception: true},
+    { re: /([.,])\b/mg, msg: "Un espace apr�s , et ,", replaceby: "$1 "}
+  ),
+
+  // --- error detection/fix
+
+  HasError: function(CurrentSub, PreviousSub, NextSub) {
+    var check = this.checkIt(CurrentSub);
+    if (check.msg != "")
+      return check.msg + " >> " + check.newText.replace(/[\r\n]+/gm,"|");
+    else
+      return "";
+  },
+
+  FixError: function(CurrentSub, PreviousSub, NextSub) {
+    var check = this.checkIt(CurrentSub);
+    if (check.msg != "")
+      CurrentSub.Text = check.newText;
+  },
+
+  // --- generic check
+
+  // because the g switch is used, replace MUST be executed if a match is found
+  checkIt: function(Sub) {
+
+    var tagText = Sub.Text;
+
+    // fix all: replace repeatedly, stopping at the first exception
+    if (this.Param_AllAtOnce.Value) {
+      var msg = "";
+      for (i=0; i < this.Rules.length; i++) {
+        if (this.Rules[i].re.test(tagText)) {
+          var fix = tagText.replace(this.Rules[i].re, this.Rules[i].replaceby);
+          if (this.Rules[i].exception) break;
+          tagText = fix;
+          if (msg == "")
+            msg = this.Rules[i].msg;
+          else if (msg != this.Rules[i].msg)
+            msg = "Ponctuation";
+        }
+      }
+      return { newText: tagText, msg: msg };;
+    }
+
+    // fix the first one: return the first modified text
+    else {
+      for (i=0; i < this.Rules.length; i++) {
+        if ((this.Rules[i].replaceby != null) && (this.Rules[i].re.test(tagText))) {
+          fix = tagText.replace(this.Rules[i].re, this.Rules[i].replaceby);
+          if (!this.Rules[i].exception)
+            return { newText: fix, msg: this.Rules[i].msg };
+        }
+      }
+      return { newText: tagText, msg: "" };
+    }
+
+  }
+
+}

jsplugin/general/action_ideal_duration.js

+/*
+  Change the stop time of the selected subtitle(s) to match the ideal reading speed.
+  The new duration will be at least VSSCore.MinimumDuration.
+
+  14 Nov 2008   thyresias at gmail dot com
+*/
+
+JSAction_SetIdealDuration = {
+  onExecute: function() {
+    for (var sub = VSSCore.GetFirstSelected(); sub; sub = VSSCore.GetNextSelected(sub)) {
+      var newStop = sub.Start + idealDuration(sub).exact_ms;
+      if (newStop - sub.Start < VSSCore.MinimumDuration)
+        newStop = sub.Start + VSSCore.MinimumDuration;
+      sub.Stop = newStop;
+    }
+  }
+};
+
+VSSCore.RegisterJavascriptAction('JSAction_SetIdealDuration', 'Make Duration Ideal', 'Ctrl+U');
+

jsplugin/general/action_quick_stats.js

+/*
+  Show quick stats:
+  - subtitle count
+  - average reading speed
+  - number of subtitles per reading speed rating
+
+  08 Dec 2008   modified by thyresias at gmail dot com
+*/
+
+function percentAsText(percent) {
+  // 0: a dash, no % sign
+  if (percent == 0) return "- ";
+  // round to integer
+  percent = Math.round(percent);
+  // if now zero, less than 0.5
+  if (percent == 0) return "<1%"; // don't add a decimal just for this case
+  return "" + percent + "%";
+}
+
+JSAction_QuickStats = {
+  onExecute: function() {
+
+    logTitle("Quick Stats");
+
+    var subCount = VSSCore.GetSubCount();
+    ScriptLog("    Subtitle Count: " + subCount);
+
+    if (subCount <= 0) return;
+
+    // array to store the results
+    var rsStats = new Array(RS_RATINGS.length);
+    for (var i = 0; i < rsStats.length; i++)
+      rsStats[i] = {obsCount: 0};
+
+    // loop on subtitles to collect stats
+    var sub = VSSCore.GetFirst();
+    var mean = 0;
+    var nvar = 0;  // N times the variance
+    var n = 0;          // subtitles with computable speed (duration > 500 ms)
+    var nInfinite = 0;  // unreadable subtitles (duration <= 500 ms)
+    for (var i = 1; sub; i++) {
+      var len = sub.StrippedText.length;
+      var dur = (sub.Stop - sub.Start) / 1000 - 0.5;
+      if (dur <= 0)
+        nInfinite++;
+      else {
+        n++;
+        var rs = len / dur;
+        var delta = rs - mean;
+        mean += delta / n;
+        nvar += delta * delta * (n - 1) / n;
+      }
+      rsStats[readingSpeedRating(sub).index].obsCount++;
+      sub = VSSCore.GetNext(sub);
+    }
+    var std = Math.sqrt(nvar/n);
+
+    // display the average reading speed
+    ScriptLog("    Average Reading Speed: " + round1(mean) + " c/s (pro: 18 to 20)");
+    ScriptLog("    Standard Deviation:    " + round1(std) + " c/s (pro: 4 to 5)");
+    ScriptLog("    Unreadable Subtitles:  " + nInfinite + " (pro: 0)");
+
+    // Compute % and expected values.
+    // Find the longest text in each display column,
+    // and the max number of subtitles in a category
+    var xSym = 0;
+    var xText = 0;
+    var xPercent = 3;  // 99% or <1%
+    var xObsCount = 0;
+    var xExpCount = 0;
+    var maxObsCount = 0;
+    var maxExpCount = 0;
+    for (var i = 0; i < RS_RATINGS.length; i++) {
+      var rs = RS_RATINGS[i];
+      var obsCount = rsStats[i].obsCount;
+      var obsPercent = obsCount / subCount * 100;
+      var expCount = Math.round(rs.proba * subCount);
+      var expPercent = rs.proba * 100;
+      rsStats[i].obsPercent = obsPercent;
+      rsStats[i].expCount = expCount;
+      rsStats[i].expPercent = expPercent;
+      // text lengths
+      if (rs.symbol.length > xSym) xSym = rs.symbol.length;
+      if (rs.text.length > xText) xText = rs.text.length;
+      // count lengths
+      var len = ("" + obsCount).length;
+      if (len > xObsCount) xObsCount = len;
+      len = ("" + expCount).length;
+      if (len > xExpCount) xExpCount = len;
+      // max counts
+      if (obsCount > maxObsCount) maxObsCount = obsCount;
+      if (expCount > maxExpCount) maxExpCount = expCount;
+    }
+
+    // space left for bars
+    var left = MAX_LOG_WIDTH - (xSym + 1 + xText+4 + 1 + xPercent + 1+xObsCount + 1+0 + 1 + xPercent + 1+xExpCount + 1+0);
+    var xObsBar = Math.ceil(left/2);
+    var xExpBar = Math.floor(left/2);
+
+    ScriptLog(
+      padRight("", xSym, "-") + " " +
+      padRight("Rating ", xText+4, "-") + " " +
+      padRight("--- Observed ", xPercent + 1+xObsCount + 1+xObsBar, "-") + " " +
+      padRight("--- Expected ", xPercent + 1+xExpCount + 1+xExpBar, "-")
+    );
+
+    // display the stats by rating
+    for (var i = 0; i < RS_RATINGS.length; i++) {
+      var rs = RS_RATINGS[i];
+      // symbol, text & leading dots
+      var text = padLeft(rs.symbol, xSym, " ") + " " + padRight(rs.text + " ", xText+4, ".");
+      // observed percentage, count & bar
+      text += padLeft(percentAsText(rsStats[i].obsPercent), 1+xPercent, " ");
+      text += padLeft("" + rsStats[i].obsCount, 1+xObsCount, " ");
+      var barLength = Math.round(xObsBar / maxObsCount * rsStats[i].obsCount)
+      text += " " + padRight("", barLength, OBS);
+      text += padRight("", xObsBar - barLength, " ");
+      // observed percentage, count & bar
+      text += padLeft(percentAsText(rsStats[i].expPercent), 1+xPercent, " ");
+      text += padLeft("" + rsStats[i].expCount, 1+xExpCount, " ");
+      var barLength = Math.round(xExpBar / maxExpCount * rsStats[i].expCount)
+      text += " " + padRight("", barLength, EXP);
+      // output the line
+      ScriptLog(text);
+    }
+
+  }
+};
+
+VSSCore.RegisterJavascriptAction('JSAction_QuickStats', 'Quick stats', 'Ctrl+Q');

jsplugin/general/action_rs_stats.js

+/*
+  Display an histogram of reading speeds.
+
+  14 Nov 2008   thyresias at gmail dot com
+*/
+
+JSAction_ReadingSpeedStats = {
+  onExecute: function() {
+
+    if (VSSCore.GetSubCount() == 0) return;
+
+    logTitle("Reading Speed Statistics");
+    LoadScript("statistics.js");
+
+    // collect frequency for each rounded reading speed
+    var mean = 0;
+    var std = 0;
+    var freqs = [];
+    for (var sub = VSSCore.GetFirst(); sub; sub = VSSCore.GetNext(sub)) {
+      var rs = readingSpeed(sub).exact_per_ms * 1000;
+      var i = Math.round(rs);
+      if (freqs[i]) freqs[i]++;
+      else          freqs[i] = 1;
+    }
+
+    // display them
+    histogram(freqs, RS_MEAN, RS_STD, "Distribution of Reading Speed (pro: between 5 and 35)", RS_MAX);
+
+  }
+};
+
+VSSCore.RegisterJavascriptAction('JSAction_ReadingSpeedStats', 'Reading speed stats', 'Ctrl+R');

jsplugin/general/general_plugin.js

+/*
+  General plugin:
+  - status bar display
+  - definition of additional columns
+  - load javascript custom actions
+  this file MUST be saved as UTF8 with BOM
+
+  20-Jan-2007   thyresias at gmail dot com (www.calorifix.net)
+                first version
+  2007-2008     Christophe Paris / Nathbot (TBC)
+                updates for new VSS features
+  14-Nov-2008   thyresias
+                refactoring, new Rating column, color based on actual reading speed
+*/
+
+LoadScript("tools.js");
+
+// ---------------------------------------------------------------------------
+//  status bar text
+// ---------------------------------------------------------------------------
+
+// returns a bar of (max - min + 1) dots,
+// so the first dot is min, the last dot is max
+function initBar(min, max) {
+  var bar = "·····················································";
+  var length = max - min + 1;
+  while (bar.length < length)
+    bar = bar + bar;
+  return bar.substr(0, length);
+}
+
+// template bar for reading speeds
+var TEMPLATE_BAR = initBar(RS_MIN, RS_MAX);
+
+// returns a bar displaying "value" between min and max
+// for a 10-dot bar representing values from 1 to 10,
+// "value" will be rounded, and then marked on the bar:
+// value < 1   =>  «··········
+// value > 10  =>   ··········»
+// value = 3   =>   ··¦·······
+function getBar(value, min, max, templateBar) {
+  // ~{|}[\]^_`:<=>-+!#%&‘’¡¤§«¬­¯°±´º»
+  var iVal = Math.round(value);
+  // below min: «·························
+  if (iVal < min)
+    return "«" + templateBar;
+  // above max: ·························»
+  else if (iVal > max)
+    return templateBar + "»";
+  // in the range
+  else {
+    iVal = iVal - min;
+    return templateBar.substr(0, iVal-1) + "¦" + templateBar.substr(iVal+1);
+  }
+}
+
+// returns the text for the status bar
+function statusBarText(Sub) {
+
+  // values displayed
+  var rs = readingSpeed(Sub).rounded_per_s;
+  var rating = readingSpeedRating(Sub);
+  var dur = duration(Sub).rounded_s;
+  var ideal = idealDuration(Sub).rounded_s;
+
+  // bar displayed
+  var barRS = getBar(rs, RS_MIN, RS_MAX, TEMPLATE_BAR);
+
+  // symbol versus ideal duration
+  var sym;
+  if (dur < ideal)      sym = "<";
+  else if (dur > ideal) sym = ">";
+  else                  sym = "=";
+
+  return "Reading Speed: " + rs + " " + barRS +
+    "  |  Duration: " + dur + " " + sym + " ideal=" + ideal +
+    "  |  " + rating.text;
+
+}
+
+// ---------------------------------------------------------------------------
+//  general plugin
+// ---------------------------------------------------------------------------
+
+VSSPlugin = {
+
+  // Called on subtitle modifications (time or text)
+  OnSubtitleModification: function(CurrentSub, PreviousSub, NextSub) {
+    SetStatusBarText(statusBarText(CurrentSub));
+  },
+
+  // Called when the selected subtitle change
+  OnSelectedSubtitle: function(CurrentSub, PreviousSub, NextSub) {
+    SetStatusBarText(statusBarText(CurrentSub));
+  },
+
+  // Called when the WAV subtitle start is double-clicked
+  // => try to fix delay to previous, if needed
+  OnRangeStartDblClick : function(CurrentSub, PreviousSub, NextSub) {
+
+    if (PreviousSub == null) return;
+    var delayToPrevious = CurrentSub.Start - PreviousSub.Stop;
+
+    // nothing to do if everything is OK
+    if (delayToPrevious >= VSSCore.MinimumBlank) return;
+
+    // can't fix if there is less room than twice the min duration plus the minimum delay
+    if (CurrentSub.Stop - PreviousSub.Start < 2 * VSSCore.MinimumDuration + VSSCore.MinimumBlank) return;
+
+    // if some jerk set the VSSCore parameters to stupid values, check for reasonable ones
+    if (NextSub.Stop - CurrentSub.Start < 1002) return;
+
+    var newStart = PreviousSub.Stop + VSSCore.MinimumBlank;
+    if (newStart < CurrentSub.Stop) CurrentSub.Start = newStart;
+
+  },
+
+  // Called when the WAV subtitle end is double-clicked
+  // => try to set the ideal duration, preserving the delay to the next subtitle
+  OnRangeStopDblClick : function(CurrentSub, PreviousSub, NextSub) {
+
+    // ideal stop
+    var idealStop = CurrentSub.Start + idealDuration(CurrentSub).exact_ms;
+
+    // if shortening, everything is OK
+    if (idealStop <= CurrentSub.Stop) {
+      CurrentSub.Stop = idealStop;
+      return;
+    }
+
+    // if no next subtitle, we can't check versus video end...
+    if (NextSub == null) {
+      CurrentSub.Stop = idealStop;
+      return;
+    }
+
+    // check delay to next
+    var delayToNext = NextSub.Start - idealStop;
+    if (delayToNext >= VSSCore.MinimumBlank) {
+      CurrentSub.Stop = idealStop;
+      return;
+    }
+
+    // we can't set the ideal duration, because the subtitle would be too close to the next one
+    var newStop = NextSub.Start - VSSCore.MinimumBlank;
+    if (newStop > CurrentSub.Stop) {
+      CurrentSub.Stop = newStop;
+      return;
+    }
+
+    // we cannot preserve the delay to the next subtitle: at least avoid overlapping
+    if (CurrentSub.Stop >= NextSub.Start) {
+      CurrentSub.Stop = NextSub.Start - 1;
+      return;
+    }
+
+    // we can't do anything
+
+  },
+
+
+  // COLUMNS -----------------------------------------------------------------
+
+  // VSS core column indices:
+  // VSSCore.INDEX_COL_IDX      subtitle number
+  // VSSCore.START_COL_IDX      start time
+  // VSSCore.STOP_COL_IDX       stop time
+  // VSSCore.STYLE_COL_IDX      style (displayed for SSA/ASS only)
+  // VSSCore.TEXT_COL_IDX       text
+  // VSSCore.LAST_CORE_COL_IDX  index of the last core VSS column
+
+  // Declare extra column index here
+  RS_COL_IDX: VSSCore.LAST_CORE_COL_IDX + 1,     // Reading Speed
+  RATING_COL_IDX: VSSCore.LAST_CORE_COL_IDX + 2, // RS rating
+
+  // --- functions called only at VSS startup
+
+  // number of extra-columns
+  GetExtraColumnsCount: function() {
+    return 2;
+  },
+
+  // title of each extra-column
+  GetColumnTitle: function(Index) {
+    switch(Index) {
+      case this.RS_COL_IDX:     return 'RS';
+      case this.RATING_COL_IDX: return 'Rating';
+      default:                  return '';
+    }
+  },
+
+  // size of each extra-column
+  GetColumnSize: function(Index) {
+    switch(Index) {
+      case this.RS_COL_IDX:     return 40;
+      case this.RATING_COL_IDX: return 50;
+      default:                  return 0;
+    }
+  },
+
+  // is the column background colorized?
+  IsColumnBGColorized: function(Index) {
+    switch(Index) {
+      case this.RS_COL_IDX:     return true;
+      case this.RATING_COL_IDX: return true;
+      default:                  return false;
+    }
+  },
+
+  // column has custom text?
+  HasColumnCustomText: function(Index) {
+    switch(Index) {
+      case this.RS_COL_IDX:     return true;
+      case this.RATING_COL_IDX: return true;
+      default: return false;
+    }
+  },
+
+  // --- functions called on each cell repaint
+
+  // cell background color
+  GetColumnBGColor: function(Index, CurrentSub, PreviousSub, NextSub) {
+    switch(Index) {
+      case this.RS_COL_IDX:     return readingSpeedColor(CurrentSub);
+      case this.RATING_COL_IDX: return readingSpeedColor(CurrentSub);
+      default:                  return 0xFFFFFF; // white
+    }
+  },
+
+  // cell text
+  GetColumnText: function(Index, CurrentSub, PreviousSub, NextSub) {
+    switch(Index) {
+      case this.RS_COL_IDX:
+        var rs = "" + readingSpeed(CurrentSub).rounded_per_s;
+        if (rs == "Infinity") rs = "Unreadable";
+        return rs;
+      case this.RATING_COL_IDX:
+        return "" + readingSpeedRating(CurrentSub).symbol;
+      default:
+        return "";
+    }
+  }
+
+};
+
+// ---------------------------------------------------------------------------
+//  Load javascript actions
+// ---------------------------------------------------------------------------
+
+LoadScript('action_*.js');

jsplugin/general/statistics.js

+/*
+  Statistics on subtitles
+  thyresias at gmail dot com (www.calorifix.net)
+  14 Jan 2006
+  27 Apr 2006  added selection of histograms
+  20 Jan 2007  used "display/reading speed" wording
+  14 Nov 2008  modified for new fixed-pitch log font, removed display speed
+*/
+
+// Array.max()
+Array.prototype.max = function() {
+  var max = this[0];
+  for (var i = 1; i < this.length; i++)
+    if (this[i] > max) max = this[i];
+  return max;
+}
+
+// Array.nz(valueIfNull)
+// converts all undefined/null values to the passed value
+Array.prototype.nz = function(valueIfNull) {
+  var undef;
+  for (var i = 0; i < this.length; i++)
+    if (this[i] == null || this[i] == undef)
+      this[i] = valueIfNull;
+}
+
+// Draw a histogram comparing an observed distribution with an expected one.
+// The expected distribution follows a gaussian law
+// with the given mean and standard deviation.
+function histogram(obsFreqs, mean, std, title, max) {
+
+  var subCount = VSSCore.GetSubCount();
+
+  // convert nulls to zero
+  obsFreqs.nz(0);
+
+  // compute and store expected frequencies
+  var expFreqs = [];
+  var nonZeroFound = false;
+  for (var i = 0; ; i++) {
+    // expected frequency for a value between i-0.5 (excluded) and i+0.5 (included)
+    var f = Math.round((ncdf((i+0.5 - mean)/std) - ncdf((i-0.5 - mean)/std)) * subCount);
+    // if we already found a non-zero frequency, we are looking for the moment where
+    // the expected frequency reaches 0 and we are after the last observed value.
+    if (nonZeroFound) {
+      if (f == 0 && i >= obsFreqs.length) break;
+    }
+    // if we did not, we are at the beginning of the distribution,
+    // and we look for the first non-zero frequency
+    else if (f > 0)
+      nonZeroFound = true;
+    expFreqs[i] = f;
+  }
+
+  // find the lowest value with a non-zero frequency (observed or expected)
+  var iMin;
+  for (iMin = 0; iMin < obsFreqs.length; iMin++)
+    if (obsFreqs[iMin] || expFreqs[iMin]) break;
+
+  // find the highest value with a non-zero frequency (observed or expected)
+  var iMax = obsFreqs.length;
+  if (expFreqs.length > iMax) iMax = expFreqs.length;
+
+  // adjust max to the next 10 multiple, correct if too high
+  var iMaxRegular = Math.ceil(max/10) * 10;
+  if (iMaxRegular > iMax) iMaxRegular = iMax;
+
+  // find the highest frequency
+  var maxObs = obsFreqs.max();
+  var maxExp = expFreqs.max();
+  var maxFreq = maxObs > maxExp ? maxObs : maxExp;
+  var maxLen = MAX_LOG_WIDTH - 13; // xx [bar] (xxx) xxx
+
+  // display title & legend
+  ScriptLog("    " + title);
+  ScriptLog("    Legend:  " + padRight("", 3, OBS) + " Observed (number at end of bar)");
+  ScriptLog("             " + padRight("", 3, EXP) + " Pro (number between parentheses)");
+
+  // display histogram between min & regular max
+  for (var i = iMin; i < iMaxRegular; i++) {
+    var xText = i < 10 ? " " + i : "" + i;
+    var expLen = Math.round(expFreqs[i] * maxLen / maxFreq);
+    var obsLen = Math.round(obsFreqs[i] * maxLen / maxFreq);
+    var bar, endText;
+    if (expLen < obsLen) {
+      endText = "(" + expFreqs[i] + ") " + obsFreqs[i];
+      var obsLen = Math.round((obsFreqs[i] - expFreqs[i]) * maxLen / maxFreq);
+      bar = padRight("", expLen, EXP) + padRight("", obsLen, OBS);
+    }
+    else {
+      endText = "" + obsFreqs[i] + " (" + expFreqs[i] + ")";
+      var expLen = Math.round((expFreqs[i] - obsFreqs[i]) * maxLen / maxFreq);
+      bar = padRight("", obsLen, OBS) + padRight("", expLen, EXP);
+    }
+    ScriptLog( xText + " " + bar + " " + endText );
+  }
+
+  // display outliers
+  for (var iLow = iMaxRegular; iLow < iMax; ) {
+    var iHigh = iLow < 100 ? iLow + 10 : iMax + 1;
+    var n = 0;
+    for (i = iLow; i < iHigh; i++)
+      if (obsFreqs[i])
+        n += obsFreqs[i];
+    if (n > 0) {
+      len = Math.round(n * maxLen / maxFreq);
+      ScriptLog( iLow + "-" + (iHigh-1) + " " + padRight("", len, OBS) + " (0) " + n );
+    }
+    iLow = iHigh;
+  }
+
+}

jsplugin/general/tools.js

+/*
+  Tools used by other plugins
+  this file MUST be saved as UTF8 with BOM
+
+  14 Nov 2008   refactored by thyresias at gmail dot com (www.calorifix.net)
+*/
+
+// ---------------------------------------------------------------------------
+//  Reading speed definitions
+// ---------------------------------------------------------------------------
+
+// reading speeds must be compared after rounding to 1 decimal (to match what the user sees)
+var RS_MIN = 5;   // min authorized speed
+var RS_MAX = 35;  // max authorized speed
+
+// parameters for a "pro" distribution
+var RS_MEAN = 20;
+var RS_STD = 4.6;
+
+// note: max is excluded from the rating range, min is included
+// these objects are updated at the end of this file,
+// once the ncdf() function has been defined.
+var RS_RATINGS = [
+  { index: 0, max_per_s: 5,        symbol: '!---', text: 'TOO SLOW!'},
+  { index: 1, max_per_s: 10,       symbol: '---',  text: 'Slow, acceptable.'},
+  { index: 2, max_per_s: 13,       symbol: '--',   text: 'A bit slow.'},
+  { index: 3, max_per_s: 15,       symbol: '-',    text: 'Good.'},
+  { index: 4, max_per_s: 23,       symbol: '=',    text: 'Perfect.'},
+  { index: 5, max_per_s: 27,       symbol: '+',    text: 'Good.'},
+  { index: 6, max_per_s: 31,       symbol: '++',   text: 'A bit fast.'},
+  { index: 7, max_per_s: 35.1,     symbol: '+++',  text: 'Fast, acceptable.'},
+  { index: 8, max_per_s: Infinity, symbol: '!+++', text: 'TOO FAST!'}
+];
+
+// characters for observed & expected on histograms
+var OBS = "■"; // ░ ▒ ▓ █
+var EXP = "□"; // ■ □
+
+// max width of output in the log window
+var MAX_LOG_WIDTH = 100;
+
+// ---------------------------------------------------------------------------
+//  Utility objects
+// ---------------------------------------------------------------------------
+
+function Duration(milliseconds) {
+  this.exact_ms = Math.round(milliseconds);
+  this.rounded_s = round1(milliseconds / 1000);
+}
+
+function Speed(length, duration_ms) {
+  this.exact_per_ms = length / duration_ms;
+  this.rounded_per_s = round1(this.exact_per_ms * 1000);
+}
+
+// HLS object for the color with the specified RGB color (integer).
+// The hue is in the range [0, 360[
+// The saturation & lightness are in the range [0, 1]
+function HLSColor(rgb) {
+
+  var red   = (rgb & 0xFF0000) >> 16;
+  var green = (rgb & 0x00FF00) >> 8;
+  var blue  = (rgb & 0x0000FF);
+
+  var r = red / 255;
+  var g = green / 255;
+  var b = blue / 255;
+
+  // lightness
+  var max = r > g ? r : g;
+  if (b > max) max = b;
+  var min = r < g ? r : g;
+  if (b < min) min = b;
+
+  var l = (max + min) / 2;
+
+  // hue & saturation
+  var h, s;
+
+  if (max == min) {
+    // red = green = blue: a shade of grey
+    s = 0;
+    h = 0; // actually undefined
+  }
+  else {
+    var delta = max - min;
+    // saturation
+    if (l <= 0.5)   s = delta / (max + min);
+    else            s = delta / (2 - max - min);
+    // hue
+    if (r == max)       h = (g - b) / delta;
+    else if (g == max)  h = (b - r) / delta + 2;
+    else                h = (r - g) / delta + 4;
+    h *= 60;
+    if (h < 0) h += 360;
+    if (h >= 360) h -= 360;
+  }
+
+  this.hue = h;
+  this.lightness = l;
+  this.saturation = s;
+
+}
+
+// Returns the RGB color (integer) of an HLS color object.
+HLSColor.prototype.toRGB = function() {
+  var s = this.saturation;
+  var l = this.lightness;
+  var r, g, b;
+  if (s == 0) {
+    r = Math.round(l * 255);
+    g = Math.round(l * 255);
+    b = Math.round(l * 255);
+  }
+  else {
+    var v2 = l < 0.5 ? l * (1 + s) : l + s - s * l;
+    var v1 = 2 * l - v2;
+    var h = this.hue;
+    r = hrgb(v1, v2, h + 120);
+    g = hrgb(v1, v2, h);
+    b = hrgb(v1, v2, h - 120);
+  }
+  return (r << 16) | (g << 8) | b;
+}
+
+// utility function
+function hrgb(v1, v2, v3) {
+  v3 /= 360;
+  if (v3 < 0) v3 += 1;
+  if (v3 > 1) v3 -= 1;
+  var c;
+  if (6*v3 < 1)       c = v1 + (v2 - v1) * 6 * v3;
+  else if (2*v3 < 1)  c = v2;
+  else if (3*v3 < 2)  c = v1 + (v2 - v1 ) * (2/3 - v3) * 6;
+  else                c = v1;
+  return Math.round(c * 255);
+}
+
+// ---------------------------------------------------------------------------
+//  Global functions
+// ---------------------------------------------------------------------------
+
+// Round a number to 1 decimal
+// e.g., 1.23 => 1.2
+function round1(aValue) {
+  return Math.round(aValue * 10) / 10;
+}
+
+// pad a string on the right up to the given length, with the given character
+function padRight(string, length, pad) {
+  while (string.length < length)
+    string += pad;
+  return string;
+}
+
+// pad to the left (right-align)
+function padLeft(string, length, pad) {
+  while (string.length < length)
+    string = pad + string;
+  return string;
+}
+
+// output title in the log window
+function logTitle(text) {
+  ScriptLog(padRight("=== " + text + " ", MAX_LOG_WIDTH, "="));
+}
+
+// Returns the duration of the subtitle as a Duration object
+function duration(sub) {
+  return new Duration(sub.Stop - sub.Start);
+}
+
+// Returns the reading speed for the subtitle as a Speed object
+function readingSpeed(sub) {
+  var readingTime = sub.Stop - sub.Start - 500;
+  if (readingTime <= 0) return new Speed(1, 0); // +Infinity
+  return new Speed(sub.StrippedText.length, readingTime);
+}
+
+// Returns the reading speed rating for this subtitle.
+function readingSpeedRating(sub) {
+  var rs = readingSpeed(sub).rounded_per_s;
+  for (var i = 0; i < RS_RATINGS.length-1; i++)
+    if (rs < RS_RATINGS[i].max_per_s)
+      return RS_RATINGS[i];
+  return RS_RATINGS[RS_RATINGS.length - 1];
+}
+
+// Returns the reading speed as an RGB color
+// (the same HLS object is reused between calls)
+
+var RS_COLOR = new HLSColor(0x99FF99);  // light green: sets the saturation & lightness
+var HUE_FOR_MIN = 240;  // blue
+var HUE_FOR_MAX = 0;    // red
+var HUE_FACTOR = (HUE_FOR_MAX - HUE_FOR_MIN) / (RS_MAX - RS_MIN);
+
+function readingSpeedColor(sub) {
+  var rs = readingSpeed(sub).exact_per_ms * 1000;
+  if (rs < RS_MIN) rs = RS_MIN;
+  else if (rs > RS_MAX) rs = RS_MAX;
+  RS_COLOR.hue = Math.round(HUE_FOR_MIN + (rs - RS_MIN) * HUE_FACTOR);
+  return RS_COLOR.toRGB();
+}
+
+// Returns the "ideal" duration for a subtitle as a Duration object.
+// It corresponds to the ideal reading speed (pro is 17-18) set in the UI.
+function idealDuration(sub) {
+  return new Duration(Math.round(500 + sub.StrippedText.length / VSSCore.CpsTarget * 1000));
+}
+
+// Normal Cumulative Distribution Function
+// Returns Probability(X <= z), where X follows a normal (gaussian) law
+// with mean = 0 and standard deviation = 1.
+function ncdf(z) {
+
+  const p0 = 220.2068679123761;
+  const p1 = 221.2135961699311;
+  const p2 = 112.0792914978709;
+  const p3 = 33.91286607838300;
+  const p4 = 6.373962203531650;
+  const p5 = .7003830644436881;
+  const p6 = .3526249659989109E-01;
+
+  const q0 = 440.4137358247522;
+  const q1 = 793.8265125199484;
+  const q2 = 637.3336333788311;
+  const q3 = 296.5642487796737;
+  const q4 = 86.78073220294608;
+  const q5 = 16.06417757920695;
+  const q6 = 1.755667163182642;
+  const q7 = .8838834764831844E-1;
+
+  const cutoff = 7.071;
+  const root2pi = 2.506628274631001;
+
+  // |z| > 37
+
+  if (z > 37.0)  return 1;
+  if (z < -37.0) return 0;
+
+  // |z| <= 37.
+
+  var zabs = Math.abs(z);
+  var expn = Math.exp(-0.5 * zabs * zabs);
+  var pdf = expn / root2pi;
+  var p;
+
+  // |z| < cutoff = 10/sqrt(2).
+  if (zabs < cutoff)
+    p = expn *
+       ((((((p6  * zabs + p5) * zabs + p4) * zabs + p3) * zabs + p2) * zabs + p1) * zabs + p0) /
+       (((((((q7*zabs + q6) * zabs + q5) * zabs + q4) * zabs + q3) * zabs + q2) * zabs + q1) * zabs + q0);
+  else
+    p = pdf / (zabs + 1 / (zabs + 2 / (zabs + 3 / (zabs + 4 / (zabs + 0.65)))));
+
+  if (z < 0)  return p;
+  else        return 1 - p;
+
+}
+
+// ---------------------------------------------------------------------------
+//  Update reading speed ratings
+// ---------------------------------------------------------------------------
+
+function normalized(rs) {
+  return (rs - RS_MEAN) / RS_STD;
+}
+
+function addRSProbas() {
+  var prev = 0;
+  // Probability(min < RS < max)
+  for (var i = 0; i < RS_RATINGS.length - 1; i++) {
+    var cur = ncdf(normalized(RS_RATINGS[i].max_per_s));
+    RS_RATINGS[i].proba = cur - prev;
+    prev = cur;
+  }
+  // Probability(RS > RS_MAX)
+  RS_RATINGS[RS_RATINGS.length-1].proba = 1 - prev;
+  // move "too slow" & "too fast" to accepted values
+  RS_RATINGS[1].proba += RS_RATINGS[0].proba;
+  RS_RATINGS[0].proba = 0;
+  RS_RATINGS[RS_RATINGS.length-2].proba += RS_RATINGS[RS_RATINGS.length-1].proba;
+  RS_RATINGS[RS_RATINGS.length-1].proba = 0;
+}
+
+addRSProbas();

jsplugin/line_count_length.js

+/*
+  Too many lines, too long line
+  original version by christophe dot paris at free dot fr
+  all changes below by thyresias at gmail dot com (www.calorifix.net)
+  26 Nov 2005  refactoring, added autofix with tags
+  11 Mar 2006  added joining subtitles that fit on one line
+  27 Apr 2006  added switch for "fits on 1 line"
+  14 Nov 2008  renamed to line_count_length
+*/
+
+var debugMode = false;
+
+VSSPlugin = {
+
+  Name: "Line count and line length",
+  Color: 0x16C9B7, // turquoise
+  Message: "Line count/length",
+  Description:
+    "At most 2 lines, of length less than or equal to MaxLineLength (pro: 36).\r\n" +
+    "The autofix will first reduce the number of lines, " +
+    "then limit the number of characters per line (if possible). " +
+    "Dialogs are not processed.",
+
+  // --- parameters
+
+  Param_MaxLineLength: { Value: 40, Unit: "Characters", Description: "Maximum line length (36 pro, 40 max)." },
+  Param_CheckOneLine: { Value: 1, Unit: "1=Yes 0=No", Description: "Report/fix subtitles on several lines that can fit on one?" },
+
+  // --- error detection/fix
+
+  HasError: function(CurrentSub, PreviousSub, NextSub) {
+    var lines = CurrentSub.StrippedText.split("\r\n");
+    var nLines = lines.length;
+    var maxChars = 0;
+    var oneLineChars = 0;
+    for (i = 0; i < nLines; i++) {
+      nChars = lines[i].length;
+      oneLineChars = oneLineChars + (oneLineChars > 0 ? 1 : 0) + nChars;
+      if (nChars > maxChars)
+        maxChars = nChars;
+    }
+    var msg = nLines > 2 ? nLines + " lines" : "";
+    if (maxChars > this.Param_MaxLineLength.Value)
+      msg += (msg == "" ? "" : ", ") + maxChars + " characters";
+    else if (this.Param_CheckOneLine.Value != 0 && nLines > 1 && msg == "" && oneLineChars <= this.Param_MaxLineLength.Value && !CurrentSub.StrippedText.match(/^-/m))
+      msg = "fits on one line, " + oneLineChars + " characters";
+    return msg;
+  },
+
+  FixError: function(CurrentSub, PreviousSub, NextSub) {
+
+    if (debugMode) {
+      ScriptLog("");
+      ScriptLog("CurrentSub.Text=[" + CurrentSub.Text + "]");
+    }
+
+    // ignore dialogs
+    if (CurrentSub.StrippedText.match(/^-/m)) {
+      if (debugMode) ScriptLog("Dialog detected, exiting.");
+      return;
+    }
+
+    // remove CR, LF, multiple spaces
+    var tagText = CurrentSub.Text.replace(/\s+/mg, " ");
+    tagText = tagText.replace(/(^\s)|(\s$)/g, "");
+
+    if (debugMode) ScriptLog("tagText=[" + tagText + "]");
+
+    // if the text is now ok, exit
+    var text = tagText.replace(/<\/?\w>/g, "");
+    if (text.length <= this.Param_MaxLineLength.Value) {
+      if (this.Param_CheckOneLine.Value != 0) {
+        CurrentSub.Text = tagText;
+        if (debugMode) ScriptLog("Text fits on 1 line.");
+      }
+      return;
+    }
+    if (debugMode) ScriptLog("text=[" + text + "]");
+
+    // mark non-breaking spaces
+    tagText = tagText.replace(/\s([!?:;])/g, "\u00A0$1");
+    text = text.replace(/\s([!?:;])/g, "\u00A0$1");
+
+    // split at spaces
+    var tagWords = tagText.split(" ");
+    var words = text.split(" ");
+    if (debugMode)
+      for (var i = 0; i < words.length; i++)
+        ScriptLog("(tag)words[" + i + "]=[" + tagWords[i] + "] / [" + words[i] + "]");
+
+    // mid and max line length
+    var textLen = text.length;
+    var midLen = textLen / 2;
+    var maxLen = midLen;
+    if (maxLen < this.Param_MaxLineLength.Value) maxLen = this.Param_MaxLineLength.Value;
+
+    // compute break index
+    // 1. prefer cut
+    //    1. after ! ? .
+    //    2. after ... ;
+    //    3. after ,
+    //    4. after ) or before (
+    //    5. after other punctuation
+    //    6. before other punctuation
+    //    7. after letter
+    //    8. after digit
+    // 2. prefer to cut near the middle of the text
+    // 3. prefer a shorter first line
+    // 4. prefer both lines <= maxLen
+
+    var lenSoFar = words[0].length;
+    var fact = textLen > 10 ? textLen : 10;
+    var iCut = 0;
+    var maxBreakValue = 0;
+    for (var i = 1; i < words.length; i++) {
+      var prevWord = words[i-1];
+      var nextWord = words[i];
+      // criterion 1
+      var punct;
+      if (prevWord.match(/(\.\.\.$)|(;$)/))                     punct = 2;
+      else if (prevWord.match(/[!?.]$/))                        punct = 1;
+      else if (prevWord.match(/,$/))                            punct = 3;
+      else if (prevWord.match(/\)$/) || nextWord.match(/^\(/))  punct = 4;
+      else if (prevWord.match(/[^A-Z��-��-�0-9a-z��-��-�]$/))   punct = 5;
+      else if (nextWord.match(/^[^A-Z��-��-�0-9a-z��-��-�]/))   punct = 6;
+      else if (prevWord.match(/[0-9]$/))                        punct = 8;
+      else                                                      punct = 7;
+      // criterion 2
+      var distMid = Math.abs(midLen - lenSoFar);
+      // criterion 3
+      var firstShorter = lenSoFar < midLen ? 1 : 0;
+      // criterion 4
+      var lessMax = lenSoFar > maxLen || (textLen-lenSoFar-1) > maxLen ? 0 : 1;
+      // 4 > 1 > 2 > 3
+      breakValue = lessMax*fact*fact + (10-punct)*fact + firstShorter + (textLen-distMid)/fact;
+      if (debugMode) ScriptLog(
+        " lessMax=" + lessMax +
+        " punct=" + punct +
+        " firstShorter=" + firstShorter +
+        " distMid=" + distMid +
+        " breakValue=" + breakValue + " [" + words[i] + "]"
+      );
+      if (breakValue > maxBreakValue) {
+        maxBreakValue = breakValue;
+        iCut = i;
+      }
+      lenSoFar += 1 + words[i].length;
+    }
+
+    if (debugMode) ScriptLog("iCut=" + iCut);
+
+    var finalText = tagWords[0];
+    for (var i = 1; i < words.length; i++)
+      finalText += (i == iCut ? "\r\n" : " ") + tagWords[i];
+    finalText = finalText.replace(/\u00A0/g, " ");
+
+    if (debugMode) ScriptLog("finalText=[" + finalText + "]");
+
+    CurrentSub.Text = finalText;
+
+  }
+
+}

jsplugin/missing_punctuation.js

+/*
+  Missing punctuation
+  thyresias at gmail dot com (www.calorifix.net)
+  3 Dec 2005
+*/
+
+var debugMode = false;
+
+function checkSub(CurrentSub, NextSub) {
+
+  var tagText = CurrentSub.Text;
+
+  // get lines
+  var tagLines = tagText.split(/[\r\n]+/gm);
+  var nLines = tagLines.length;
+
+  // append the first line of the next subtitle
+  if (NextSub==null)
+    tagLines[nLines] = "New sentence.";
+  else {
+    var nextLines = NextSub.Text.split(/[\r\n]+/gm);
+    tagLines[nLines] = nextLines[0];
+  }
+
+  // strip leading & training tags
+  var lines = new Array(tagLines.length);
+  var begTags = new Array(lines.length);
+  var endTags = new Array(lines.length);
+  for (i=0; i<tagLines.length; i++) {
+    begTags[i] = getMatch(tagLines[i], /^(<\/?\w>)+/);
+    endTags[i] = getMatch(tagLines[i], /(<\/?\w>)+$/);
+    lines[i] = tagLines[i].substring(begTags[i].length, tagLines[i].length - endTags[i].length);
+    if (debugMode) ScriptLog(
+      i + ". =[" + tagLines[i] + "] => [" + begTags[i] + "|" + lines[i] + "|" + endTags[i] + "]"
+    );
+  }
+
+  // add missing punctuation
+  var finalText = "";
+  for (i=0; i<nLines; i++) {
+    finalText += (i>0 ? "\r\n" : "") + begTags[i] + lines[i];
+    if (
+      /[a-z��-��-�0-9]"?$/.test(lines[i]) &&
+      /^(-|"?[A-Z��-��-�])/.test(lines[i+1])
+    ) finalText += ".";
+    finalText += endTags[i];
+  }
+
+  if (debugMode) ScriptLog("finalText=[" + finalText.replace(/\r\n/mg, "|") + "]");
+
+  return finalText;
+
+}
+
+function getMatch(text, re) {
+  var match = text.match(re);
+  return match ? match[0] : "";
+}
+
+VSSPlugin = {
+
+  Name: " Missing Punctuation", // space: after French punctuation
+  Color: 0x4F9D00, // dark yellowish green
+  Message: "Missing punctuation",
+  Description: "Checks lines that seem to miss a trailing punctuation.",
+
+  // --- error detection/fix
+
+  HasError: function(CurrentSub, PreviousSub, NextSub) {
+    var fix = checkSub(CurrentSub, NextSub);
+    return fix==CurrentSub.Text ? "" : ">> " + fix.replace(/[\r\n]+/gm,"|");
+  },
+
+  FixError: function(CurrentSub, PreviousSub, NextSub) {
+    CurrentSub.Text = checkSub(CurrentSub, NextSub);
+  },
+
+}

jsplugin/open_close_tags.js

+/*
+  Forgotten open/close tags
+  thyresias at gmail dot com (www.calorifix.net)
+  11 Mar 2006
+*/
+
+function checkSub(Sub) {
+
+  // no tag: no error
+  if (Sub.Text==Sub.StrippedText)
+    return null;
+
+  // look for tags
+  var hasOpen = Sub.Text.match(/<i>/mg);
+  var hasClose = Sub.Text.match(/<\/i>/mg);
+  if (hasOpen)
+    if (hasClose)
+      return null;
+    else
+      return Sub.Text + "</i>";
+  else
+    return Sub.Text.replace(/<\/i>/m, "");
+
+}
+
+VSSPlugin = {
+
+  Name: " Forgotten open/close tags",
+  Color: 0x00FF00, // aggressive green
+  Message: "Missing tag",
+  Description:
+    "Looks for <i> without </i>, or </i> without <i>. " +
+    "When </i> is missing, the autofix adds it. " +
+    "When only </i> is found, the autofix deletes it.",
+
+  // --- error detection/fix
+
+  HasError: function(CurrentSub, PreviousSub, NextSub) {
+    var newText = checkSub(CurrentSub);
+    if (newText)
+      return ">> " + newText.replace(/\r\n/mg, "|");
+    else
+      return "";
+  },
+
+  FixError: function(CurrentSub, PreviousSub, NextSub) {
+    var newText = checkSub(CurrentSub);
+    if (newText)
+      CurrentSub.Text = newText;
+  }
+
+}

jsplugin/overlapping.js

+/*
+  Overlapping & delay between subtitles
+  original version by christophe dot paris at free dot fr
+  all changes below by thyresias at gmail dot com (www.calorifix.net)
+  26 Nov 2005  refactoring, added inter-subtitle delay and smart autofix
+  25 Feb 2007  Param_MinDelay set to 170 by default
+  14 Nov 2008  use global VSS parameter VSSCore.MinimumBlank
+*/
+
+// can we fix the problem?
+function canFix(CurrentSub, NextSub, maxOverlapFixed) {
+  // if overlap too large, we can't
+  if ((NextSub.Start - CurrentSub.Stop) < -maxOverlapFixed)
+    return false;
+  // if there is less room than twice the min duration plus the minimum delay, we can't either
+  if (NextSub.Stop - CurrentSub.Start < 2 * VSSCore.MinimumDuration + VSSCore.MinimumBlank)
+    return false;
+  // if some jerk set the VSSCore parameters to stupid values, check for reasonable ones
+  if (NextSub.Stop - CurrentSub.Start < 1002)
+    return false;
+  // YES WE CAN!
+  return true;
+}
+
+function stopEarlier(CurrentSub, NextSub) {
+  var newStop = NextSub.Start - VSSCore.MinimumBlank;
+  if (newStop > CurrentSub.Start) CurrentSub.Stop = newStop;
+}
+
+function startLater(CurrentSub, NextSub) {
+  var newStart = CurrentSub.Stop + VSSCore.MinimumBlank;
+  if (newStart < NextSub.Stop) NextSub.Start = newStart;
+}
+
+function optimizeReadingSpeeds(CurrentSub, NextSub) {
+
+  var totalDur = NextSub.Stop - CurrentSub.Start - VSSCore.MinimumBlank;
+
+  // get the current delay/overlap zone
+  var zoneStart, zoneStop;
+  if (CurrentSub.Stop <= NextSub.Start) {
+    // delay, possibly 0
+    zoneStart = CurrentSub.Stop;
+    zoneStop = NextSub.Start;
+  }
+  else {
+    // overlap
+    zoneStart = NextSub.Start;
+    zoneStop = CurrentSub.Stop;
+  }
+
+  // ideally, we have the same reading speed for both subtitles: total length / (total time - 1s)
+  var curLen = CurrentSub.StrippedText.length;
+  var nextLen = NextSub.StrippedText.length;
+  var rs = (curLen + nextLen) / (totalDur - 1000);
+
+  // new display times
+  var curDur = Math.round(curLen / rs + 500);
+  var nextDur = totalDur - curDur;
+
+  // corresponding new stop & start times
+  var newCurStop = CurrentSub.Start + curDur;
+  var newNextStart = NextSub.Stop - nextDur;
+
+  // we do not want to move the split zone too far from the current one
+  // the rule here is that the new zone must touch/overlap the old one
+
+  // if the new zone is before the old one,
+  // it means the current sub is way slower than the next one,
+  // so we move the stop time of the current one,
+  // which will remain slower than the next one, but not as much as before
+  if (newNextStart < zoneStart)
+    CurrentSub.Stop = NextSub.Start - VSSCore.MinimumBlank;
+
+  // if the new zone is after the old one, it is the reverse,
+  // so we move the start time of the next one
+  else if (newCurStop > zoneStop)
+    NextSub.Start = CurrentSub.Stop + VSSCore.MinimumBlank;
+
+  // otherwise, the new zone overlaps the old one, so we choose it
+  else {
+    CurrentSub.Stop = newCurStop;
+    NextSub.Start = newNextStart;
+  }
+
+}
+
+VSSPlugin = {
+
+  Name: "Overlap & delay between subtitles",
+  Color: 0xFF0000, // red
+  Message: "Delay to next subtitle",
+  Description: "Detects subtitles that overlap, or are too close from one another.",
+
+  // --- parameters
+
+  Param_Mode: {
+    Value: 1, Unit: '(1/2/3)', Description:
+    "How a problem will be fixed:\r\n" +
+    "1: Stop current subtitle earlier.\r\n" +
+    "2: Start next subtitle later.\r\n" +
+    "3: Do what optimizes the reading speeds."
+  },
+  Param_MaxOverlapFixed: {
+    Value: 500, Unit: "ms", Description:
+    "If subtitles overlap by more than this value, they won't be fixed."
+  },
+
+  // --- error detection/fix
+
+  HasError: function(CurrentSub, PreviousSub, NextSub) {
+
+    // last sub: cannot overlap
+    if (NextSub == null) return "";
+
+    // enough delay?
+    var delayToNext = NextSub.Start - CurrentSub.Stop;
+    if (delayToNext >= VSSCore.MinimumBlank) return "";
+
+    // not enough
+    return (
+      delayToNext >= 0
+      ? delayToNext + " ms delay to next subtitle"
+      : (-delayToNext) + " ms overlap"
+    ) + (
+      canFix(CurrentSub, NextSub, this.Param_MaxOverlapFixed.Value)
+      ? ""
+      : ", no autofix"
+    );
+  },
+
+  FixError: function(CurrentSub, PreviousSub, NextSub) {
+
+    if (NextSub == null) return;
+    var delayToNext = NextSub.Start - CurrentSub.Stop;
+    if (delayToNext >= VSSCore.MinimumBlank) return;
+    if (!canFix(CurrentSub, NextSub, this.Param_MaxOverlapFixed.Value)) return;
+    switch(this.Param_Mode.Value) {
+      case 1: stopEarlier(CurrentSub, NextSub); return;
+      case 2: startLater(CurrentSub, NextSub); return;
+      case 3: optimizeReadingSpeeds(CurrentSub, NextSub); return;
+      default: ScriptLog("Invalid overlap parameter Param_Mode = " + this.Param_Mode.Value); return;
+    }
+
+  }
+
+}

jsplugin/scene_change.js

+/*
+  Scene change check
+
+  original version by Nathbot
+  modified by Toff for integration in VSS 0.9.11
+  07 Dec 2008   re-written by thyresias at gmail dot com
+*/
+
+function readingSpeed(length, start, stop) {
+  var readingDuration = (stop - start) / 1000.0 - 0.5;
+  if (readingDuration <= 0)
+    return Infinity;
+  else
+    return length / readingDuration;
+}
+
+function checkSub(CurrentSub, PreviousSub, NextSub) {
+
+  // check for a scene change inside the subtitle "hot zone"
+  var hotZoneStart = CurrentSub.Start - SceneChange.StopOffset + 1;
+  var hotZoneStop = CurrentSub.Stop + SceneChange.StartOffset - 1;
+  if (!SceneChange.Contains(hotZoneStart, hotZoneStop))
+    return null;
+
+  var scTime;   // time of scene change
+
+  // scene change before the subtitle starts?
+  scTime = SceneChange.GetPrevious(CurrentSub.Start);
+  if (scTime > 0 && hotZoneStart <= scTime)
+    return {
+      msg: "Starts too close to a scene change: " + (CurrentSub.Start - scTime) + " ms",
+      fix: "start a bit later",
+      fixShift: false,
+      fixStart: scTime + SceneChange.StopOffset,
+      fixStop: CurrentSub.Stop
+    };
+
+  // scene change after the subtitle stops?
+  scTime = SceneChange.GetNext(CurrentSub.Stop);
+  if (scTime > 0 && hotZoneStop >= scTime)
+    return {
+      msg: "Ends too close to a scene change: " + (scTime - CurrentSub.Stop) + " ms",
+      fix: "stop a bit earlier",
+      fixShift: false,
+      fixStart: CurrentSub.Start,
+      fixStop: scTime - SceneChange.StartOffset
+    };
+
+  // at this point, there is a scene change inside the subtitle
+  scTime = SceneChange.GetNext(CurrentSub.Start);
+  var timeToStart = scTime - CurrentSub.Start;
+  var timeToStop = CurrentSub.Stop - scTime;
+
+  // closer to start than end
+  if (timeToStart < timeToStop && timeToStart < SceneChange.FilterOffset) {
+    var msg = "Start overlaps a scene change: " + timeToStart + " ms";
+    // we can fix if it will preserve a reasonable reading speed
+    var newStart = scTime + SceneChange.StopOffset;
+    var rs = readingSpeed(CurrentSub.StrippedText.length, newStart, CurrentSub.Stop);
+    // --- if the RS would be reasonable this way, we're done
+    if (rs <= 35)
+      return {
+        msg: msg,
+        fix: "start later",
+        fixShift: false,
+        fixStart: newStart,
+        fixStop: CurrentSub.Stop
+      };
+    // --- the RS would be too fast: maybe we can move the end?
+    // new stop for ideal reading speed
+    var newStop = Math.round(newStart + 500 + CurrentSub.StrippedText.length / 18 * 1000);
+    // can't move too close to next subtitle
+    if (NextSub && newStop + VSSCore.MinimumBlank > NextSub.Start)
+      newStop = NextSub.Start - VSSCore.MinimumBlank;
+    // can't move too close to a scene change
+    if (newStop > CurrentSub.Stop && SceneChange.Contains(CurrentSub.Stop, newStop + SceneChange.StartOffset))
+      newStop = SceneChange.GetNext(CurrentSub.Stop) - SceneChange.StartOffset;
+    // do we have a reasonable RS now?
+    rs = readingSpeed(CurrentSub.StrippedText.length, newStart, newStop);
+    // nope: no fix
+    if (rs > 35) return {msg: msg, fix: null};
+    // YESSS!
+    return {
+      msg: msg,
+      fix: "shift subtitle",
+      fixShift: true,
+      fixStart: newStart,
+      fixStop: newStop
+    };
+  }
+
+  // closer to end than start
+  else if (timeToStart > timeToStop && timeToStop < SceneChange.FilterOffset) {
+    var msg = "End overlaps a scene change: " + timeToStop + " ms";
+    // we can fix if it will preserve a reasonable reading speed
+    var newStop = scTime - SceneChange.StartOffset;
+    var rs = readingSpeed(CurrentSub.StrippedText.length, CurrentSub.Start, newStop);
+    // --- if the RS would be reasonable this way, we're done
+    if (rs <= 35)
+      return {
+        msg: msg,
+        fix: "stop earlier",
+        fixShift: false,
+        fixStart: CurrentSub.Start,
+        fixStop: newStop
+      };
+    // --- the RS would be too fast: maybe we can move the start?
+    // new start for ideal reading speed
+    var newStart = Math.round(newStop - 500 - CurrentSub.StrippedText.length / 18 * 1000);
+    // can't move too close to previous subtitle
+    if (PreviousSub && newStart - VSSCore.MinimumBlank < PreviousSub.Stop)
+      newStart = PreviousSub.Stop + VSSCore.MinimumBlank;
+    // can't move too close to a scene change
+    if (newStart < CurrentSub.Start && SceneChange.Contains(newStart - SceneChange.StopOffset, CurrentSub.Start))
+      newStop = SceneChange.GetPrevious(CurrentSub.Start) + SceneChange.StopOffset;
+    // do we have a reasonable RS now?
+    rs = readingSpeed(CurrentSub.StrippedText.length, newStart, newStop);
+    // nope: no fix
+    if (rs > 35) return {msg: msg, fix: null};
+    // YESSS!
+    return {
+      msg: msg,
+      fix: "shift subtitle",
+      fixShift: true,
+      fixStart: newStart,
+      fixStop: newStop
+    };
+  }
+
+  // equal distance from start & end, or inside filtered zone
+  var msg = "Overlaps a scene change";
+  if (CurrentSub.Stop - CurrentSub.Start >= 2* VSSCore.MinimumBlank)
+    msg += ": consider splitting";
+  return {
+    msg: msg,
+    fix: null
+  }
+
+}
+
+VSSPlugin = {
+
+  Name: "Scene change",
+  Color: 0xFF8040, // orange
+  Message: "Scene change",
+  Description:
+    "Error when a subtitle overlaps a scene change, " +
+    "or is too close to a scene change.",
+
+  // --- parameters
+
+  Param_ReportNoFix: { Value: 1, Unit: "1=Yes, 0=No", Description: "Report problems that have no fix?" },
+  Param_ReportShift: { Value: 1, Unit: "1=Yes, 0=No", Description: "Report problems where fix is shifting the subtitle?" },
+
+  // --- error detection/fix
+
+  HasError: function(CurrentSub, PreviousSub, NextSub) {
+    check = checkSub(CurrentSub, PreviousSub, NextSub);
+    if (check == null) return "";
+    if (check.fix == null)
+      if (this.Param_ReportNoFix.Value == 0)  return "";
+      else                                    return check.msg + ", no fix";
+    if (check.fixShift && this.Param_ReportShift.Value == 0) return "";
+    return check.msg + ", fix: " + check.fix;
+  },
+
+  FixError: function(CurrentSub, PreviousSub, NextSub) {
+    check = checkSub(CurrentSub, PreviousSub, NextSub);
+    if (check == null || check.fix == null) return;
+    CurrentSub.Start = check.fixStart;
+    CurrentSub.Stop = check.fixStop;
+  }
+
+};

jsplugin/too_fast_reading.js

+/*
+  Too fast reading speed
+  thyresias at gmail dot com (www.calorifix.net)
+
+  14 Jan 2006
+  05 May 2006   used Math.round to compare to limit given, so 35.4 is OK for max=35, but 35.6 is not
+  20 Jan 2007   used "reading speed" wording
+  25 Feb 2007   Param_MinDelay set to 170 by default, Param_ShowOptimal=0 by default
+  14 Nov 2008   use global VSS parameter VSSCore.MinimumBlank, removed Param_ShowOptimal
+*/
+
+// generic check
+function checkSub (Sub, NextSub, maxRS) {
+
+  // length, current duration, reading speed
+  var len = Sub.StrippedText.length;
+  var durMS = Sub.Stop - Sub.Start;
+  var rsX = len * 1000 / (durMS - 500);
+  var rs = Math.round(rsX*10) / 10;
+
+  // duration in seconds, rounded to 1 decimal digit
+  var dur = Math.round(durMS/100) / 10;
+
+  // check duration
+  if (Math.round(rs) <= maxRS) return null;
+
+  var fixDurX = len / maxRS + 0.5;
+  var fixDur = Math.round(fixDurX*10)/10;
+  var fixLen = Math.round((dur - 0.5) * maxRS);
+
+  // new duration = min, new c/s, new stop time
+  var newStop = Sub.Start + Math.round(fixDurX*1000);
+
+  // preserve minimum delay to next subtitle
+  if (NextSub != null && newStop > NextSub.Start - VSSCore.MinimumBlank)
+    newStop = NextSub.Start - VSSCore.MinimumBlank;
+
+  // return null if we cannot correct
+  if (newStop <= Sub.Stop)
+   newStop = null;
+
+  if (fixLen<2 || fixLen==len) fixLen = null;
+
+  return {
+    curRS: rs,
+    fixDurDiff: Math.round(fixDurX*1000 - durMS),
+    fixLenDiff: (fixLen ? len - fixLen : null),
+    fixable: (newStop != null),
+    newStop: newStop
+  };
+
+}
+
+VSSPlugin = {
+
+  Name: "Too fast",
+  Color: 0xFFFF00, // yellow
+  Message: "Too fast reading",
+  Description:
+    "Error when reading speed > MaxRS (pro: 35).\r\n" +
+    "Reading Speed = (subtitle length) / (reading time).\r\n" +
+    "(reading time = display time - 0.5 seconds)",
+
+  // --- parameters
+
+  Param_MaxRS: { Value: 35, Unit: "c/s", Description: "Maximum reading speed." },
+
+  // --- error detection/fix
+
+  HasError: function(CurrentSub, PreviousSub, NextSub) {
+    var check = checkSub(CurrentSub, NextSub, this.Param_MaxRS.Value);
+    if (check == null)
+      return "";
+    var msg = check.curRS + " c/s >> +" + check.fixDurDiff + " ms";
+    if (check.fixable) {
+      if (check.fixLenDiff) {
+        msg += " or -" + check.fixLenDiff + " char";
+        if (check.fixLenDiff > 1) msg += "s";
+      }
+    }
+    else
+      msg += ", no fix";
+    return msg;
+  },
+
+  FixError: function(CurrentSub, PreviousSub, NextSub) {
+    var check = checkSub(CurrentSub, NextSub, this.Param_MaxRS.Value);
+    if (check && check.fixable) CurrentSub.Stop = check.newStop;
+  }
+
+}

jsplugin/too_long_duration.js

+/*
+  Too long duration
+  thyresias at gmail dot com (www.calorifix.net)
+  26 Nov 2005
+  14 Dec 2008   renamed to "too long duration"
+  07 Feb 2009   use new 0.9.18 VSS parameter MaximumDuration
+*/
+
+VSSPlugin = {
+
+  Name: "Too long duration",
+  Color: 0x000080, // dark blue
+  Message: "Too long",
+  Description:
+    "Error when the duration is less than the maximum duration " +
+    "(set in the 'Subtitle' tab, pro: 6000 ms = 6 s).",
+
+  // --- parameters
+
+  // --- error detection/fix
+
+  HasError: function(CurrentSub, PreviousSub, NextSub) {
+    var durx = CurrentSub.Stop - CurrentSub.Start;
+    var dur = Math.round((durx)/100)/10;
+    if (durx > VSSCore.MaximumDuration)
+      return (dur + " s");
+    else
+      return "";
+  },
+
+  // shorten display time
+  FixError: function(CurrentSub, PreviousSub, NextSub) {
+    var durx = CurrentSub.Stop - CurrentSub.Start;
+    if (durx <= VSSCore.MaximumDuration)
+      return;
+    CurrentSub.Stop = CurrentSub.Start + VSSCore.MaximumDuration;
+  }
+}

jsplugin/too_short_duration.js

+/*
+  Too short duration
+  thyresias at gmail dot com (www.calorifix.net)
+  26 Nov 2005
+  07 Mar 2006   refactoring: use generic function
+  25 Feb 2007   Param_MinDelay set to 170 by default
+  14 Nov 2008   update for new VSS capabilities
+  14 Dec 2008   renamed to "too short duration"
+*/
+
+// generic check
+function checkSub (Sub, NextSub) {
+
+  // current duration
+  var dur = Sub.Stop - Sub.Start;
+
+  // return null if OK
+  if (dur >= VSSCore.MinimumDuration) return null;
+
+  // new duration
+  var newStop = Sub.Start + VSSCore.MinimumDuration;
+
+  // preserve minimum delay to next subtitle
+  if (NextSub != null && newStop > NextSub.Start - VSSCore.MinimumBlank)
+    newStop = NextSub.Start - VSSCore.MinimumBlank;
+
+  // return null if we cannot correct
+  if (newStop <= Sub.Stop)
+    newStop = null;
+
+  return {
+    dur: dur,
+    fixable: (newStop != null),
+    newStop: newStop
+  };
+
+}
+
+VSSPlugin = {
+
+  Name: "Too short duration",
+  Color: 0xFF80FF, // pink
+  Message: "Too short",
+  Description:
+    "Error when the duration is less than the minimum duration " +
+    "(set in the 'Subtitle' tab, pro: 600 ms = 0.6 s).",
+
+  // --- error detection/fix
+
+  HasError: function(CurrentSub, PreviousSub, NextSub) {
+    var check = checkSub(CurrentSub, NextSub);
+    if (check == null)
+      return "";
+    else
+      return check.dur + " ms" + ( check.fixable ? "": ", no fix" ) ;
+  },
+
+  // lengthen display time
+  FixError: function(CurrentSub, PreviousSub, NextSub) {
+    var check = checkSub(CurrentSub, NextSub);
+    if (check != null && check.fixable) CurrentSub.Stop = check.newStop;
+  }
+}

jsplugin/too_slow_reading.js

+/*
+  Too slow reading speed
+  thyresias at gmail dot com (www.calorifix.net)
+
+  14 Jan 2006
+  05 May 2006   used Math.round to compare to limit given, so 4.6 is OK for min=5, but 4.4 is not
+                added check for negative corrected CPS (very fast!)
+  20 Jan 2007   used "reading speed" wording
+  25 Feb 2007   Param_ShowOptimal=0 by default
+  14 Nov 2008   use global VSS parameter VSSCore.MinimumBlank, removed Param_ShowOptimal
+*/
+
+function checkSub (Sub, NextSub, minRS) {
+
+  // length, current duration, reading speed
+  var len = Sub.StrippedText.length;
+  var durMS = Sub.Stop - Sub.Start;
+  var rsX = len * 1000 / (durMS - 500);
+  if (rsX <= 0) return null;  // way too fast!
+  var rs = Math.round(rsX*10) / 10;
+
+  // duration in seconds, rounded to 1 decimal digit
+  var dur = Math.round(durMS/100) / 10;
+
+  // check duration
+  if (Math.round(rs) >= minRS) return null;
+
+  var fixDurX = len / minRS + 0.5;
+  var fixDur = Math.round(fixDurX*10)/10;
+  var fixLen = Math.round((dur - 0.5) * minRS);
+
+  // new stop time
+  var newStop = Sub.Start + Math.round(fixDurX*1000);
+
+  if (fixLen<2 || fixLen==len) fixLen = null;
+
+  return {
+    curDur: dur,
+    curRS: rs,
+    fixDurDiff: Math.round(durMS - fixDurX*1000),
+    fixLenDiff: (fixLen ? fixLen - len : null),
+    fixable: (newStop != null),
+    newStop: newStop
+  };
+
+}
+
+VSSPlugin = {
+
+  Name: "Too slow",
+  Color: 0x0080FF, // light blue
+  Message: "Too slow reading",