Initial commit
This commit is contained in:
@@ -0,0 +1,518 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
|
||||
function Speech2Device(adapter, libs, options) {
|
||||
const sayitFuncs = {
|
||||
browser: {func: sayItBrowser },
|
||||
mp24ftp: {func: sayItMP24ftp },
|
||||
mp24: {func: sayItMP24 },
|
||||
system: {func: sayItSystem },
|
||||
windows: {func: sayItWindows },
|
||||
sonos: {func: sayItSonos },
|
||||
chromecast: {func: sayItChromecast },
|
||||
mpd: {func: sayItMpd },
|
||||
googleHome: {func: sayItGoogleHome }
|
||||
};
|
||||
const MP3FILE = path.normalize(__dirname + '/../' + adapter.namespace + '.say.mp3');
|
||||
const that = this;
|
||||
|
||||
// Function to fill the .tts.mp3 state
|
||||
function uploadToStates(text, callback) {
|
||||
let fileData;
|
||||
if (isPlayFile(text)) {
|
||||
text = path.normalize(text);
|
||||
try {
|
||||
fileData = libs.fs.readFileSync(text);
|
||||
} catch (e) {
|
||||
callback && callback('Cannot upload file "' + text + '" to state: ' + e.toString());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
fileData = libs.fs.readFileSync(MP3FILE);
|
||||
} catch (e) {
|
||||
callback && callback('Cannot upload file "' + MP3FILE + '" to state: ' + e.toString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
adapter.setBinaryState(adapter.namespace + '.tts.mp3', fileData, callback);
|
||||
}
|
||||
|
||||
// Google home
|
||||
let Client;
|
||||
let DefaultMediaReceiver;
|
||||
|
||||
function isPlayFile(text) {
|
||||
if (text.length > 4) {
|
||||
const ext = text.substring(text.length - 4).toLowerCase();
|
||||
if (ext === '.mp3' || ext === '.wav') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// If text is gong.mp3 or bong.wav
|
||||
function sayItGetFileName(text) {
|
||||
if (isPlayFile(text)) {
|
||||
if (libs.fs.existsSync(text)) {
|
||||
return text;
|
||||
} else {
|
||||
return __dirname + '/' + text;
|
||||
}
|
||||
}
|
||||
|
||||
return MP3FILE;
|
||||
}
|
||||
|
||||
function sayItBrowser(error, text, language, volume, duration, callback) {
|
||||
if (error) {
|
||||
callback && callback(error, 0);
|
||||
return;
|
||||
}
|
||||
uploadToStates(text, err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
adapter.setForeignState('vis.0.control.instance', adapter.config.instance);
|
||||
adapter.setForeignState('vis.0.control.data', '/state/' + adapter.namespace + '.tts.mp3');
|
||||
adapter.setForeignState('vis.0.control.command', 'playSound');
|
||||
callback && callback(null, duration);
|
||||
});
|
||||
}
|
||||
|
||||
function sayItSonos(error, text, language, volume, duration, callback) {
|
||||
if (error) {
|
||||
callback && callback(error, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
volume = volume || options.sayLastVolume;
|
||||
uploadToStates(text, err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
if (volume === 'null') volume = 0;
|
||||
|
||||
if (adapter.config.device && options.webLink) {
|
||||
adapter.log.info('Set "' + adapter.config.device + '.tts: ' + (volume ? (volume + ';') : '') + options.webLink + '/state/' + adapter.namespace + '.tts.mp3');
|
||||
adapter.setForeignState(adapter.config.device + '.tts', (volume ? (volume + ';') : '') + options.webLink + '/state/' + adapter.namespace + '.tts.mp3');
|
||||
} else if (options.webLink) {
|
||||
adapter.log.info('Send to sonos ' + (volume ? (volume + ';') : '') + options.webLink + '/state/' + adapter.namespace + '.tts.mp3');
|
||||
adapter.sendTo('sonos', 'send', (volume ? (volume + ';') : '') + options.webLink + '/state/' + adapter.namespace + '.tts.mp3');
|
||||
} else {
|
||||
adapter.log.warn('Web server is unavailable!');
|
||||
}
|
||||
callback && callback(null, duration);
|
||||
});
|
||||
}
|
||||
|
||||
function sayItMpd(error, text, language, volume, duration, callback) {
|
||||
if (error) {
|
||||
callback && callback(error, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
volume = volume || options.sayLastVolume;
|
||||
uploadToStates(text, err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
if (volume === 'null') volume = 0;
|
||||
|
||||
if (adapter.config.mpd_device && options.webLink) {
|
||||
adapter.log.info('Set "' + adapter.config.mpd_device + '.say: ' + (volume ? (volume + ';') : '') + options.webLink + '/state/' + adapter.namespace + '.tts.mp3');
|
||||
adapter.setForeignState(adapter.config.mpd_device + '.say', (volume ? (volume + ';') : '') + options.webLink + '/state/' + adapter.namespace + '.tts.mp3');
|
||||
} else if (options.webLink) {
|
||||
adapter.log.info('Send to MPD ' + (volume ? (volume + ';') : '') + options.webLink + '/state/' + adapter.namespace + '.tts.mp3');
|
||||
adapter.sendTo('mpd', 'say', (volume ? (volume + ';') : '') + options.webLink + '/state/' + adapter.namespace + '.tts.mp3');
|
||||
} else {
|
||||
adapter.log.warn('Web server is unavailable!');
|
||||
}
|
||||
callback && callback(null, duration);
|
||||
});
|
||||
}
|
||||
|
||||
function sayItChromecast(error, text, language, volume, duration, callback) {
|
||||
if (error) {
|
||||
callback && callback(error, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
volume = volume || options.sayLastVolume;
|
||||
uploadToStates(text, err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
if (volume === 'null') volume = 0;
|
||||
|
||||
//Create announcement JSON
|
||||
const announcement = {
|
||||
url: options.webLink + '/state/' + adapter.namespace + '.tts.mp3'
|
||||
};
|
||||
if (volume) {
|
||||
announcement.volume = volume;
|
||||
}
|
||||
const announcementJSON = JSON.stringify(announcement);
|
||||
|
||||
if (adapter.config.cDevice && options.webLink) {
|
||||
const chromecastAnouncementDev = adapter.config.cDevice + '.player.announcement';
|
||||
adapter.log.info('Set "' + chromecastAnouncementDev + ' to ' + announcementJSON);
|
||||
adapter.setForeignState(chromecastAnouncementDev, announcementJSON, () => {
|
||||
//Check every 500 ms if the announcement has finished playing
|
||||
let intervalHandler = setInterval(() => {
|
||||
adapter.getForeignState(chromecastAnouncementDev, (err, state) => {
|
||||
if (!err && state.ack) {
|
||||
adapter.log.debug(chromecastAnouncementDev + ' finished playing announcement: ' + announcementJSON);
|
||||
clearInterval(intervalHandler);
|
||||
intervalHandler = null;
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
} else {
|
||||
adapter.log.warn('Web server is unavailable!');
|
||||
}
|
||||
callback && callback(null, duration);
|
||||
});
|
||||
}
|
||||
|
||||
function launchGoogleHome(client, url, callback) {
|
||||
client.launch(DefaultMediaReceiver, (err, player) => {
|
||||
const media = {
|
||||
contentId: url,
|
||||
contentType: 'audio/mp3',
|
||||
streamType: 'BUFFERED' // or LIVE
|
||||
};
|
||||
player.load(media, {autoplay: true}, (err /*, status */) => {
|
||||
if (err) {
|
||||
adapter.log.error(err);
|
||||
}
|
||||
client.close();
|
||||
callback(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendToGoogleHome(host, url, volume, callback) {
|
||||
const client = new Client();
|
||||
client.connect(host, () => {
|
||||
if (volume !== undefined) {
|
||||
client.setVolume({ level: volume / 100 }, (err /* , newvol */) => {
|
||||
if (err) adapter.log.error('there was an error setting the volume: ' + err);
|
||||
launchGoogleHome(client, url, callback);
|
||||
});
|
||||
} else {
|
||||
launchGoogleHome(client, url, callback);
|
||||
}
|
||||
});
|
||||
|
||||
client.on('error', err => {
|
||||
client.close();
|
||||
callback && callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
function sayItGoogleHome(error, text, language, volume, duration, callback) {
|
||||
if (error) {
|
||||
callback && callback(error, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
Client = Client || require('castv2-client').Client;
|
||||
DefaultMediaReceiver = DefaultMediaReceiver || require('castv2-client').DefaultMediaReceiver;
|
||||
|
||||
volume = volume || options.sayLastVolume;
|
||||
uploadToStates(text, err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
if (volume === 'null' || volume === null) volume = 0;
|
||||
|
||||
if (adapter.config.server && options.webLink) {
|
||||
const url = options.webLink + '/state/' + adapter.namespace + '.tts.mp3';
|
||||
adapter.log.debug('Send to google home "' + adapter.config.server + '": ' + url);
|
||||
|
||||
sendToGoogleHome(adapter.config.server, url, volume, err => err && adapter.log.error(err));
|
||||
} else {
|
||||
error = 'Web server is unavailable!';
|
||||
}
|
||||
callback && callback(error, duration);
|
||||
});
|
||||
}
|
||||
|
||||
function sayItMP24(error, text, language, volume, duration, callback) {
|
||||
if (error) {
|
||||
callback && callback(error, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
uploadToStates(text, err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
if (adapter.config.server && !isPlayFile(text)) {
|
||||
adapter.log.debug('Request MediaPlayer24 "http://' + adapter.config.server + ':50000/tts=' + encodeURI(text) + '"');
|
||||
const opts = {
|
||||
host: adapter.config.server,
|
||||
port: 50000,
|
||||
path: '/tts=' + encodeURI(text)
|
||||
};
|
||||
libs.http.get(opts, res => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
// all data has been downloaded
|
||||
res.on('end', () => adapter.log.debug('Response from MediaPlayer24 "' + adapter.config.server + '": ' + body));
|
||||
res.on('error', e => adapter.log.error('Cannot say text on MediaPlayer24 "' + adapter.config.server + '": ' + e.message));
|
||||
}).on('error', e => {
|
||||
if (e.message === 'Parse Error') {
|
||||
adapter.log.debug('Played successfully');
|
||||
} else {
|
||||
error = 'Cannot say text on MediaPlayer24 "' + adapter.config.server + '":' + e.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
callback && callback(error, duration + 2);
|
||||
});
|
||||
}
|
||||
|
||||
function sayItMP24ftp(error, text, language, volume, duration, callback) {
|
||||
if (error) {
|
||||
callback && callback(error, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
uploadToStates(text, err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
// Copy mp3 file to android device to play it later with MediaPlayer
|
||||
if (adapter.config.port && adapter.config.server) {
|
||||
|
||||
const file = sayItGetFileName(text);
|
||||
libs.jsftp = libs.jsftp || require('jsftp');
|
||||
|
||||
const ftp = new libs.jsftp({
|
||||
host: adapter.config.server,
|
||||
port: parseInt(adapter.config.port, 10), // defaults to 21
|
||||
user: adapter.config.user || 'anonymous', // defaults to 'anonymous'
|
||||
pass: adapter.config.pass || 'anonymous' // defaults to 'anonymous'
|
||||
});
|
||||
|
||||
try {
|
||||
// Copy file to FTP server
|
||||
ftp.put(file, adapter.namespace + '.say.mp3', hadError => {
|
||||
if (!hadError) {
|
||||
const opts = {
|
||||
host: adapter.config.server,
|
||||
port: 50000,
|
||||
path: '/track=' + adapter.namespace + '.say.mp3'
|
||||
};
|
||||
|
||||
libs.http.get(opts, res => {
|
||||
let body = '';
|
||||
res.on('data', chunk =>body += chunk);
|
||||
// all data has been downloaded
|
||||
res.on('end', () => adapter.log.debug('Response from MediaPlayer24 "' + adapter.config.server + '": ' + body));
|
||||
res.on('error', e => adapter.log.error('Cannot say text on MediaPlayer24 "' + adapter.config.server + '":' + e.message));
|
||||
}).on('error', e => {
|
||||
if (e.message === 'Parse Error') {
|
||||
adapter.log.debug('Played successfully');
|
||||
} else {
|
||||
adapter.log.error('Cannot say text on MediaPlayer24 "' + adapter.config.server + '":' + e.message);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
adapter.log.error ('FTP error:' + hadError);
|
||||
}
|
||||
ftp.raw('quit', (err /* , data */) => {
|
||||
if (err) adapter.log.error(err);
|
||||
ftp.destroy();
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
error = 'Cannot upload file to ' + adapter.config.server + ':' + adapter.config.port;
|
||||
}
|
||||
}
|
||||
callback && callback(error, duration + 2);
|
||||
});
|
||||
}
|
||||
|
||||
function sayItSystem(error, text, language, volume, duration, callback) {
|
||||
if (error) {
|
||||
callback && callback(error, 0);
|
||||
return;
|
||||
}
|
||||
if (!libs.os) libs.os = require('os');
|
||||
|
||||
uploadToStates(text, err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
||||
const p = libs.os.platform();
|
||||
let ls = null;
|
||||
const file = sayItGetFileName(text);
|
||||
let cmd;
|
||||
|
||||
that.sayItSystemVolume(volume);
|
||||
|
||||
if (adapter.config.command) {
|
||||
//custom command
|
||||
adapter.setState('tts.playing', true);
|
||||
if (adapter.config.command.indexOf('%s') !== -1) {
|
||||
cmd = adapter.config.command.replace('%s', file);
|
||||
} else {
|
||||
if (p.match(/^win/)) {
|
||||
cmd = adapter.config.command + ' "' + file + '"';
|
||||
} else {
|
||||
cmd = adapter.config.command + ' ' + file;
|
||||
}
|
||||
}
|
||||
ls = libs.child_process.exec(cmd, (error /* , stdout, stderr */) => {
|
||||
if (error) adapter.log.error('Cannot play:' + error);
|
||||
adapter.setState('tts.playing', false);
|
||||
});
|
||||
} else {
|
||||
if (p === 'linux') {
|
||||
//linux
|
||||
adapter.setState('tts.playing', true);
|
||||
|
||||
if (adapter.config.player === 'omxplayer') {
|
||||
cmd = 'omxplayer -o local ' + file;
|
||||
} else {
|
||||
cmd = 'mpg321 -g ' + options.sayLastVolume + ' ' + file;
|
||||
}
|
||||
ls = libs.child_process.exec(cmd, (error /* , stdout, stderr */) => {
|
||||
if (error) adapter.log.error('Cannot play:' + error);
|
||||
adapter.setState('tts.playing', false);
|
||||
});
|
||||
} else if (p.match(/^win/)) {
|
||||
//windows
|
||||
adapter.setState('tts.playing', true);
|
||||
ls = libs.child_process.exec('cmdmp3.exe "' + file + '"', {cwd: __dirname + '/../cmdmp3/'}, (error /* , stdout, stderr */) => {
|
||||
if (error) adapter.log.error('Cannot play:' + error);
|
||||
adapter.setState('tts.playing', false);
|
||||
});
|
||||
} else if (p === 'darwin') {
|
||||
//mac osx
|
||||
adapter.setState('tts.playing', true);
|
||||
ls = libs.child_process.exec('/usr/bin/afplay ' + file, (error /* , stdout, stderr */) => {
|
||||
if (error) adapter.log.error('Cannot play:' + error);
|
||||
adapter.setState('tts.playing', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (ls) {
|
||||
ls.on('error', e => {
|
||||
throw new Error('sayIt.play: there was an error while playing the mp3 file:' + e);
|
||||
});
|
||||
}
|
||||
if (text === adapter.config.announce) {
|
||||
callback && callback(null, duration);
|
||||
} else {
|
||||
callback && callback(null, duration + 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sayItWindows(error, text, language, volume, duration, callback) {
|
||||
if (error) {
|
||||
callback && callback(error, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// If mp3 file
|
||||
if (isPlayFile(text)) {
|
||||
sayItSystem(text, language, volume);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!libs.os) libs.os = require('os');
|
||||
|
||||
// Call windows own text 2 speech
|
||||
const p = libs.os.platform();
|
||||
let ls = null;
|
||||
// var file = sayItGetFileName(text);
|
||||
|
||||
if (volume || volume === 0) that.sayItSystemVolume(volume);
|
||||
|
||||
if (p.match(/^win/)) {
|
||||
//windows
|
||||
adapter.setState('tts.playing', true);
|
||||
ls = libs.child_process.exec(__dirname + '/../say/SayStatic.exe ' + text, (error /* , stdout, stderr */) => {
|
||||
if (error) adapter.log.error ('sayItWindows: ' + error);
|
||||
adapter.setState('tts.playing', false);
|
||||
});
|
||||
} else {
|
||||
adapter.log.error ('sayItWindows: only windows OS is supported for Windows default mode');
|
||||
}
|
||||
|
||||
if (ls) {
|
||||
ls.on('error', e => {
|
||||
throw new Error('sayIt.play: there was an error while text2speech on window:' + e);
|
||||
});
|
||||
}
|
||||
callback && callback(null, duration + 2);
|
||||
}
|
||||
|
||||
this.sayItSystemVolume = function (level) {
|
||||
if ((!level && level !== 0) || level === 'null') return;
|
||||
|
||||
level = parseInt(level);
|
||||
if (level < 0) level = 0;
|
||||
if (level > 100) level = 100;
|
||||
|
||||
if (level === options.sayLastVolume) return;
|
||||
if (!libs.os) libs.os = require('os');
|
||||
|
||||
adapter.setState('tts.volume', level, true);
|
||||
|
||||
options.sayLastVolume = level;
|
||||
|
||||
const p = libs.os.platform();
|
||||
let ls = null;
|
||||
|
||||
if (p === 'linux' && adapter.config.player !== 'mpg321') {
|
||||
//linux
|
||||
try {
|
||||
ls = libs.child_process.spawn('amixer', ['cset', 'name="Master Playback Volume"', '--', level + '%']);
|
||||
} catch (err) {
|
||||
adapter.log.error('amixer is not available, so you may hear no audio. Install manually!');
|
||||
ls = null;
|
||||
}
|
||||
} else if (p.match(/^win/)) {
|
||||
//windows
|
||||
// windows volume is from 0 to 65535
|
||||
level = Math.round((65535 * level) / 100); // because this level is from 0 to 100
|
||||
ls = libs.child_process.spawn(__dirname + '/../nircmd/nircmdc.exe', ['setsysvolume', level]);
|
||||
} else if (p === 'darwin') {
|
||||
//mac osx
|
||||
ls = libs.child_process.spawn('sudo', ['osascript', '-e', '"set Volume ' + Math.round(level / 10) + '"']);
|
||||
}
|
||||
|
||||
if (ls) {
|
||||
ls.on('error', e => {
|
||||
adapter.log.error('sayIt.play: there was an error while playing the mp3 file:' + e);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.getFunction = function (type) {
|
||||
if (sayitFuncs[type]) {
|
||||
return sayitFuncs[type].func;
|
||||
} else {
|
||||
adapter.log.error('No processor for "' + type + '"');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
this.sayItIsPlayFile = isPlayFile;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
module.exports = Speech2Device;
|
||||
@@ -0,0 +1,484 @@
|
||||
'use strict';
|
||||
|
||||
function Text2Speech(adapter, libs, options, sayIt) {
|
||||
const that = this;
|
||||
const engines = require(__dirname + '/../admin/engines.js');
|
||||
const sayitEngines = engines.sayitEngines;
|
||||
const MP3FILE = __dirname + '/../' + adapter.namespace + '.say.mp3';
|
||||
let appkey = null;
|
||||
let cloudUrl = null;
|
||||
let polly = null;
|
||||
|
||||
function splitText(text, max) {
|
||||
if (!max) max = 70;
|
||||
if (text.length > max) {
|
||||
const parts = text.split(/,|.|;|:/);
|
||||
const result = [];
|
||||
for (let p = 0; p < parts.length; p++) {
|
||||
if (parts[p].length < max) {
|
||||
result.push(parts[p]);
|
||||
continue;
|
||||
}
|
||||
|
||||
const _parts = parts[p].split(' ');
|
||||
let i = 0;
|
||||
for (let w = 0; w < _parts.length; w++) {
|
||||
if (_parts[i] && ((result[i] || '') + ' ' + _parts[w]).length > max) {
|
||||
i++;
|
||||
}
|
||||
if (!result[i]) {
|
||||
result.push(_parts[w]);
|
||||
} else {
|
||||
result[i] += ' ' + _parts[w];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return [text];
|
||||
}
|
||||
}
|
||||
|
||||
function copyFile(text, language, volume, source, dest, callback) {
|
||||
libs.fs = libs.fs || require('fs');
|
||||
|
||||
try {
|
||||
const input = libs.fs.createReadStream(source); // Input stream
|
||||
const output = libs.fs.createWriteStream(dest); // Output stream
|
||||
|
||||
input.on('data', d =>output.write(d));
|
||||
// Copy in to out
|
||||
input.on('error', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Raise errors
|
||||
input.on('end', () => { // When input ends
|
||||
output.end(); // close output
|
||||
adapter.log.info('Copied file "' + source + '" to "' + dest + '"');
|
||||
if (callback) callback(null, text, language, volume); // And notify callback
|
||||
});
|
||||
} catch (e) {
|
||||
if (callback) callback('Cannot copy file "' + source + '" to "' + dest + '": ' + e, '', '', volume); // And notify callback
|
||||
}
|
||||
}
|
||||
|
||||
function cacheFile(error, text, language, volume, seconds, callback) {
|
||||
libs.fs = libs.fs || require('fs');
|
||||
libs.path = libs.path || require('path');
|
||||
|
||||
if (!error) {
|
||||
if (adapter.config.cache) {
|
||||
const md5filename = libs.path.join(options.cacheDir, libs.crypto.createHash('md5').update(language + ';' + text).digest('hex') + '.mp3');
|
||||
|
||||
const stat = libs.fs.statSync(MP3FILE);
|
||||
if (stat.size < 100) {
|
||||
adapter.log.warn('Received file is too short: ' + libs.fs.readFileSync(MP3FILE).toString());
|
||||
} else {
|
||||
copyFile(text, language, volume, MP3FILE, md5filename, (error, text, language, volume) => {
|
||||
if (error) adapter.log.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(error, text, language, volume, seconds);
|
||||
}
|
||||
|
||||
function sayItGetSpeechGoogle(text, language, volume, callback) {
|
||||
if (typeof volume === 'function') {
|
||||
callback = volume;
|
||||
volume = undefined;
|
||||
}
|
||||
|
||||
if (text.length === 0) {
|
||||
if (callback) callback(null, '', language, volume, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.length > 70) {
|
||||
const parts = splitText(text);
|
||||
for (let t = 1; t < parts.length; t++) {
|
||||
sayIt(parts[t], language, volume);
|
||||
}
|
||||
text = parts[0];
|
||||
}
|
||||
|
||||
language = language || adapter.config.engine;
|
||||
|
||||
// https://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&q=%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D0%B8%D1%82%D1%8C%207&tl=ru&total=1&idx=0&textlen=10
|
||||
const options = {
|
||||
host: 'translate.google.com',
|
||||
//port: 443,
|
||||
path: '/translate_tts?ie=UTF-8&client=tw-ob&q=' + encodeURI(text) + '&tl=' + language + '&total=1&idx=0&textlen=' + text.length //
|
||||
};
|
||||
|
||||
if (language === 'ru') {
|
||||
options.headers = {
|
||||
'Accept-Encoding': 'identity;q=1, *;q=0',
|
||||
Range: 'bytes=0-',
|
||||
Referer: 'https://www.google.de/',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36'
|
||||
//"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:25.0) Gecko/20100101 Firefox/25.0",
|
||||
//"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
//"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
|
||||
//"Accept-Encoding": "gzip, deflate"//
|
||||
};
|
||||
}
|
||||
|
||||
if (!libs.https) libs.https = require('https');
|
||||
if (!libs.fs) libs.fs = require('fs');
|
||||
|
||||
let sounddata = '';
|
||||
libs.https.get(options, res => {
|
||||
res.setEncoding('binary');
|
||||
|
||||
res.on('data', chunk => sounddata += chunk);
|
||||
|
||||
res.on('end', () => {
|
||||
if (sounddata.length < 100) {
|
||||
if (callback) callback('Cannot get file: received file is too short', text, language, volume, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sounddata.toString().indexOf('302 Moved') !== -1) {
|
||||
if (callback) callback('http://' + options.host + options.path + '\nCannot get file: ' + sounddata, text, language, volume, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
libs.fs.writeFile(MP3FILE, sounddata, 'binary', err => {
|
||||
if (err) {
|
||||
if (callback) callback('File error: ' + err, text, language, volume, 0);
|
||||
} else {
|
||||
that.getLength(MP3FILE, (error, seconds) => {
|
||||
if (callback) callback(error, text, language, volume, seconds);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}).on('error', err => {
|
||||
sounddata = '';
|
||||
if (callback) callback('Cannot get file:' + err, text, language, volume, 0);
|
||||
});
|
||||
}
|
||||
|
||||
function sayItGetSpeechAcapela(text, language, volume, callback) {
|
||||
const options = {
|
||||
host: 'vaassl3.acapela-group.com',
|
||||
path: '/Services/Synthesizer?prot_vers=2&req_voice=' + language + '22k&cl_env=FLASH_AS_3.0&req_text=%5Cvct%3D100%5C+%5Cspd%3D180%5C+' +
|
||||
encodeURI(text) + '&req_asw_type=STREAM&cl_vers=1-30&req_err_as_id3=yes&cl_login=ACAPELA_BOX&cl_app=PROD&cl_pwd=0g7znor2aa'
|
||||
};
|
||||
|
||||
if (!libs.https) libs.https = require('https');
|
||||
|
||||
let sounddata = '';
|
||||
libs.https.get(options, res => {
|
||||
res.setEncoding('binary');
|
||||
|
||||
res.on('data', chunk => sounddata += chunk);
|
||||
|
||||
res.on('end', () => {
|
||||
if (sounddata.length < 100) {
|
||||
if (callback) callback('Cannot get file: received file is too short', text, language, volume);
|
||||
return;
|
||||
}
|
||||
|
||||
libs.fs.writeFile(MP3FILE, sounddata, 'binary', err => {
|
||||
if (err) {
|
||||
if (callback) callback('File error:' + err, text, language, volume);
|
||||
} else {
|
||||
console.log('File saved.');
|
||||
if (callback) callback(null, text, language, volume);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).on('error', err => {
|
||||
sounddata = '';
|
||||
if (callback) callback('Cannot get file:' + err, text, language, volume);
|
||||
});
|
||||
}
|
||||
|
||||
function sayItGetSpeechYandex(text, language, volume, callback) {
|
||||
if (language === 'ru' || language === 'ru_YA') {
|
||||
language = 'ru-RU';
|
||||
}
|
||||
|
||||
if (!libs.https) libs.https = require('https');
|
||||
|
||||
/*emotion: good, neutral, evil, mixed
|
||||
drunk: true, false
|
||||
ill: true, false
|
||||
robot: true, false
|
||||
*/
|
||||
const options = {
|
||||
host: 'tts.voicetech.yandex.net',
|
||||
path: '/generate?lang=' + language + '&format=mp3&speaker=' + adapter.config.voice + '&key=' + adapter.config.key +
|
||||
'&text=' + encodeURI(text.trim())
|
||||
};
|
||||
|
||||
if (adapter.config.emotion && adapter.config.emotion !== 'none') options.path += '&emotion=' + adapter.config.emotion;
|
||||
if (adapter.config.drunk === 'true' || adapter.config.drunk === true) options.path += '&drunk=true';
|
||||
if (adapter.config.ill === 'true' || adapter.config.ill === true) options.path += '&ill=true';
|
||||
if (adapter.config.robot === 'true' || adapter.config.robot === true) options.path += '&robot=true';
|
||||
|
||||
let sounddata = '';
|
||||
libs.https.get(options, res => {
|
||||
res.setEncoding('binary');
|
||||
|
||||
res.on('data', chunk => sounddata += chunk);
|
||||
|
||||
res.on('end', () => {
|
||||
if (sounddata.length < 100) {
|
||||
if (callback) callback('Cannot get file: received file is too short', text, language, volume, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
libs.fs.writeFile(MP3FILE, sounddata, 'binary', err => {
|
||||
if (err) {
|
||||
if (callback) callback('File error: ' + err, text, language, volume, 0);
|
||||
} else {
|
||||
if (callback) callback(null, text, language, volume);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).on('error', err => {
|
||||
sounddata = '';
|
||||
if (callback) callback('Cannot get file: ' + err, text, language, volume);
|
||||
});
|
||||
}
|
||||
|
||||
function sayItGetSpeechPolly(text, language, volume, callback) {
|
||||
if (!libs.aws) libs.aws = require('aws-sdk');
|
||||
if (!libs.fs) libs.fs = require('fs');
|
||||
|
||||
try {
|
||||
polly = polly || new libs.aws.Polly({
|
||||
accessKeyId: adapter.config.accessKey,
|
||||
secretAccessKey: adapter.config.secretKey,
|
||||
region: adapter.config.region
|
||||
});
|
||||
let type = 'text';
|
||||
if (text.match(/<[-+\w\s'"=]+>/)) {
|
||||
if (!text.match(/^<speak>/)) text = '<speak>' + text + '</speak>';
|
||||
type = 'ssml';
|
||||
}
|
||||
const params = {
|
||||
Text: text,
|
||||
OutputFormat: 'mp3',
|
||||
TextType: type,
|
||||
VoiceId: sayitEngines[language].ename
|
||||
};
|
||||
|
||||
polly.synthesizeSpeech(params, (err, data) => {
|
||||
if (err) {
|
||||
if (callback) callback('Cannot get answer: ' + JSON.stringify(err), text, language, volume, 0);
|
||||
} else if (data) {
|
||||
if (data.AudioStream instanceof Buffer) {
|
||||
libs.fs.writeFile(MP3FILE, data.AudioStream, 'binary', err => {
|
||||
if (err) {
|
||||
if (callback) callback('File error: ' + err, text, language, volume, 0);
|
||||
} else {
|
||||
that.getLength(MP3FILE, (error, seconds) => {
|
||||
if (callback) callback(error, text, language, volume, seconds);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (callback) callback('Answer in invalid format: ' + (data ? data.toString() : 'null'), text, language, volume, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (callback) callback(e.toString(), text, language, volume);
|
||||
}
|
||||
}
|
||||
|
||||
function sayItGetSpeechCloud(text, language, volume, callback) {
|
||||
if (!libs.https) libs.https = require('https');
|
||||
if (!libs.fs) libs.fs = require('fs');
|
||||
|
||||
if (!appkey) {
|
||||
adapter.getForeignObject('system.adapter.' + adapter.config.cloud, (err, obj) => {
|
||||
appkey = obj && obj.native && obj.native.apikey ? obj.native.apikey : 'error';
|
||||
cloudUrl = (obj && obj.native && obj.native.cloudUrl) || '';
|
||||
if (appkey && appkey.match(/^@pro_/)) {
|
||||
if (cloudUrl.indexOf('https://yunkong2.pro:') === -1 &&
|
||||
cloudUrl.indexOf('https://yunkong2.info:') === -1) {
|
||||
cloudUrl = 'https://yunkong2.pro:10555';
|
||||
}
|
||||
}
|
||||
cloudUrl = cloudUrl.replace(/https\:\/\//, '').replace(/:\d+/, '');
|
||||
sayItGetSpeechCloud(text, language, volume, callback);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (appkey === 'error') {
|
||||
if (callback) callback('No app key found in "' + adapter.config.cloud + '".', text, language, volume, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let type = 'text';
|
||||
if (text.match(/<[-+\w\s'"=]+>/)) {
|
||||
if (!text.match(/^<speak>/)) text = '<speak>' + text + '</speak>';
|
||||
type = 'ssml';
|
||||
}
|
||||
const postData = JSON.stringify({
|
||||
Text: text,
|
||||
appkey: appkey,
|
||||
TextType: type,
|
||||
VoiceId: sayitEngines[language].ename
|
||||
});
|
||||
|
||||
const postOptions = {
|
||||
host: cloudUrl,
|
||||
port: 443,
|
||||
path: '/polly/',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': Buffer.byteLength(postData)
|
||||
}
|
||||
};
|
||||
// Set up the request
|
||||
const postReq = libs.https.request(postOptions, (res) => {
|
||||
adapter.log.debug('Status code: ' + res.statusCode);
|
||||
if (res.statusCode === 200) {
|
||||
res.setEncoding('binary');
|
||||
} else {
|
||||
res.setEncoding('utf8');
|
||||
}
|
||||
let data = [];
|
||||
res.on('data', chunk => {
|
||||
if (typeof chunk === 'string') chunk = new Buffer(chunk, 'binary');
|
||||
data.push(chunk);
|
||||
});
|
||||
|
||||
res.on('error', error => {
|
||||
if (callback) {
|
||||
callback('Cannot read: ' + error, text, language, volume, 0);
|
||||
callback = null;
|
||||
}
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
data = Buffer.concat(data);
|
||||
if (data instanceof Buffer && data.length > 500) {
|
||||
libs.fs.writeFile(MP3FILE, data, 'binary', err => {
|
||||
if (err) {
|
||||
if (callback) callback('File error: ' + err, text, language, volume, 0);
|
||||
} else {
|
||||
that.getLength(MP3FILE, (error, seconds) => {
|
||||
if (callback) callback(error, text, language, volume, seconds);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (callback) callback('Answer in invalid format: ' + (data ? data.toString().substring(0, 100) : 'null'), text, language, volume, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// post the data
|
||||
postReq.write(postData);
|
||||
postReq.end();
|
||||
} catch (e) {
|
||||
if (callback) callback(e.toString(), text, language, volume);
|
||||
}
|
||||
}
|
||||
|
||||
function sayItGetSpeechPicoTTS(text, language, volume, callback) {
|
||||
if (!libs.fs) libs.fs = require('fs');
|
||||
if (!libs.exec) libs.exec = require('child_process').exec;
|
||||
|
||||
try {
|
||||
const cmd = 'pico2wave -l ' + language + ' -w ' + __dirname + '/say.wav "' + text + '"';
|
||||
libs.exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
if (callback) callback('Cannot create (pico2wave) "say.wav": ' + error, text, language, volume);
|
||||
} else {
|
||||
libs.exec('lame ' + __dirname + '/say.wav ' + MP3FILE, (error, stdout, stderr) => {
|
||||
if (callback) callback(error ? 'Cannot create (lame) "say.mp3": ' + error : null, text, language, volume);
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (callback) callback(e.toString(), text, language, volume);
|
||||
}
|
||||
}
|
||||
|
||||
this.getLength = function (fileName, callback) {
|
||||
libs.fs = libs.fs || require('fs');
|
||||
|
||||
// create a new parser from a node ReadStream
|
||||
if (fileName === adapter.config.announce && adapter.config.annoDuration) {
|
||||
if (callback) callback(null, adapter.config.annoDuration - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (libs.fs.existsSync(fileName)) {
|
||||
try {
|
||||
const stat = libs.fs.statSync(fileName);
|
||||
const size = stat.size;
|
||||
if (callback) callback(null, Math.ceil(size / 4096));
|
||||
} catch (e) {
|
||||
adapter.log.warn('Cannot read length of file ' + fileName);
|
||||
if (callback) callback(null, 0);
|
||||
}
|
||||
} else {
|
||||
if (callback) callback(null, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const ENGINES = {
|
||||
'google': sayItGetSpeechGoogle,
|
||||
'yandex': sayItGetSpeechYandex,
|
||||
'acapela': sayItGetSpeechAcapela,
|
||||
'polly': sayItGetSpeechPolly,
|
||||
'cloud': sayItGetSpeechCloud,
|
||||
'PicoTTS': sayItGetSpeechPicoTTS
|
||||
};
|
||||
|
||||
this.sayItGetSpeech = function (text, language, volume, callback) {
|
||||
if (adapter.config.cache) {
|
||||
libs.path = libs.path || require('path');
|
||||
const md5filename = libs.path.join(options.cacheDir, libs.crypto.createHash('md5').update(language + ';' + text).digest('hex') + '.mp3');
|
||||
if (libs.fs.existsSync(md5filename)) {
|
||||
let cacheFileValid = true;
|
||||
if (adapter.config.cacheExpiryDays) {
|
||||
const fileStat = libs.fs.statSync(md5filename);
|
||||
if (fileStat.ctime && (new Date().getTime()-new Date(fileStat.ctime).getTime() > adapter.config.cacheExpiryDays*1000*60*60*24)) {
|
||||
cacheFileValid = false;
|
||||
adapter.log.info('Cached File expired, remove and re-generate');
|
||||
libs.fs.unlinkSync(md5filename);
|
||||
}
|
||||
}
|
||||
if (cacheFileValid) {
|
||||
that.getLength(md5filename, (error, seconds) => {
|
||||
if (callback) callback(error, md5filename, language, volume, seconds);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sayitEngines[language] && sayitEngines[language].engine) {
|
||||
if (!sayitEngines[language].ssml) text = text.replace(/<\/?[-+\w\s'"=]+>/g, ''); // remove SSML
|
||||
|
||||
if (!ENGINES[sayitEngines[language].engine]) {
|
||||
callback && callback('Engine ' + sayitEngines[language].engine + ' not yet supported.', text, language, volume, 0);
|
||||
} else {
|
||||
ENGINES[sayitEngines[language].engine](text, language, volume,
|
||||
(error, _text, _language, _volume, seconds) => cacheFile(error, _text, _language, _volume, seconds, callback));
|
||||
}
|
||||
} else {
|
||||
sayItGetSpeechGoogle(text, language, volume,
|
||||
(error, _text, _language, _volume, seconds) => cacheFile(error, _text, _language, _volume, seconds, callback));
|
||||
}
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
module.exports = Text2Speech;
|
||||
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let controllerDir;
|
||||
let appName;
|
||||
|
||||
/**
|
||||
* returns application name
|
||||
*
|
||||
* The name of the application can be different and this function finds it out.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function getAppName() {
|
||||
const parts = __dirname.replace(/\\/g, '/').split('/');
|
||||
return parts[parts.length - 2].split('.')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* looks for js-controller home folder
|
||||
*
|
||||
* @param {boolean} isInstall
|
||||
* @returns {string}
|
||||
*/
|
||||
function getControllerDir(isInstall) {
|
||||
// Find the js-controller location
|
||||
const possibilities = [
|
||||
'yunkong2.js-controller',
|
||||
'yunkong2.js-controller',
|
||||
];
|
||||
/** @type {string} */
|
||||
let controllerPath;
|
||||
for (const pkg of possibilities) {
|
||||
try {
|
||||
const possiblePath = require.resolve(pkg);
|
||||
if (fs.existsSync(possiblePath)) {
|
||||
controllerPath = possiblePath;
|
||||
break;
|
||||
}
|
||||
} catch (e) { /* not found */ }
|
||||
}
|
||||
if (controllerPath == null) {
|
||||
if (!isInstall) {
|
||||
console.log('Cannot find js-controller');
|
||||
process.exit(10);
|
||||
} else {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
// we found the controller
|
||||
return path.dirname(controllerPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* reads controller base settings
|
||||
*
|
||||
* @alias getConfig
|
||||
* @returns {object}
|
||||
*/
|
||||
function getConfig() {
|
||||
let configPath;
|
||||
if (fs.existsSync(
|
||||
configPath = path.join(controllerDir, 'conf', appName + '.json')
|
||||
)) {
|
||||
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} else if (fs.existsSync(
|
||||
configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json')
|
||||
)) {
|
||||
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} else {
|
||||
throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json');
|
||||
}
|
||||
}
|
||||
appName = getAppName();
|
||||
controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1);
|
||||
const adapter = require(path.join(controllerDir, 'lib/adapter.js'));
|
||||
|
||||
exports.controllerDir = controllerDir;
|
||||
exports.getConfig = getConfig;
|
||||
exports.Adapter = adapter;
|
||||
exports.appName = appName;
|
||||
Reference in New Issue
Block a user