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