jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/314664 )

Change subject: Skip back sentence
......................................................................


Skip back sentence

Enabled skipping back to the previous sentence while playing. If the
current sentence has played for less than a certain number of seconds,
it's played from the beginning. This threshold can be set in the
configuration and is 3 seconds by default. Button and keyboard
shortcut (ctrl + left arrow by default) were added.

Bug: T133687
Change-Id: Ib63a013f525ce41f4e03bccf115ebe2a15fe4992
---
M Hooks.php
M extension.json
M modules/ext.wikispeech.css
M modules/ext.wikispeech.js
M tests/qunit/ext.wikispeech.test.js
5 files changed, 234 insertions(+), 127 deletions(-)

Approvals:
  Lokal Profil: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/Hooks.php b/Hooks.php
index a7c3211..0d15c02 100644
--- a/Hooks.php
+++ b/Hooks.php
@@ -120,6 +120,9 @@
                global $wgWikispeechKeyboardShortcuts;
                $vars['wgWikispeechKeyboardShortcuts'] =
                        $wgWikispeechKeyboardShortcuts;
+               global $wgWikispeechSkipBackRewindsThreshold;
+               $vars['wgWikispeechSkipBackRewindsThreshold'] =
+                       $wgWikispeechSkipBackRewindsThreshold;
                return true;
        }
 }
diff --git a/extension.json b/extension.json
index efba7c8..6f0c748 100644
--- a/extension.json
+++ b/extension.json
@@ -80,10 +80,15 @@
                                "key": 39,
                                "modifiers": [ "ctrl" ]
                        },
+                       "skipBackSentence": {
+                               "key": 37,
+                               "modifiers": [ "ctrl" ]
+                       },
                        "skipAheadWord": {
                                "key": 40,
                                "modifiers": [ "ctrl" ]
                        }
-               }
+               },
+               "WikispeechSkipBackRewindsThreshold": 3.0
        }
 }
diff --git a/modules/ext.wikispeech.css b/modules/ext.wikispeech.css
index 0ea613c..d362b04 100644
--- a/modules/ext.wikispeech.css
+++ b/modules/ext.wikispeech.css
@@ -14,6 +14,10 @@
        content: "Skip ahead sentence";
 }
 
+.ext-wikispeech-skip-back-sentence:after {
+       content: "Skip back sentence";
+}
+
 .ext-wikispeech-skip-ahead-word:after {
        content: "Skip ahead word";
 }
diff --git a/modules/ext.wikispeech.js b/modules/ext.wikispeech.js
index e7a76cf..0268571 100644
--- a/modules/ext.wikispeech.js
+++ b/modules/ext.wikispeech.js
@@ -5,30 +5,49 @@
                self = this;
                $currentUtterance = $();
 
+               /**
+                * Add buttons for controlling playback to the top of the page.
+                */
+
                this.addButtons = function () {
-                       self.addPlayStopButton();
-                       self.addSkipAheadSentenceButton();
-                       self.addSkipAheadWordButton();
+                       self.addButton(
+                               'ext-wikispeech-play-stop-button',
+                               'ext-wikispeech-play',
+                               self.playOrStop
+                       );
+                       self.addButton(
+                               'ext-wikispeech-skip-ahead-sentence-button',
+                               'ext-wikispeech-skip-ahead-sentence',
+                               self.skipAheadUtterance
+                       );
+                       self.addButton(
+                               'ext-wikispeech-skip-back-sentence-button',
+                               'ext-wikispeech-skip-back-sentence',
+                               self.skipBackUtterance
+                       );
+                       self.addButton(
+                               'ext-wikispeech-skip-ahead-word-button',
+                               'ext-wikispeech-skip-ahead-word',
+                               self.skipAheadToken
+                       );
                };
 
                /**
-                * Add a button for starting and stopping recitation to the
-                * page.
-                *
-                * When no utterance is playing, clicking starts the first
-                * utterance.  When an utterance is being played, clicking
-                * stops the playback.  The button changes appearance to
-                * reflect its current function.
-                */
+               * Add a control button.
+               *
+               * @param {string} id The id of the button.
+               * @param {string} cssClass The name of the CSS class to add to
+               *  the button.
+               * @param {string} onClickFunction The name of the function to
+               *  call when the button is clicked.
+               */
 
