-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f0d6435
Showing
8 changed files
with
402 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
Internet Systems Consortium license | ||
=================================== | ||
|
||
Copyright (c) 2016, Ives De Bruycker | ||
|
||
Permission to use, copy, modify, and/or distribute this software for any purpose | ||
with or without fee is hereby granted, provided that the above copyright notice | ||
and this permission notice appear in all copies. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | ||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND | ||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | ||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS | ||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER | ||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF | ||
THIS SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
{ | ||
"page": { | ||
"label": "Text-to-speech Configuration" | ||
}, | ||
"sections": [ | ||
{ | ||
"id": "section_tts", | ||
"element": "section", | ||
"label": "Text-to-speech", | ||
"icon": "fa-list-ol", | ||
"onSave": {"type":"controller", "endpoint":"miscellanea/text_to_speech", "method":"savePluginOptions"}, | ||
"saveButton": { | ||
"label": "Save", | ||
"data": [ | ||
"accessKeyId", | ||
"secretAccessKey", | ||
"voice", | ||
"pauseWhenTalking", | ||
"announcer" | ||
] | ||
}, | ||
"content": [ | ||
{ | ||
"id": "accessKeyId", | ||
"type":"text", | ||
"element": "input", | ||
"doc": "AWS access key id", | ||
"label": "AWS access key id", | ||
"value": "" | ||
}, | ||
{ | ||
"id":"secretAccessKey", | ||
"type":"text", | ||
"element": "input", | ||
"doc": "AWS secret access key", | ||
"label": "AWS secret access key", | ||
"value": "" | ||
}, | ||
{ | ||
"id":"voice", | ||
"type":"text", | ||
"element": "input", | ||
"doc": "Voice", | ||
"label": "Voice", | ||
"value": "" | ||
}, | ||
{ | ||
"id":"pauseWhenTalking", | ||
"element": "switch", | ||
"doc": "Pause when talking", | ||
"label": "Pause when talking", | ||
"value": true | ||
}, | ||
{ | ||
"id":"announcer", | ||
"element": "switch", | ||
"doc": "Announcer", | ||
"label": "Automatically announce track info", | ||
"value": false | ||
} | ||
] | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"accessKeyId":{ | ||
"type": "string", | ||
"value": "" | ||
}, | ||
"secretAccessKey":{ | ||
"type": "string", | ||
"value": "" | ||
}, | ||
"voice":{ | ||
"type": "string", | ||
"value": "Kimberly" | ||
}, | ||
"pauseWhenTalking":{ | ||
"type": "boolean", | ||
"value": true | ||
}, | ||
"announcer":{ | ||
"type": "boolean", | ||
"value": false | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
'use strict'; | ||
|
||
var libQ = require('kew'); | ||
const AWS = require('aws-sdk') | ||
const Stream = require('stream') | ||
const Speaker = require('speaker') | ||
var exec = require('child_process').exec; | ||
|
||
module.exports = text_to_speech; | ||
|
||
function text_to_speech(context) { | ||
|
||
var self = this; | ||
|
||
this.context = context; | ||
this.commandRouter = this.context.coreCommand; | ||
this.logger = this.context.logger; | ||
this.configManager = this.context.configManager; | ||
this.playlistManager = this.context.playlistManager; | ||
} | ||
|
||
text_to_speech.prototype.onVolumioStart = function () { | ||
var self = this; | ||
self.config = new (require('v-conf'))(); | ||
var configFile = self.commandRouter.pluginManager.getConfigurationFile(self.context,'config.json'); | ||
self.config.loadFile(configFile); | ||
|
||
return libQ.resolve(); | ||
}; | ||
|
||
text_to_speech.prototype.onStart = function () { | ||
var self = this; | ||
|
||
self.websocketPlugin = this.commandRouter.pluginManager.getPlugin('user_interface', 'websocket'); | ||
self.websocketPlugin.libSocketIO.on('connection', function (connWebSocket) { | ||
connWebSocket.on('tts-say', function (data) { | ||
// TODO: type ssml | ||
// TODO: queue sentences | ||
if (data.text) { | ||
var pauseAndResume = self.config.data.pauseWhenTalking.value && self.commandRouter.volumioGetState().status === 'play'; | ||
|
||
self.speak(data.text, function () { | ||
if (pauseAndResume) { | ||
self.commandRouter.volumioPause.bind(self.commandRouter)(); | ||
} | ||
}, function () { | ||
if (pauseAndResume) { | ||
self.commandRouter.volumioPlay.bind(self.commandRouter)(); | ||
} | ||
}); | ||
} | ||
}); | ||
connWebSocket.on('tts-announce', function () { | ||
self.announceTrack(self.commandRouter.volumioGetState(), self.config.data.pauseWhenTalking.value); | ||
}); | ||
}); | ||
|
||
self.commandRouter.addCallback("volumioPushState", function (state) { | ||
if (self.config.data.announcer.value) { | ||
var announcement = self.generateAnnouncement(state); | ||
if (state.status === "play" && self.lastAnnouncement !== announcement) { | ||
self.speak(announcement); | ||
self.lastAnnouncement = announcement; | ||
} | ||
} | ||
}); | ||
|
||
self.Polly = new AWS.Polly({ | ||
accessKeyId: self.config.data.accessKeyId.value, | ||
secretAccessKey: self.config.data.secretAccessKey.value, | ||
signatureVersion: 'v4', | ||
region: 'eu-west-1' | ||
}); | ||
|
||
return libQ.resolve(); | ||
} | ||
|
||
text_to_speech.prototype.getConfigurationFiles = function() { | ||
var self = this; | ||
|
||
return ['config.json']; | ||
}; | ||
|
||
text_to_speech.prototype.onStop = function () { | ||
var self = this; | ||
//Perform stop tasks here | ||
return libQ.resolve(); | ||
}; | ||
|
||
text_to_speech.prototype.onRestart = function () { | ||
var self = this; | ||
//Perform restart tasks here | ||
}; | ||
|
||
text_to_speech.prototype.onInstall = function () { | ||
var self = this; | ||
//Perform your installation tasks here | ||
}; | ||
|
||
text_to_speech.prototype.onUninstall = function () { | ||
var self = this; | ||
//Perform your deinstallation tasks here | ||
}; | ||
|
||
text_to_speech.prototype.getUIConfig = function() { | ||
var defer = libQ.defer(); | ||
var self = this; | ||
|
||
var lang_code = this.commandRouter.sharedVars.get('language_code'); | ||
|
||
self.commandRouter.i18nJson(__dirname+'/i18n/strings_' + lang_code + '.json', | ||
__dirname+'/i18n/strings_en.json', | ||
__dirname + '/UIConfig.json') | ||
.then(function(uiconf) | ||
{ | ||
uiconf.sections[0].content[0].value = self.config.get('accessKeyId'); | ||
uiconf.sections[0].content[1].value = self.config.get('secretAccessKey'); | ||
uiconf.sections[0].content[2].value = self.config.get('voice'); | ||
uiconf.sections[0].content[3].value = self.config.get('pauseWhenTalking'); | ||
uiconf.sections[0].content[4].value = self.config.get('announcer'); | ||
defer.resolve(uiconf); | ||
}) | ||
.fail(function() | ||
{ | ||
defer.reject(new Error()); | ||
}); | ||
|
||
return defer.promise; | ||
}; | ||
|
||
text_to_speech.prototype.setUIConfig = function (data) { | ||
var self = this; | ||
//Perform your UI configuration tasks here | ||
}; | ||
|
||
text_to_speech.prototype.getConf = function (varName) { | ||
var self = this; | ||
//Perform your tasks to fetch config data here | ||
}; | ||
|
||
text_to_speech.prototype.setConf = function (varName, varValue) { | ||
var self = this; | ||
//Perform your tasks to set config data here | ||
}; | ||
|
||
//Optional functions exposed for making development easier and more clear | ||
text_to_speech.prototype.getSystemConf = function (pluginName, varName) { | ||
var self = this; | ||
//Perform your tasks to fetch system config data here | ||
}; | ||
|
||
text_to_speech.prototype.setSystemConf = function (pluginName, varName) { | ||
var self = this; | ||
//Perform your tasks to set system config data here | ||
}; | ||
|
||
text_to_speech.prototype.getAdditionalConf = function () { | ||
var self = this; | ||
//Perform your tasks to fetch additional config data here | ||
}; | ||
|
||
text_to_speech.prototype.setAdditionalConf = function () { | ||
var self = this; | ||
//Perform your tasks to set additional config data here | ||
}; | ||
|
||
text_to_speech.prototype.savePluginOptions = function (data) { | ||
var self = this; | ||
|
||
var defer = libQ.defer(); | ||
|
||
var credentialsChanged = self.config.data.accessKeyId.value !== data.accessKeyId.value || self.config.data.secretAccessKey.value !== data.secretAccessKey.value; | ||
|
||
self.config.set('accessKeyId', data.accessKeyId); | ||
self.config.set('secretAccessKey', data.secretAccessKey); | ||
self.config.set('voice', data.voice); | ||
self.config.set('pauseWhenTalking', data.pauseWhenTalking); | ||
self.config.set('announcer', data.announcer); | ||
|
||
self.logger.info('Text-to-speech configurations have been set'); | ||
|
||
self.commandRouter.pushToastMessage('success', 'Text-to-speech', 'Configuration saved'); | ||
|
||
if (credentialsChanged) { | ||
// reload Polly, in case credentials are changed | ||
self.Polly = new AWS.Polly({ | ||
accessKeyId: self.config.data.accessKeyId.value, | ||
secretAccessKey: self.config.data.secretAccessKey.value, | ||
signatureVersion: 'v4', | ||
region: 'eu-west-1' | ||
}); | ||
} | ||
|
||
defer.resolve({}); | ||
|
||
return defer.promise; | ||
|
||
}; | ||
|
||
text_to_speech.prototype.generateAnnouncement = function (state) { | ||
var announcement = '"' + state.title + '"'; | ||
announcement += state.artist ? ' by "' + state.artist + '"' : ' by an unknown artist'; | ||
if (state.album) { | ||
announcement += ' from the album "' + state.album + '"'; | ||
} | ||
|
||
return announcement; | ||
} | ||
|
||
text_to_speech.prototype.announceTrack = function (state, pauseWhenTalking) { | ||
var self = this; | ||
|
||
var announcement = self.generateAnnouncement(state); | ||
var pauseAndResume = pauseWhenTalking && state.status === 'play'; | ||
|
||
self.speak(announcement, function () { | ||
if (pauseAndResume) { | ||
self.commandRouter.volumioPause.bind(self.commandRouter)(); | ||
} | ||
}, function () { | ||
if (pauseAndResume) { | ||
self.commandRouter.volumioPlay.bind(self.commandRouter)(); | ||
} | ||
}); | ||
} | ||
|
||
text_to_speech.prototype.speak = function (text, callbackReadyToSpeak, callbackDoneSpeaking) { | ||
var self = this; | ||
var params = { | ||
'Text': text, | ||
'OutputFormat': 'pcm', | ||
'VoiceId': self.config.data.voice.value || 'Kimberly', | ||
'SampleRate': '16000' | ||
}; | ||
self.Polly.synthesizeSpeech(params, (err, data) => { | ||
if (err) { | ||
console.log(err.code) | ||
} else if (data) { | ||
callbackReadyToSpeak && callbackReadyToSpeak(); | ||
if (data.AudioStream instanceof Buffer) { | ||
var speakerInstance = new Speaker({ | ||
channels: 1, | ||
bitDepth: 16, | ||
sampleRate: 16000 | ||
}); | ||
speakerInstance.on('close', function () { | ||
callbackDoneSpeaking && callbackDoneSpeaking(); | ||
}); | ||
try { | ||
// Initiate the source | ||
var bufferStream = new Stream.PassThrough() | ||
// convert AudioStream into a readable stream | ||
bufferStream.end(data.AudioStream) | ||
// Pipe into Player | ||
bufferStream.pipe(speakerInstance) | ||
} catch(e) { | ||
console.log('Error', e); | ||
callbackDoneSpeaking && callbackDoneSpeaking(); | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
|
||
/*process.on('uncaughtException', function (err) { | ||
console.log('UNCAUGHT EXCEPTION - keeping process alive:', err); | ||
});*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
#!/bin/bash | ||
|
||
echo "Install NodeJS dependencies" | ||
( cd /data/plugins/miscellanea/text_to_speech && npm install ) | ||
chown -R volumio:volumio /data/plugins/miscellanea/text_to_speech | ||
|
||
echo "plugininstallend" |
Oops, something went wrong.