diff --git a/tools/sapi_voice.vbs b/tools/sapi_voice.vbs index 371c5c5a23..5bfc820dfa 100644 --- a/tools/sapi_voice.vbs +++ b/tools/sapi_voice.vbs @@ -46,6 +46,7 @@ Dim oVoice ' for traversing the list of voices Dim nLangID, sSelectString Dim aLine, aData ' used in command reading +Dim nError, sError ' error returned to the controlling process On Error Resume Next @@ -146,10 +147,27 @@ Do aData = Split(aLine(1), vbTab, 2) If bVerbose Then WScript.StdErr.WriteLine "Saying " & aData(1) _ & " in " & aData(0) + Err.Clear oSpFS.Open aData(0), SSFMCreateForWrite, false - Set oSpVoice.AudioOutputStream = oSpFS - oSpVoice.Speak aData(1) - oSpFS.Close + nError = Err.Number + sError = Err.Description + If nError = 0 Then + Set oSpVoice.AudioOutputStream = oSpFS + oSpVoice.Speak aData(1) + nError = Err.Number + sError = Err.Description + oSpFS.Close + If nError = 0 And Err.Number <> 0 Then + nError = Err.Number + sError = Err.Description + End If + End If + If nError <> 0 Then + oStdOut.WriteLine "ERROR" & vbTab & nError & ": " & sError + Err.Clear + ElseIf Not oFSO.FileExists(aData(0)) Then + oStdOut.WriteLine "ERROR" & vbTab & "SAPI reported success but created no wave file" + End If Case "EXEC" If bVerbose Then WScript.StdErr.WriteLine "> " & aLine(1) oShell.Run aLine(1), 0, true diff --git a/utils/rbutilqt/base/ttssapi.cpp b/utils/rbutilqt/base/ttssapi.cpp index 14c0cae70e..1ef12cb628 100644 --- a/utils/rbutilqt/base/ttssapi.cpp +++ b/utils/rbutilqt/base/ttssapi.cpp @@ -38,6 +38,8 @@ TTSSapi::TTSSapi(QObject* parent) : TTSBase(parent) m_TTSType = "sapi"; defaultLanguage = "english"; + voicescript = nullptr; + voicestream = nullptr; m_started = false; } @@ -158,15 +160,24 @@ bool TTSSapi::start(QString *errStr) QString TTSSapi::voiceVendor(void) { bool keeprunning = m_started; - QString vendor; + QString vendor = "(unknown)"; if(!m_started) { QString error; - start(&error); + if(!start(&error)) { + LOG_ERROR() << "could not start SAPI while querying vendor:" << error; + return vendor; + } } *voicestream << "QUERY\tVENDOR\r\n"; voicestream->flush(); - while((vendor = voicestream->readLine()).isEmpty()) - QCoreApplication::processEvents(); + if(voicescript->waitForReadyRead(5000)) { + QString response = voicestream->readLine(); + if(!response.isEmpty()) + vendor = response; + } + else { + LOG_ERROR() << "SAPI timed out while querying the voice vendor"; + } LOG_INFO() << "TTS vendor:" << vendor; if(!keeprunning) { @@ -231,31 +242,93 @@ QStringList TTSSapi::getVoiceList(QString language) TTSStatus TTSSapi::voice(const QString& text, const QString& wavfile, QString *errStr) { - (void) errStr; QString query = "SPEAK\t"+wavfile+"\t"+text; LOG_INFO() << "voicing" << query; // append newline to query. Done now to keep debug output more readable. query.append("\r\n"); - *voicestream << query; - *voicestream << "SYNC\tbla\r\n"; - voicestream->flush(); - // do NOT poll the output with readLine(), this causes sync issues! - voicescript->waitForReadyRead(); - if(!QFileInfo(wavfile).isFile()) { - LOG_ERROR() << "output file does not exist:" << wavfile; - return FatalError; + // Some third-party SAPI voices occasionally stop responding after many + // consecutive requests. Restart the private cscript process and retry the + // current string instead of discarding the entire voice-file operation. + constexpr int maxAttempts = 3; + constexpr int responseTimeout = 20000; + for(int attempt = 1; attempt <= maxAttempts; ++attempt) { + QFile::remove(wavfile); + *voicestream << query; + *voicestream << "SYNC\tbla\r\n"; + voicestream->flush(); + + // Wait for the explicit SYNC reply. QProcess::waitForReadyRead() alone + // is insufficient: the script can also write warnings, and older code + // mistook those for completion before the wave file existed. + QElapsedTimer timer; + timer.start(); + bool synced = false; + while(timer.elapsed() < responseTimeout) { + int remaining = responseTimeout - static_cast(timer.elapsed()); + if(!voicescript->waitForReadyRead(remaining)) + break; + + QString response = voicestream->readLine(); + if(response == "bla") { + synced = true; + break; + } + if(response.startsWith("ERROR\t")) { + *errStr = response.mid(6); + LOG_ERROR() << "SAPI error:" << *errStr; + return FatalError; + } + } + + if(synced && QFileInfo(wavfile).isFile()) + return NoError; + + if(synced) { + *errStr = tr("SAPI did not create the output wave file"); + LOG_ERROR() << "output file does not exist:" << wavfile; + return FatalError; + } + + LOG_WARNING() << "SAPI timed out on attempt" << attempt + << "of" << maxAttempts << "for" << text; + if(attempt < maxAttempts) { + stop(); + QString startError; + if(!start(&startError)) { + *errStr = tr("Could not restart SAPI after a timeout: %1") + .arg(startError); + LOG_ERROR() << *errStr; + return FatalError; + } + } } - return NoError; + + *errStr = tr("SAPI timed out repeatedly while generating speech"); + LOG_ERROR() << *errStr; + return FatalError; } bool TTSSapi::stop() { + if(!m_started || voicescript == nullptr) + return true; + *voicestream << "QUIT\r\n"; voicestream->flush(); - voicescript->waitForFinished(); + if(!voicescript->waitForFinished(5000)) { + LOG_WARNING() << "SAPI process did not quit, terminating it"; + voicescript->terminate(); + if(!voicescript->waitForFinished(2000)) { + LOG_WARNING() << "SAPI process did not terminate, killing it"; + voicescript->kill(); + voicescript->waitForFinished(2000); + } + } delete voicestream; delete voicescript; + voicestream = nullptr; + voicescript = nullptr; QFile::setPermissions(QDir::tempPath() +"/sapi_voice.vbs", QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadUser | QFile::WriteUser | QFile::ExeUser