Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f3de9d5
Merge pull request #6 from fluid-project/master
waharnum Jul 27, 2016
8a0255f
FLUID-5936: use correct API event handler for end of speech
waharnum Jul 28, 2016
a66947d
FLUID-5936: add a comment about the tests currently being skipped in …
waharnum Jul 28, 2016
e85d86a
FLUID-5936: rewrite tests to use the IoC framework
waharnum Jul 28, 2016
2dba2cf
FLUID-5936: restructure tests to better handle variations in asynchro…
waharnum Aug 4, 2016
03f3dd1
FLUID-5936: linting
waharnum Aug 4, 2016
8be2d97
FLUID-5936: potential fix for test failures on Windows
waharnum Aug 5, 2016
15b6cf6
FLUID-5936: continuing the quest for a sequencing approach that will …
waharnum Aug 5, 2016
a92d6ed
FLUID-5936: issue pause/resume commands in test with slight delay
waharnum Aug 5, 2016
9143c35
FLUID-5936: set volume to 0
waharnum Aug 5, 2016
53099b7
FLUID-5936: linting
waharnum Aug 5, 2016
d6077c5
FLUID-5936: 50ms timeout for control interaction
waharnum Aug 8, 2016
d05ac06
FLUID-5936: move asynchronous command issuance into main component fr…
waharnum Aug 8, 2016
3f162b6
FLUID-5936: reset volume to 0
waharnum Aug 8, 2016
5ca02a8
FLUID-5936: use an asyncTest wrapper around the promise to prevent th…
waharnum Aug 15, 2016
13ba66a
FLUID-5936: add a comment about setTimeout of asyncTest wrapper
waharnum Aug 16, 2016
5b08b2a
FLUID-5936: fix comment line length
waharnum Aug 16, 2016
12d3b15
FLUID-5936: simplify asynctest structure
waharnum Aug 17, 2016
5f6809e
FLUID-5936: extract generalized wrapper for choosing which test to ex…
waharnum Aug 17, 2016
fca22cd
Merge pull request #7 from fluid-project/master
waharnum Aug 17, 2016
49bd622
FLUID-5936: use 'task' naming convention.
waharnum Aug 22, 2016
d4e918e
FLUID-5936: comment further on the promise-based test execution
waharnum Aug 22, 2016
ad41acf
FLUID-5936: use task naming convention properly.
waharnum Aug 22, 2016
6f8f445
FLUID-5936: queueing implementation, async wrapper for all speech com…
waharnum Aug 25, 2016
09caf85
FLUID-5936: refactoring
waharnum Aug 25, 2016
5a73622
FLUID-5936: further refactoring
waharnum Aug 25, 2016
5129f87
FLUID-5936: more refactoring, more aggressive testing
waharnum Aug 25, 2016
e35df8e
FLUID-5936: updated comment.
waharnum Aug 25, 2016
1e5af4d
FLUID-5936: refactoring
waharnum Aug 25, 2016
7db392b
FLUID-5936: store the currentUtterance on a member of the component t…
waharnum Aug 29, 2016
2accf4f
FLUID-5936: reduce volume of test to 0
waharnum Aug 29, 2016
543b95d
Merge pull request #8 from fluid-project/master
waharnum Aug 30, 2016
71f116b
Merge branch 'master' into FLUID-5936
waharnum Aug 30, 2016
df09711
NOJIRA: add keyword-spacing rule to linter, correct two files for tha…
waharnum Aug 30, 2016
98a0330
Merge branch 'keyword-spacing' into FLUID-5936
waharnum Aug 30, 2016
00bedf1
FLUID-5936: lintin
waharnum Aug 30, 2016
e9cd164
FLUID-5936: comment in accidental comment out.
waharnum Aug 30, 2016
71bc4ec
FLUID-5936: place currentUtterance in queue structure with texts.
waharnum Aug 30, 2016
5b2c2da
FLUID-5936: add some tests for the currentUtterance implementation.
waharnum Aug 30, 2016
75f3de3
FLUID-5936: remove onpause/onresume events
waharnum Aug 30, 2016
f4d3152
FLUID-5936: modelizing
waharnum Aug 30, 2016
e9e6069
FLUID-5936: implement throttle-based control
waharnum Sep 1, 2016
265f3fe
FLUID-5936: remove unneeded function.
waharnum Sep 1, 2016
839f735
FLUID-5936: store utterance as part of queue array.
waharnum Sep 1, 2016
fe384eb
FLUID-5936: refactor queue implementation to clear queue item on hand…
waharnum Sep 1, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 60 additions & 26 deletions src/components/textToSpeech/js/TextToSpeech.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ var fluid_2_0_0 = fluid_2_0_0 || {};
var toSpeak = new SpeechSynthesisUtterance(" "); // short text to attempt to speak
toSpeak.volume = 0; // mutes the Speech Synthesizer
var timeout = setTimeout(function () {
speechSynthesis.cancel();
fluid.textToSpeech.asyncSpeechSynthesisControl("cancel", 10);
promise.reject();
}, delay || 1000);
toSpeak.onstop = function () {
toSpeak.onend = function () {
clearTimeout(timeout);
speechSynthesis.cancel();
fluid.textToSpeech.asyncSpeechSynthesisControl("cancel", 10);
promise.resolve();
};
speechSynthesis.speak(toSpeak);
fluid.textToSpeech.asyncSpeechSynthesisControl("speak", 10, toSpeak);
} else {
setTimeout(promise.reject, 0);
}
Expand All @@ -72,9 +72,11 @@ var fluid_2_0_0 = fluid_2_0_0 || {};
onSpeechQueued: null
},
members: {
queue: []
queue: [],
//
currentUtterance: {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my suggestion of sticking this in the queue is better, since otherwise you have to make extra effort to clean it up when the utterance is done.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The queue member contains the queue of texts yet to be spoken; it has nothing to do with the queueing behaviour I implemented, which (as you state above) is probably more correctly described as a throttle to prevent more pause/resume commands from being sent via the wrapper while outstanding pause/resume requests are still resolving.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right - the queue has nothing to do with your throttling, but it's still an appropriate place for the utterance. Note that it is created one for one with each queue element: https://github.com/fluid-project/infusion/blob/master/src/components/textToSpeech/js/TextToSpeech.js#L188

},
// Model paths: speaking, pending, paused, utteranceOpts
// Model paths: speaking, pending, paused, utteranceOpts, pauseRequested, resumeRequested
model: {
// Changes to the utteranceOpts will only text that is queued after the change.
// All of these options can be overriden in the queueSpeech method by passing in
Expand All @@ -83,7 +85,7 @@ var fluid_2_0_0 = fluid_2_0_0 || {};
utteranceOpts: {
// text: "", // text to synthesize. avoid as it will override any other text passed in
// lang: "", // the language of the synthesized text
// voiceURI: "" // a uri pointing at a voice synthesizer to use. If not set, will use the default one provided by the browser
// voice: {} // a WebSpeechSynthesis object; if not set, will use the default one provided by the browser
// volume: 1, // a value between 0 and 1
// rate: 1, // a value from 0.1 to 10 although different synthesizers may have a smaller range
// pitch: 1, // a value from 0 to 2
Expand All @@ -93,10 +95,6 @@ var fluid_2_0_0 = fluid_2_0_0 || {};
"speaking": {
listener: "fluid.textToSpeech.speak",
args: ["{that}", "{change}.value"]
},
"paused": {
listener: "fluid.textToSpeech.pause",
args: ["{that}", "{change}.value"]
}
},
invokers: {
Expand All @@ -109,12 +107,12 @@ var fluid_2_0_0 = fluid_2_0_0 || {};
args: ["{that}"]
},
pause: {
"this": "speechSynthesis",
"method": "pause"
funcName: "fluid.textToSpeech.issueControlRequest",
args: ["{that}", "pauseRequested", "pause", false]
},
resume: {
"this": "speechSynthesis",
"method": "resume"
funcName: "fluid.textToSpeech.issueControlRequest",
args: ["{that}", "resumeRequested", "resume", true]
},
getVoices: {
"this": "speechSynthesis",
Expand All @@ -132,25 +130,55 @@ var fluid_2_0_0 = fluid_2_0_0 || {};
},
handleError: "{that}.events.onError.fire",
handlePause: {
changePath: "paused",
value: true
funcName: "fluid.textToSpeech.handlePause",
args: ["{that}"]
},
handleResume: {
changePath: "paused",
value: false
funcName: "fluid.textToSpeech.handleResume",
args: ["{that}"]
}
}
});