-               this.addPlayStopButton = function () {
-                       var $playStopButton = $( '<button></button>' )
-                               .attr( 'id', 'ext-wikispeech-play-stop-button' )
-                               .addClass( 'ext-wikispeech-play' );
-                       $( '#firstHeading' ).append( $playStopButton );
-                       // For some reason, testing doesn't work with
-                       // .click( self.playOrStop ).
-                       $playStopButton.click( function () { self.playOrStop(); 
} );
+               this.addButton = function ( id, cssClass, onClickFunction ) {
+                       var $button = $( '<button></button>' )
+                               .attr( 'id', id )
+                               .addClass( cssClass );
+                       $( '#firstHeading' ).append( $button );
+                       $button.click( onClickFunction );
                };
 
                /**
@@ -106,23 +125,6 @@
                };
 
                /**
-                * Add a button for skipping to the next sentence.
-                *
-                * This actually skips to the next utterance; it's assumed
-                * that the utterances are sentences.
-                */
-
-               this.addSkipAheadSentenceButton = function () {
-                       var $skipAheadSentenceButton = $( '<button></button>' )
-                               .attr( 'id', 
'ext-wikispeech-skip-ahead-sentence-button' )
-                               .addClass( 'ext-wikispeech-skip-ahead-sentence' 
);
-                       $( '#firstHeading' ).append( $skipAheadSentenceButton );
-                       $skipAheadSentenceButton.click( function () {
-                               self.skipAheadUtterance();
-                       } );
-               };
-
-               /**
                 * Skip to the next utterance.
                 *
                 * Stop the current utterance and start playing the next one.
@@ -138,41 +140,34 @@
                };
 
                /**
-                * Get the utterance after the given utterance.
+                * Skip to the previous utterance.
                 *
-                * @param {jQuery} $utterance The original utterance.
-                * @return {jQuery} The utterance after the original
-                *  utterance. Empty object if $utterance isn't a valid
-                *  utterance.
+                * Stop the current utterance and start playing the previous 
one. If
+                * the first utterance is playing, restart it.
                 */
 
-               this.getNextUtterance = function ( $utterance ) {
-                       var utteranceIdParts, nextUtteranceIndex, 
nextUtteranceId;
+               this.skipBackUtterance = function () {
+                       var previousUtterance, rewindThreshold, $audio, time;
 
-                       if ( !$utterance.length ) {
-                               return $();
+                       previousUtterance =
+                               self.getPreviousUtterance( $currentUtterance );
+                       if ( previousUtterance.length ) {
+                               // Only consider skipping back to previous if 
the
+                               // current utterance isn't the first one.
+                               rewindThreshold = mw.config.get(
+                                       'wgWikispeechSkipBackRewindsThreshold' 
);
+                               $audio = $currentUtterance.children( 'audio' );
+                               time = $audio.prop( 'currentTime' );
+                               if ( time > rewindThreshold ) {
+                                       $audio.prop( 'currentTime', 0.0 );
+                               } else {
+                                       self.playUtterance( previousUtterance );
+                               }
+                       } else if ( self.isPlaying() ) {
+                               // Alwas skip to start of utterance if the 
current
+                               // uterrance is the first.
+                               self.play();
                        }
-                       // Utterance id's follow the pattern "utterance-x", 
where
-                       // x is the index.
-                       utteranceIdParts = $utterance.attr( 'id' ).split( '-' );
-                       nextUtteranceIndex = parseInt( utteranceIdParts[ 1 ], 
10 ) + 1;
-                       utteranceIdParts[ 1 ] = nextUtteranceIndex;
-                       nextUtteranceId = utteranceIdParts.join( '-' );
-                       return $( '#' + nextUtteranceId );
-               };
-
-               /**
-                * Add a button for skipping to the next word.
-                */
-
-               this.addSkipAheadWordButton = function () {
-                       var $button = $( '<button></button>' )
-                               .attr( 'id', 
'ext-wikispeech-skip-ahead-word-button' )
-                               .addClass( 'ext-wikispeech-skip-ahead-word' );
-                       $( '#firstHeading' ).append( $button );
-                       $button.click( function () {
-                               self.skipAheadToken();
-                       } );
                };
 
                /**
@@ -277,6 +272,11 @@
                                ) {
                                        self.skipAheadUtterance();
                                        return false;
+                               } else if ( self.eventMatchShortcut(
+                                       event,
+                                       shortcuts.skipBackSentence )
+                               ) {
+                                       self.skipBackUtterance();
                                } else if (
                                        self.eventMatchShortcut( event, 
shortcuts.skipAheadWord )
                                ) {
@@ -352,6 +352,58 @@
                };
 
                /**
+                * Get the utterance after the given utterance.
+                *
+                * @param {jQuery} $utterance The original utterance.
+                * @return {jQuery} The utterance after the original
+                *  utterance. Empty object if $utterance is the last one.
+                */
+
+               this.getNextUtterance = function ( $utterance ) {
+                       return self.getUtteranceByOffset( $utterance, 1 );
+               };
+
+               /**
+                * Get the utterance by offset from another utterance.
+                *
+                * @param {jQuery} $utterance The original utterance.
+                * @param {number} offset The difference, in index, to the
+                *  wanted utterance. Can be negative for preceding
+                *  utterances.
+                * @return {jQuery} The utterance after the original
+                *  utterance. Empty object if $utterance isn't a valid
+                *  utterance or if an utterance couldn't be found.
+                */
+
+               this.getUtteranceByOffset = function ( $utterance, offset ) {
+                       var utteranceIdParts, nextUtteranceIndex, 
nextUtteranceId;
+
+                       if ( !$utterance.length ) {
+                               return $();
+                       }
+                       // Utterance id's follow the pattern "utterance-x", 
where
+                       // x is the index.
+                       utteranceIdParts = $utterance.attr( 'id' ).split( '-' );
+                       nextUtteranceIndex =
+                               parseInt( utteranceIdParts[ 1 ], 10 ) + offset;
+                       utteranceIdParts[ 1 ] = nextUtteranceIndex;
+                       nextUtteranceId = utteranceIdParts.join( '-' );
+                       return $( '#' + nextUtteranceId );
+               };
+
+               /**
+                * Get the utterance before the given utterance.
+                *
+                * @param {jQuery} $utterance The original utterance.
+                * @return {jQuery} The utterance before the original
+                *  utterance. Empty object if $utterance is the first one.
+                */
+
+               this.getPreviousUtterance = function ( $utterance ) {
+                       return self.getUtteranceByOffset( $utterance, -1 );
+               };
+
+               /**
                 * Request audio for an utterance.
                 *
                 * Adds audio and token elements when the response is
@@ -390,7 +442,7 @@
                 * The request should specify the following parameters:
                 * - lang: the language used by the synthesizer.
                 * - input_type: "ssml" if you want SSML markup, otherwise
-                * "text" for plain text.
+                *  "text" for plain text.
                 * - input: the text to be synthesized.
                 * For more on the parameters, see:
                 * https://github.com/stts-se/wikispeech_mockup/wiki/api.
diff --git a/tests/qunit/ext.wikispeech.test.js 
b/tests/qunit/ext.wikispeech.test.js
index 7665101..f3ca51d 100644
--- a/tests/qunit/ext.wikispeech.test.js
+++ b/tests/qunit/ext.wikispeech.test.js
@@ -33,11 +33,19 @@
                                                key: 39,
                                                modifiers: [ 'ctrl' ]
                                        },
+                                       skipBackSentence: {
+                                               key: 37,
+                                               modifiers: [ 'ctrl' ]
+                                       },
                                        skipAheadWord: {
                                                key: 40,
                                                modifiers: [ 'ctrl' ]
                                        }
                                }
+                       );
+                       mw.config.set(
+                               'wgWikispeechSkipBackRewindsThreshold',
+                               3.0
                        );
                },
                teardown: function () {
@@ -156,7 +164,7 @@
        } );
 
        QUnit.test( 'addButtons()', function ( assert ) {
-               assert.expect( 3 );
+               assert.expect( 4 );
                wikispeech.addButtons();
 
                assert.strictEqual(
@@ -165,6 +173,10 @@
                );
                assert.strictEqual(
                        $( '#firstHeading 
#ext-wikispeech-skip-ahead-sentence-button' ).length,
+                       1
+               );
+               assert.strictEqual(
+                       $( '#firstHeading 
#ext-wikispeech-skip-back-sentence-button' ).length,
                        1
                );
                assert.strictEqual(
@@ -192,8 +204,8 @@
 
        function testClickButton( assert, functionName, buttonId ) {
                assert.expect( 1 );
-               wikispeech.addButtons();
                sinon.spy( wikispeech, functionName );
+               wikispeech.addButtons();
 
                $( buttonId ).click();
 
@@ -205,6 +217,14 @@
                        assert,
                        'skipAheadUtterance',
                        '#ext-wikispeech-skip-ahead-sentence-button'
+               );
+       } );
+
+       QUnit.test( 'Clicking skip back sentence button', function ( assert ) {
+               testClickButton(
+                       assert,
+                       'skipBackUtterance',
+                       '#ext-wikispeech-skip-back-sentence-button'
                );
        } );
 
@@ -284,13 +304,17 @@
                testKeyboardShortcut( assert, 'skipAheadUtterance', 39, 'c' );
        } );
 
+       QUnit.test( 'Pressing keyboard shortcut for skipBackSentence', function 
( assert ) {
+               testKeyboardShortcut( assert, 'skipBackUtterance', 37, 'c' );
+       } );
+
        QUnit.test( 'Pressing keyboard shortcut for skipAheadWord', function ( 
assert ) {
                testKeyboardShortcut( assert, 'skipAheadToken', 40, 'c' );
        } );
 
        QUnit.test( 'stop()', function ( assert ) {
                assert.expect( 4 );
-               wikispeech.addPlayStopButton();
+               wikispeech.addButtons();
                wikispeech.play();
                wikispeech.prepareUtterance( $( '#utterance-0' ) );
                $( '#utterance-0 audio' ).prop( 'currentTime', 1 );
@@ -317,7 +341,7 @@
        QUnit.test( 'play()', function ( assert ) {
                var $firstUtterance = $( '#utterance-0' );
                assert.expect( 3 );
-               wikispeech.addPlayStopButton();
+               wikispeech.addButtons();
                wikispeech.prepareUtterance( $firstUtterance );
 
                wikispeech.play();
@@ -341,6 +365,7 @@
        QUnit.test( 'skipAheadUtterance()', function ( assert ) {
                assert.expect( 2 );
                wikispeech.prepareUtterance( $( '#utterance-0' ) );
+               wikispeech.prepareUtterance( $( '#utterance-1' ) );
                wikispeech.play();
 
                wikispeech.skipAheadUtterance();
@@ -360,6 +385,62 @@
                wikispeech.skipAheadUtterance();
 
                assert.strictEqual( wikispeech.stop.called, true );
+       } );
+
+       QUnit.test( 'skipBackUtterance()', function ( assert ) {
+               assert.expect( 2 );
+               wikispeech.prepareUtterance( $( '#utterance-0' ) );
+               wikispeech.prepareUtterance( $( '#utterance-1' ) );
+               wikispeech.playUtterance( $( '#utterance-1' ) );
+
+               wikispeech.skipBackUtterance();
+
+               assert.strictEqual( $( '#utterance-1 audio' ).prop( 'paused' ), 
true );
+               assert.strictEqual(
+                       $( '#utterance-0 audio' ).prop( 'paused' ),
+                       false
+               );
+       } );
+
+       QUnit.test( 'skipBackUtterance(): restart if first utterance', function 
( assert ) {
+               assert.expect( 2 );
+               wikispeech.prepareUtterance( $( '#utterance-0' ) );
+               wikispeech.playUtterance( $( '#utterance-0' ) );
+               $( '#utterance-0 audio' ).prop( 'currentTime', 1.0 );
+
+               wikispeech.skipBackUtterance();
+
+               assert.strictEqual(
+                       $( '#utterance-0 audio' ).prop( 'paused' ),
+                       false
+               );
+               assert.strictEqual(
+                       $( '#utterance-0 audio' ).prop( 'currentTime' ),
+                       0.0
+               );
+       } );
+
+       QUnit.test( 'skipBackUtterance(): restart if played long enough', 
function ( assert ) {
+               assert.expect( 3 );
+               wikispeech.prepareUtterance( $( '#utterance-0' ) );
+               wikispeech.prepareUtterance( $( '#utterance-1' ) );
+               wikispeech.playUtterance( $( '#utterance-1' ) );
+               $( '#utterance-1 audio' ).prop( 'currentTime', 3.1 );
+
+               wikispeech.skipBackUtterance();
+
+               assert.strictEqual(
+                       $( '#utterance-1 audio' ).prop( 'paused' ),
+                       false
+               );
+               assert.strictEqual(
+                       $( '#utterance-1 audio' ).prop( 'currentTime' ),
+                       0.0
+               );
+               assert.strictEqual(
+                       $( '#utterance-0 audio' ).prop( 'paused' ),
+                       true
+               );
        } );
 
        QUnit.test( 'getNextUtterance()', function ( assert ) {
@@ -651,23 +732,13 @@
        } );
 
        QUnit.test( 'skipAheadToken()', function ( assert ) {
+               var $tokens;
+
                assert.expect( 1 );
                wikispeech.prepareUtterance( $( '#utterance-0' ) );
-               $( '#utterance-0' ).append(
-                       $( '<tokens></tokens>' )
-                               .append(
-                                       $( '<token></token>' )
-                                               .attr( 'start-time', 0.0 )
-                                               .attr( 'end-time', 1.0 )
-                                               .text( 'word' )
-                               )
-                               .append(
-                                       $( '<token></token>' )
-                                               .attr( 'start-time', 1.0 )
-                                               .attr( 'end-time', 2.0 )
-                                               .text( 'word' )
-                               )
-               );
+               $tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0' 
) );
+               addToken( $tokens, 'one', 0, 0.0, 1.0 );
+               addToken( $tokens, 'two', 0, 1.0, 2.0 );
                wikispeech.play();
 
                wikispeech.skipAheadToken();
@@ -679,21 +750,13 @@
        } );
 
        QUnit.test( 'skipAheadToken(): skip ahead utterance when last token', 
function ( assert ) {
+               var $tokens;
+
                assert.expect( 1 );
                wikispeech.prepareUtterance( $( '#utterance-0' ) );
-               $( '#utterance-0' ).append(
-                       $( '<tokens></tokens>' )
-                               .append(
-                                       $( '<token></token>' )
-                                               .attr( 'start-time', 0.0 )
-                                               .attr( 'end-time', 1.0 )
-                               )
-                               .append(
-                                       $( '<token></token>' )
-                                               .attr( 'start-time', 1.0 )
-                                               .attr( 'end-time', 2.0 )
-                               )
-               );
+               $tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0' 
) );
+               addToken( $tokens, 'first', 0, 0.0, 1.0 );
+               addToken( $tokens, 'last', 0, 1.0, 2.0 );
                wikispeech.play();
                $( '#utterance-0 audio' ).prop( 'currentTime', 1.1 );
                sinon.spy( wikispeech, 'skipAheadUtterance' );
@@ -704,35 +767,15 @@
        } );
 
        QUnit.test( 'skipAheadToken(): ignore silent tokens', function ( assert 
) {
+               var $tokens;
+
                assert.expect( 1 );
                wikispeech.prepareUtterance( $( '#utterance-0' ) );
-               $( '#utterance-0' ).append(
-                       $( '<tokens></tokens>' )
-                               .append(
-                                       $( '<token></token>' )
-                                               .attr( 'start-time', 0.0 )
-                                               .attr( 'end-time', 1.0 )
-                                               .text( 'starting point' )
-                               )
-                               .append(
-                                       $( '<token></token>' )
-                                               .attr( 'start-time', 1.0 )
-                                               .attr( 'end-time', 1.0 )
-                                               .text( 'no duration' )
-                               )
-                               .append(
-                                       $( '<token></token>' )
-                                               .attr( 'start-time', 1.0 )
-                                               .attr( 'end-time', 2.0 )
-                                               .text( '' )
-                               )
-                               .append(
-                                       $( '<token></token>' )
-                                               .attr( 'start-time', 2.0 )
-                                               .attr( 'end-time', 3.0 )
-                                               .text( 'target' )
-                               )
-               );
+               $tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0' 
) );
+               addToken( $tokens, 'starting word', 0, 0.0, 1.0 );
+               addToken( $tokens, 'no duration', 0, 1.0, 1.0 );
+               addToken( $tokens, '', 0, 1.0, 2.0 );
+               addToken( $tokens, 'goal', 0, 2.0, 3.0 );
                wikispeech.play();
 
                wikispeech.skipAheadToken();

-- 
To view, visit https://gerrit.wikimedia.org/r/314664
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ib63a013f525ce41f4e03bccf115ebe2a15fe4992
Gerrit-PatchSet: 7
Gerrit-Project: mediawiki/extensions/Wikispeech
Gerrit-Branch: master
Gerrit-Owner: Sebastian Berlin (WMSE) <[email protected]>
Gerrit-Reviewer: Lokal Profil <[email protected]>
Gerrit-Reviewer: Sebastian Berlin (WMSE) <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to