From c876d3bbefe0dc00c27ca0c12d29da5874946962 Mon Sep 17 00:00:00 2001 From: Dominik Riebeling Date: Wed, 15 Dec 2021 21:04:28 +0100 Subject: rbutil: Merge rbutil with utils folder. rbutil uses several components from the utils folder, and can be considered part of utils too. Having it in a separate folder is an arbitrary split that doesn't help anymore these days, so merge them. This also allows other utils to easily use libtools.make without the need to navigate to a different folder. Change-Id: I3fc2f4de19e3e776553efb5dea5f779dfec0dc21 --- utils/rbutilqt/base/ttsfestival.cpp | 412 ++++++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 utils/rbutilqt/base/ttsfestival.cpp (limited to 'utils/rbutilqt/base/ttsfestival.cpp') diff --git a/utils/rbutilqt/base/ttsfestival.cpp b/utils/rbutilqt/base/ttsfestival.cpp new file mode 100644 index 0000000000..d0ca400909 --- /dev/null +++ b/utils/rbutilqt/base/ttsfestival.cpp @@ -0,0 +1,412 @@ +/*************************************************************************** +* __________ __ ___. +* 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 == "") + path = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString(); + else + path = currentPath; + + serverProcess.start(QString("%1 --server").arg(path)); + serverProcess.waitForStarted(); + + /* A friendlier version of a spinlock */ + while (serverProcess.processId() == 0 && serverProcess.state() != QProcess::Running) + QCoreApplication::processEvents(QEventLoop::AllEvents, 50); + + 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) = "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(); + QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp" + " --output \"%2\" --prolog \"%3\" - ").arg(path).arg(wavfile).arg(prologPath); + LOG_INFO() << "Client cmd:" << cmd; + + QProcess clientProcess; + clientProcess.start(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(QRegExp("(description \"*\")", + Qt::CaseInsensitive, QRegExp::Wildcard)); + 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) +{ + if(!configOk()) + return ""; + + // this operation could take some time + emit busy(); + + LOG_INFO() << "queryServer with" << query; + + if (!ensureServerRunning()) + { + LOG_ERROR() << "queryServer: ensureServerRunning failed"; + emit busyEnd(); + return ""; + } + + QString response; + + QDateTime endTime; + if(timeout > 0) + 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. + */ + while(true) + { + QCoreApplication::processEvents(QEventLoop::AllEvents, 50); + QTcpSocket socket; + + socket.connectToHost("localhost", 1314); + socket.waitForConnected(); + + if(socket.state() == QAbstractSocket::ConnectedState) + { + socket.write(QString("%1\n").arg(query).toLatin1()); + socket.waitForBytesWritten(); + socket.waitForReadyRead(); + + response = socket.readAll().trimmed(); + + if (response != "LP" && response != "") + break; + } + socket.abort(); + socket.disconnectFromHost(); + + if(timeout > 0 && QDateTime::currentDateTime() >= endTime) + { + emit busyEnd(); + return ""; + } + /* 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); + } + if(response == "nil") + { + emit busyEnd(); + return ""; + } + + QStringList lines = response.split('\n'); + if(lines.size() > 2) + { + lines.removeFirst(); /* should be LP */ + lines.removeLast(); /* should be ft_StUfF_keyOK */ + } + else + LOG_ERROR() << "Response too short:" << response; + + emit busyEnd(); + return lines.join("\n"); + +} + -- cgit v1.2.3