mirror of
https://github.com/Rockbox/rockbox.git
synced 2026-10-10 08:03:04 -04:00
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:
parent
1b6767a7d7
commit
a467bfc55f
2 changed files with 109 additions and 18 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue