FS#13972 - Improve rbutil SAPI5 stability (Alessio Lenzi)
rockbox-gerrit-noreply--- via rockbox-cvs <[email protected]>
| Newsgroups | gmane.comp.systems.archos.rockbox.cvs |
|---|---|
| Message-ID | <[email protected]> |
commit a467bfc55fad45c65132fc99fcc4c9b370c4855b Author: Solomon Peachy <[email protected]> Date: Tue Aug 4 17:02:06 2026 -0400 FS#13972 - Improve rbutil SAPI5 stability (Alessio Lenzi) The existing code treated any ready-read notification from cscript as proof that synthesis had completed. The SAPI script can emit other output, so the Utility could check for the wave file before the explicit SYNC reply and report that the output file did not exist. The script also used global "On Error Resume Next" without reporting errors from SpFileStream.Open(), SpVoice.Speak(), or SpFileStream.Close(). Several waits had no timeout, allowing the GUI thread to remain blocked indefinitely. The changes: * reports SAPI COM errors to Rockbox Utility; * verifies that SAPI actually created the requested wave file; * waits for the explicit SYNC reply instead of any process output; * applies finite timeouts to vendor queries, synthesis, and shutdown; * terminates a stuck private cscript process safely; * restarts cscript and retries the current string up to three times when a third-party SAPI engine stops responding during a long generation run. Change-Id: I2cf2aefb704353c648bef0c4312f35282ac4e25d 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<int>(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 -- rockbox-cvs mailing list [email protected] https://lists.haxx.se/mailman/listinfo/rockbox-cvs