// Issue commands to the speechSynthesis interface after a delay; this
// makes the wrapper behave somewhat better when issuing commands, especially
// play and pause
fluid.textToSpeech.asyncSpeechSynthesisControl = function (control, delay, args) {
setTimeout(function () {
speechSynthesis[control](args);
}, delay);
};

fluid.textToSpeech.speak = function (that, speaking) {
that.events[speaking ? "onStart" : "onStop"].fire();
};

fluid.textToSpeech.pause = function (that, paused) {
if (paused) {
that.events.onPause.fire();
} else if (that.model.speaking) {
that.events.onResume.fire();
fluid.textToSpeech.handlePause = function (that) {
that.applier.change("paused", true);
that.events.onPause.fire();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to duplicate the function of the model events with a regular event

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One (possible) argument to make for onPause and onResume events when they could be handled by model events is that it keeps a parallel non-model event structure in the component to the events in the wrapped WebSpeechSynthesis API.

Second possible point: it's a somewhat fine-grained (and possibly redundant) distinction but the model change represents a change to the component-tracked state of the speech, while the non-model events represent callbacks from the wrapped browser API about the state of the utterance.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't go along with either of these arguments :) The whole purpose of having a model idiom is to avoid the proliferation of "uninterpreted instants in time". You have 2 events and 4 invokers, when you could have a simple flag in a model and a call to a standard algorithm. Ideally we never have "non-model event structure", parallel or otherwise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Point taken - but in this case, we do need to establish some level of communication and sequencing between the wrapper component and the WebSpeechSynthesis API (so that the model doesn't enter a state that doesn't actually reflect what's happening with the browser API, which is where some of the problems have centred working on the issue of irregularly failing tests). But I'll look for ways to simplify.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A further note on the non-model events: the public API as documented at http://docs.fluidproject.org/infusion/development/TextToSpeechAPI.html includes the onPause and onResume events.

I'm uncertain how much removing them might impact any downstream projects, if at all.

// Clear to issue any waiting resume command
fluid.textToSpeech.clearControlRequest(that, "resumeRequested", "resume");
};

fluid.textToSpeech.handleResume = function (that) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two handleXxxx can be eliminated

that.applier.change("paused", false);
that.events.onResume.fire();
// Clear to issue any waiting pause command
fluid.textToSpeech.clearControlRequest(that, "pauseRequested", "pause");
};

fluid.textToSpeech.clearControlRequest = function (that, modelBoolPath, controlName) {

@amb26 amb26 Aug 29, 2016

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be replaced with a straightforward modelListener which performs a "debounce" on the model state. Consult @jobara who has a mothballed pull request containing this algorithm. #566 - given we have also ripped it off here - https://github.com/fluid-project/node-jqunit/blob/master/lib/jqUnit-node.js#L158 - our "rule of thumb" suggests that this now needs to go into the core framework

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok - the guide is helpful to remind me too - the algorithm we want is actually "throttle" (with trailing) rather than "debounce" :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense per comment above about Rule of Thumb to simply move fluid.debounce from the jqUnit implementation and borrow the Underscore implementation of throttle for fluid.throttle? Since I have need of them here, I can do this as part of the PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone ahead and adapted Underscore's implementation of throttle (with credit and license) in the component itself.

if(fluid.get(that.model, modelBoolPath)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember to lint!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got my pre-commit linter running now on my Infusion project, so it runs (and aborts the commit on any linting errors) before every commit. Whatever the issue the .eslintrc.json config in Infusion isn't catching it. (made sure to update to latest upstream master before saying this...)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, looks like we need to add in this rule to our mix: http://eslint.org/docs/rules/keyword-spacing

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made a small NOJIRA PR for this (and fixed two files that had the same style issue as part of it): #741

that.applier.change(modelBoolPath, false);
fluid.textToSpeech.asyncSpeechSynthesisControl(controlName, 10);
}
};

fluid.textToSpeech.issueControlRequest = function (that, modelBoolPath, controlName, invert) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be eliminated and the invokers replaced with a "changePath" record - http://docs.fluidproject.org/infusion/development/ChangeApplierAPI.html#declarative-style-for-triggering-a-change

if(invert ? !that.model.paused : that.model.paused) {
that.applier.change(modelBoolPath, true);
} else {
fluid.textToSpeech.asyncSpeechSynthesisControl(controlName, 10);
}
};

