2018-06-05 17:51:55 -04:00
|
|
|
const formatMessage = require('format-message');
|
|
|
|
const nets = require('nets');
|
|
|
|
|
|
|
|
const ArgumentType = require('../../extension-support/argument-type');
|
|
|
|
const BlockType = require('../../extension-support/block-type');
|
|
|
|
const Cast = require('../../util/cast');
|
|
|
|
const log = require('../../util/log');
|
|
|
|
|
|
|
|
/**
|
2018-06-05 18:32:02 -04:00
|
|
|
* The url of the synthesis server.
|
2018-06-05 17:51:55 -04:00
|
|
|
* @type {string}
|
|
|
|
*/
|
|
|
|
const SERVER_HOST = 'https://synthesis-service.scratch.mit.edu';
|
|
|
|
|
|
|
|
/**
|
2018-06-05 18:32:02 -04:00
|
|
|
* How long to wait in ms before timing out requests to synthesis server.
|
2018-06-05 17:51:55 -04:00
|
|
|
* @type {int}
|
|
|
|
*/
|
2018-07-09 14:34:31 -04:00
|
|
|
const SERVER_TIMEOUT = 10000; // 10 seconds
|
2018-06-05 17:51:55 -04:00
|
|
|
|
|
|
|
/**
|
2018-06-05 18:32:02 -04:00
|
|
|
* Class for the synthesis block in Scratch 3.0.
|
2018-06-05 17:51:55 -04:00
|
|
|
* @constructor
|
|
|
|
*/
|
|
|
|
class Scratch3SpeakBlocks {
|
2018-07-09 14:34:31 -04:00
|
|
|
constructor (runtime) {
|
|
|
|
/**
|
|
|
|
* The runtime instantiating this block package.
|
|
|
|
* @type {Runtime}
|
|
|
|
*/
|
|
|
|
this.runtime = runtime;
|
|
|
|
|
|
|
|
// Clear sound effects on green flag and stop button events.
|
|
|
|
// this._clearEffectsForAllTargets = this._clearEffectsForAllTargets.bind(this);
|
|
|
|
if (this.runtime) {
|
|
|
|
// @todo
|
|
|
|
// this.runtime.on('PROJECT_STOP_ALL', this._clearEffectsForAllTargets);
|
|
|
|
}
|
|
|
|
|
2018-06-05 17:51:55 -04:00
|
|
|
/**
|
|
|
|
* Locale code of the viewer
|
|
|
|
* @type {string}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
this._language = this.getViewerLanguageCode();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-05 18:32:02 -04:00
|
|
|
* The key to load & store a target's synthesis state.
|
2018-06-05 17:51:55 -04:00
|
|
|
* @return {string} The key.
|
|
|
|
*/
|
|
|
|
static get STATE_KEY () {
|
2018-07-09 14:34:31 -04:00
|
|
|
return 'Scratch.text2speech';
|
2018-06-05 17:51:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @returns {object} metadata for this extension and its blocks.
|
|
|
|
*/
|
|
|
|
getInfo () {
|
|
|
|
return {
|
2018-07-09 14:34:31 -04:00
|
|
|
id: 'text2speech',
|
|
|
|
name: 'Text-to-Speech',
|
2018-06-05 17:51:55 -04:00
|
|
|
menuIconURI: '', // @todo Add the final icons.
|
|
|
|
blockIconURI: '',
|
|
|
|
blocks: [
|
|
|
|
{
|
2018-06-05 18:32:02 -04:00
|
|
|
opcode: 'speakAndWait',
|
2018-06-05 17:51:55 -04:00
|
|
|
text: formatMessage({
|
2018-06-05 18:32:02 -04:00
|
|
|
id: 'speak.speakAndWaitBlock',
|
2018-06-05 17:51:55 -04:00
|
|
|
default: 'speak [WORDS]',
|
|
|
|
description: 'speak some words'
|
|
|
|
}),
|
|
|
|
blockType: BlockType.COMMAND,
|
|
|
|
arguments: {
|
|
|
|
WORDS: {
|
|
|
|
type: ArgumentType.STRING,
|
|
|
|
defaultValue: formatMessage({
|
|
|
|
id: 'speak.defaultTextToSpeak',
|
|
|
|
default: 'hello',
|
|
|
|
description: 'hello: the default text to speak'
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
],
|
|
|
|
menus: {
|
|
|
|
voices: this.supportedVoices
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the viewer's language code.
|
|
|
|
* @return {string} the language code.
|
|
|
|
*/
|
|
|
|
getViewerLanguageCode () {
|
|
|
|
// @todo This should be the language code of the project *creator*
|
|
|
|
// rather than the project viewer.
|
|
|
|
return navigator.language || navigator.userLanguage || 'en-US';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the supported voices for the extension.
|
|
|
|
* @return {Array} Supported voices
|
|
|
|
*/
|
|
|
|
supportedVoices () {
|
|
|
|
return [
|
|
|
|
'Quinn',
|
|
|
|
'Max',
|
|
|
|
'Squeak',
|
|
|
|
'Monster',
|
|
|
|
'Puppy'
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert the provided text into a sound file and then play the file.
|
|
|
|
* @param {object} args Block arguments
|
|
|
|
* @return {Promise} A promise that resolves after playing the sound
|
|
|
|
*/
|
2018-06-05 18:32:02 -04:00
|
|
|
speakAndWait (args) {
|
2018-06-05 17:51:55 -04:00
|
|
|
// Cast input to string
|
|
|
|
args.WORDS = Cast.toString(args.WORDS);
|
|
|
|
|
|
|
|
// Build up URL
|
|
|
|
let path = `${SERVER_HOST}/synth`;
|
|
|
|
path += `?locale=${this._language}`;
|
|
|
|
path += `&gender=male`; // @todo
|
|
|
|
path += `&text=${encodeURI(args.WORDS)}`;
|
|
|
|
|
|
|
|
// Perform HTTP request to get audio file
|
|
|
|
return new Promise(resolve => {
|
|
|
|
nets({
|
|
|
|
url: path,
|
|
|
|
timeout: SERVER_TIMEOUT
|
|
|
|
}, (err, res, body) => {
|
|
|
|
if (err) {
|
|
|
|
log.warn(err);
|
|
|
|
return resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (res.statusCode !== 200) {
|
|
|
|
log.warn(res.statusCode);
|
|
|
|
return resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Play the sound
|
2018-07-09 14:34:31 -04:00
|
|
|
const sound = {
|
|
|
|
// md5: 'test',
|
|
|
|
// name: 'test',
|
|
|
|
// format: 'audio/mpg',
|
|
|
|
data: {
|
|
|
|
buffer: body.buffer
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this.runtime.audioEngine.decodeSoundPlayer(sound).then(soundPlayer => {
|
|
|
|
soundPlayer.connect(this.runtime.audioEngine);
|
|
|
|
soundPlayer.play();
|
|
|
|
soundPlayer.on('stop', resolve);
|
2018-06-05 17:51:55 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
module.exports = Scratch3SpeakBlocks;
|