/*************************************************************************** * __________ __ ___. * Open \______ \ ____ ____ | | _\_ |__ _______ ___ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * * Copyright (C) 2007 by Dominik Wenger * * All files in this archive are subject to the GNU General Public License. * See the file COPYING in the source tree root for full license agreement. * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * KIND, either express or implied. * ****************************************************************************/ #include #include #include "ttsfestival.h" #include "utils.h" #include "rbsettings.h" #include "Logger.h" TTSFestival::~TTSFestival() { LOG_INFO() << "Destroying instance"; stop(); } TTSBase::Capabilities TTSFestival::capabilities() { return RunInParallel; } void TTSFestival::generateSettings() { // server path QString exepath = RbSettings::subValue("festival-server", RbSettings::TtsPath).toString(); if(exepath == "" ) exepath = Utils::findExecutable("festival"); insertSetting(eSERVERPATH,new EncTtsSetting(this, EncTtsSetting::eSTRING, "Path to Festival server:", exepath,EncTtsSetting::eBROWSEBTN)); // client path QString clientpath = RbSettings::subValue("festival-client", RbSettings::TtsPath).toString(); if(clientpath == "" ) clientpath = Utils::findExecutable("festival_client"); insertSetting(eCLIENTPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING, tr("Path to Festival client:"), clientpath,EncTtsSetting::eBROWSEBTN)); // voice EncTtsSetting* setting = new EncTtsSetting(this, EncTtsSetting::eSTRINGLIST, tr("Voice:"), RbSettings::subValue("festival", RbSettings::TtsVoice), getVoiceList(), EncTtsSetting::eREFRESHBTN); connect(setting, &EncTtsSetting::refresh, this, &TTSFestival::updateVoiceList); connect(setting, &EncTtsSetting::dataChanged, this, &TTSFestival::clearVoiceDescription); insertSetting(eVOICE,setting); //voice description setting = new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING, tr("Voice description:"),"",EncTtsSetting::eREFRESHBTN); connect(setting, &EncTtsSetting::refresh, this, &TTSFestival::updateVoiceDescription); insertSetting(eVOICEDESC,setting); } void TTSFestival::saveSettings() { //save settings in user config RbSettings::setSubValue("festival-server", RbSettings::TtsPath,getSetting(eSERVERPATH)->current().toString()); RbSettings::setSubValue("festival-client", RbSettings::TtsPath,getSetting(eCLIENTPATH)->current().toString()); RbSettings::setSubValue("festival", RbSettings::TtsVoice,getSetting(eVOICE)->current().toString()); RbSettings::sync(); } void TTSFestival::updateVoiceDescription() { // get voice Info with current voice and path currentPath = getSetting(eSERVERPATH)->current().toString(); QString info = getVoiceInfo(getSetting(eVOICE)->current().toString()); currentPath = ""; getSetting(eVOICEDESC)->setCurrent(info); } void TTSFestival::clearVoiceDescription() { getSetting(eVOICEDESC)->setCurrent(""); } void TTSFestival::updateVoiceList() { currentPath = getSetting(eSERVERPATH)->current().toString(); QStringList voiceList = getVoiceList(); currentPath = ""; getSetting(eVOICE)->setList(voiceList); if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0)); else getSetting(eVOICE)->setCurrent(""); } void TTSFestival::startServer() { if(!configOk()) return; if(serverProcess.state() != QProcess::Running) { QString path; /* currentPath is set by the GUI - if it's set, it is the currently set path in the configuration GUI; if it's not set, use the saved path */ if (currentPath.isEmpty()) path = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString(); else path = currentPath; serverProcess.start(path, QStringList("--server")); serverProcess.waitForStarted(); if(serverProcess.state() == QProcess::Running) LOG_INFO() << "Server is up and running"; else LOG_ERROR() << "Server failed to start, state:" << serverProcess.state(); } } bool TTSFestival::ensureServerRunning() { if(serverProcess.state() != QProcess::Running) { startServer(); } return serverProcess.state() == QProcess::Running; } bool TTSFestival::start(QString* errStr) { LOG_INFO() << "Starting server with voice" << RbSettings::subValue("festival", RbSettings::TtsVoice).toString(); bool running = ensureServerRunning(); if (!RbSettings::subValue("festival",RbSettings::TtsVoice).toString().isEmpty()) { /* There's no harm in using both methods to set the voice .. */ QString voiceSelect = QString("(voice.select '%1)\n") .arg(RbSettings::subValue("festival", RbSettings::TtsVoice).toString()); queryServer(voiceSelect, 3000); if(prologFile.open()) { prologFile.write(voiceSelect.toLatin1()); prologFile.close(); prologPath = QFileInfo(prologFile).absoluteFilePath(); LOG_INFO() << "Prolog created at" << prologPath; } } if (!running) (*errStr) = tr("Festival could not be started"); return running; } bool TTSFestival::stop() { serverProcess.terminate(); serverProcess.kill(); return true; } TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr) { LOG_INFO() << "Voicing" << text << "->" << wavfile; QString path = RbSettings::subValue("festival-client", RbSettings::TtsPath).toString(); QStringList cmd; cmd << "--server" << "localhost" << "--otype" << "riff" << "--ttw" << "--withlisp" << "--output" << wavfile << "--prolog" << prologPath << "-"; LOG_INFO() << "Client cmd:" << path << cmd; QProcess clientProcess; clientProcess.start(path, cmd); clientProcess.write(QString("%1.\n").arg(text).toLatin1()); clientProcess.waitForBytesWritten(); clientProcess.closeWriteChannel(); clientProcess.waitForReadyRead(); QString response = clientProcess.readAll(); response = response.trimmed(); if(!response.contains("Utterance")) { LOG_WARNING() << "Could not voice string: " << response; *errStr = tr("engine could not voice string"); return Warning; /* do not stop the voicing process because of a single string TODO: needs proper settings */ } clientProcess.closeReadChannel(QProcess::StandardError); clientProcess.closeReadChannel(QProcess::StandardOutput); clientProcess.terminate(); clientProcess.kill(); return NoError; } bool TTSFestival::configOk() { bool ret; if (currentPath == "") { QString serverPath = RbSettings::subValue("festival-server", RbSettings::TtsPath).toString(); QString clientPath = RbSettings::subValue("festival-client", RbSettings::TtsPath).toString(); ret = QFileInfo(serverPath).isExecutable() && QFileInfo(clientPath).isExecutable(); if(RbSettings::subValue("festival",RbSettings::TtsVoice).toString().size() > 0 && voices.size() > 0) ret = ret && (voices.indexOf(RbSettings::subValue("festival", RbSettings::TtsVoice).toString()) != -1); } else /* If we're currently configuring the server, we need to know that the entered path is valid */ ret = QFileInfo(currentPath).isExecutable(); return ret; } QStringList TTSFestival::getVoiceList() { if(!configOk()) return QStringList(); if(voices.size() > 0) { LOG_INFO() << "Using voice cache"; return voices; } QString response = queryServer("(voice.list)", 10000); // get the 2nd line. It should be (, ) response = response.mid(response.indexOf('\n') + 1, -1); response = response.left(response.indexOf('\n')).trimmed(); voices = response.mid(1, response.size()-2).split(' '); voices.sort(); if (voices.size() == 1 && voices[0].size() == 0) voices.removeAt(0); if (voices.size() > 0) LOG_INFO() << "Voices:" << voices; else LOG_WARNING() << "No voices. Response was:" << response; return voices; } QString TTSFestival::getVoiceInfo(QString voice) { if(!configOk()) return ""; if(!getVoiceList().contains(voice)) return ""; if(voiceDescriptions.contains(voice)) return voiceDescriptions[voice]; QString response = queryServer(QString("(voice.description '%1)").arg(voice), 10000); if (response == "") { voiceDescriptions[voice]=tr("No description available"); } else { response = response.remove(QRegularExpression("(description \".*\")", QRegularExpression::CaseInsensitiveOption)); LOG_INFO() << "voiceInfo w/o descr:" << response; response = response.remove(')'); #if QT_VERSION >= 0x050e00 QStringList responseLines = response.split('(', Qt::SkipEmptyParts); #else QStringList responseLines = response.split('(', QString::SkipEmptyParts); #endif responseLines.removeAt(0); // the voice name itself QString description; foreach(QString line, responseLines) { line = line.remove('('); line = line.simplified(); line[0] = line[0].toUpper(); // capitalize the key int firstSpace = line.indexOf(' '); if (firstSpace > 0) { // add a colon between the key and the value line = line.insert(firstSpace, ':'); // capitalize the value line[firstSpace+2] = line[firstSpace+2].toUpper(); } description += line + "\n"; } voiceDescriptions[voice] = description.trimmed(); } return voiceDescriptions[voice]; } QString TTSFestival::queryServer(QString query, int timeout) { // make sure we always abort at some point. if(timeout == 0) timeout = 60000; if(!configOk()) return ""; // this operation could take some time emit busy(true); LOG_INFO() << "queryServer with" << query; if (!ensureServerRunning()) { LOG_ERROR() << "queryServer: ensureServerRunning failed"; emit busy(false); return ""; } QDateTime endTime = QDateTime::currentDateTime().addMSecs(timeout); /* Festival is *extremely* unreliable. Although at this * point we are sure that SIOD is accepting commands, * we might end up with an empty response. Hence, the loop. */ QTcpSocket socket; QString response; while(QDateTime::currentDateTime() < endTime) { QCoreApplication::processEvents(QEventLoop::AllEvents, 50); if(socket.state() != QAbstractSocket::ConnectedState) { LOG_INFO() << "socket not (yet) connected, trying again."; socket.connectToHost("localhost", 1314); // appears we need to recheck the state still. socket.waitForConnected(); } else { // seems to be necessary to resend the request at times. socket.write(QString("%1\n").arg(query).toLatin1()); socket.waitForBytesWritten(); socket.waitForReadyRead(); // we might not get the complete response on the first read. // Concatenate until we got a full response. response += socket.readAll(); // The query response ends with this. if (response.contains("ft_StUfF_keyOK")) { break; } } /* make sure we wait a little as we don't want to flood the server * with requests */ QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500); while(QDateTime::currentDateTime() < tmpEndTime) QCoreApplication::processEvents(QEventLoop::AllEvents); } emit busy(false); socket.disconnectFromHost(); if(response == "nil") { return ""; } /* The response starts with "LP\n", and ends with "ft_StUfF_keyOK", but we * could get trailing data -- we might have sent the request more than * once. Use a regex to get the actual response part. */ QRegularExpression regex("LP\\n(.*?)\\nft_StUfF_keyOK", QRegularExpression::MultilineOption | QRegularExpression::DotMatchesEverythingOption); QRegularExpressionMatch match = regex.match(response); if(match.hasMatch()) { response = match.captured(1); } else { LOG_WARNING() << "Invalid Festival response." << response; } return response.trimmed(); }