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
This commit is contained in:
Solomon Peachy 2026-08-04 17:02:06 -04:00
parent 1b6767a7d7
commit a467bfc55f
2 changed files with 109 additions and 18 deletions

View file

@ -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

View file

@ -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