Expand Down Expand Up @@ -186,6 +214,12 @@ var fluid_2_0_0 = fluid_2_0_0 || {};
};

var toSpeak = new SpeechSynthesisUtterance(text);
// Store toSpeak additional on the currentUtterance member to help deal with the issue
// with premature garbage collection described at https://bugs.chromium.org/p/chromium/issues/detail?id=509488#c11
// this makes the speech synthesis behave much better in Safari in
// particular
// that.currentUtterance = toSpeak;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently commented out

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The perils of running 50 differential manual tests at the end of the day is that you do things like this and comment out the actual fix for the problem.


var eventBinding = {
onstart: that.handleStart,
onend: that.handleEnd,
Expand All @@ -197,12 +231,12 @@ var fluid_2_0_0 = fluid_2_0_0 || {};

that.queue.push(text);
that.events.onSpeechQueued.fire(text);
speechSynthesis.speak(toSpeak);
fluid.textToSpeech.asyncSpeechSynthesisControl("speak", 10, toSpeak);
};

fluid.textToSpeech.cancel = function (that) {
that.queue = [];
speechSynthesis.cancel();
fluid.textToSpeech.asyncSpeechSynthesisControl("cancel", 10);
};

})(jQuery, fluid_2_0_0);
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
<script type="text/javascript" src="../../../test-core/jqUnit/js/jqUnit-browser.js"></script>
<script type="text/javascript" src="../../../lib/jquery-ui/js/jquery.simulate.js"></script>

<!-- needed for IoC testing framework -->
<script type="text/javascript" src="../../../../src/framework/enhancement/js/ContextAwareness.js"></script>
<script src="../../../test-core/utils/js/IoCTestUtils.js"></script>

<!-- These are tests that have been written using this page as data and test supports -->
<script type="text/javascript" src="../js/TextToSpeechTests.js"></script>

Expand Down
Loading