MPEGPlayer: Skip to next file when there is a problem with a video file in all-play...
[kugel-rb.git] / rbutil / rbutilqt / base / ttsfestival.cpp
blob7a9c854716f953c0fc5100973a4b7224ab1a560d
1 /***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
9 * Copyright (C) 2007 by Dominik Wenger
10 * $Id$
12 * All files in this archive are subject to the GNU General Public License.
13 * See the file COPYING in the source tree root for full license agreement.
15 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
16 * KIND, either express or implied.
18 ****************************************************************************/
20 #include "ttsfestival.h"
21 #include "utils.h"
22 #include "rbsettings.h"
24 TTSFestival::~TTSFestival()
26 qDebug() << "[Festival] Destroying instance";
27 stop();
30 TTSBase::Capabilities TTSFestival::capabilities()
32 return RunInParallel;
35 void TTSFestival::generateSettings()
37 // server path
38 QString exepath = RbSettings::subValue("festival-server",
39 RbSettings::TtsPath).toString();
40 if(exepath == "" ) exepath = Utils::findExecutable("festival");
41 insertSetting(eSERVERPATH,new EncTtsSetting(this,
42 EncTtsSetting::eSTRING, "Path to Festival server:",
43 exepath,EncTtsSetting::eBROWSEBTN));
45 // client path
46 QString clientpath = RbSettings::subValue("festival-client",
47 RbSettings::TtsPath).toString();
48 if(clientpath == "" ) clientpath = Utils::findExecutable("festival_client");
49 insertSetting(eCLIENTPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,
50 tr("Path to Festival client:"),
51 clientpath,EncTtsSetting::eBROWSEBTN));
53 // voice
54 EncTtsSetting* setting = new EncTtsSetting(this,
55 EncTtsSetting::eSTRINGLIST, tr("Voice:"),
56 RbSettings::subValue("festival", RbSettings::TtsVoice),
57 getVoiceList(), EncTtsSetting::eREFRESHBTN);
58 connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceList()));
59 connect(setting,SIGNAL(dataChanged()),this,SLOT(clearVoiceDescription()));
60 insertSetting(eVOICE,setting);
62 //voice description
63 setting = new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING,
64 tr("Voice description:"),"",EncTtsSetting::eREFRESHBTN);
65 connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceDescription()));
66 insertSetting(eVOICEDESC,setting);
69 void TTSFestival::saveSettings()
71 //save settings in user config
72 RbSettings::setSubValue("festival-server",
73 RbSettings::TtsPath,getSetting(eSERVERPATH)->current().toString());
74 RbSettings::setSubValue("festival-client",
75 RbSettings::TtsPath,getSetting(eCLIENTPATH)->current().toString());
76 RbSettings::setSubValue("festival",
77 RbSettings::TtsVoice,getSetting(eVOICE)->current().toString());
79 RbSettings::sync();
82 void TTSFestival::updateVoiceDescription()
84 // get voice Info with current voice and path
85 currentPath = getSetting(eSERVERPATH)->current().toString();
86 QString info = getVoiceInfo(getSetting(eVOICE)->current().toString());
87 currentPath = "";
89 getSetting(eVOICEDESC)->setCurrent(info);
92 void TTSFestival::clearVoiceDescription()
94 getSetting(eVOICEDESC)->setCurrent("");
97 void TTSFestival::updateVoiceList()
99 currentPath = getSetting(eSERVERPATH)->current().toString();
100 QStringList voiceList = getVoiceList();
101 currentPath = "";
103 getSetting(eVOICE)->setList(voiceList);
104 if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0));
105 else getSetting(eVOICE)->setCurrent("");
108 void TTSFestival::startServer()
110 if(!configOk())
111 return;
113 if(serverProcess.state() != QProcess::Running)
115 QString path;
116 /* currentPath is set by the GUI - if it's set, it is the currently set
117 path in the configuration GUI; if it's not set, use the saved path */
118 if (currentPath == "")
119 path = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString();
120 else
121 path = currentPath;
123 serverProcess.start(QString("%1 --server").arg(path));
124 serverProcess.waitForStarted();
126 /* A friendlier version of a spinlock */
127 while (serverProcess.pid() == 0 && serverProcess.state() != QProcess::Running)
128 QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
130 if(serverProcess.state() == QProcess::Running)
131 qDebug() << "[Festival] Server is up and running";
132 else
133 qDebug() << "[Festival] Server failed to start, state: " << serverProcess.state();
137 bool TTSFestival::ensureServerRunning()
139 if(serverProcess.state() != QProcess::Running)
141 startServer();
143 return serverProcess.state() == QProcess::Running;
146 bool TTSFestival::start(QString* errStr)
148 qDebug() << "[Festival] Starting server with voice " << RbSettings::subValue("festival", RbSettings::TtsVoice).toString();
150 bool running = ensureServerRunning();
151 if (!RbSettings::subValue("festival",RbSettings::TtsVoice).toString().isEmpty())
153 /* There's no harm in using both methods to set the voice .. */
154 QString voiceSelect = QString("(voice.select '%1)\n")
155 .arg(RbSettings::subValue("festival", RbSettings::TtsVoice).toString());
156 queryServer(voiceSelect, 3000);
158 if(prologFile.open())
160 prologFile.write(voiceSelect.toAscii());
161 prologFile.close();
162 prologPath = QFileInfo(prologFile).absoluteFilePath();
163 qDebug() << "[Festival] Prolog created at " << prologPath;
168 if (!running)
169 (*errStr) = "Festival could not be started";
170 return running;
173 bool TTSFestival::stop()
175 serverProcess.terminate();
176 serverProcess.kill();
178 return true;
181 TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr)
183 qDebug() << "[Festival] Voicing " << text << "->" << wavfile;
185 QString path = RbSettings::subValue("festival-client",
186 RbSettings::TtsPath).toString();
187 QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp"
188 " --output \"%2\" --prolog \"%3\" - ").arg(path).arg(wavfile).arg(prologPath);
189 qDebug() << "[Festival] Client cmd: " << cmd;
191 QProcess clientProcess;
192 clientProcess.start(cmd);
193 clientProcess.write(QString("%1.\n").arg(text).toAscii());
194 clientProcess.waitForBytesWritten();
195 clientProcess.closeWriteChannel();
196 clientProcess.waitForReadyRead();
197 QString response = clientProcess.readAll();
198 response = response.trimmed();
199 if(!response.contains("Utterance"))
201 qDebug() << "[Festival] Could not voice string: " << response;
202 *errStr = tr("engine could not voice string");
203 return Warning;
204 /* do not stop the voicing process because of a single string
205 TODO: needs proper settings */
207 clientProcess.closeReadChannel(QProcess::StandardError);
208 clientProcess.closeReadChannel(QProcess::StandardOutput);
209 clientProcess.terminate();
210 clientProcess.kill();
212 return NoError;
215 bool TTSFestival::configOk()
217 bool ret;
218 if (currentPath == "")
220 QString serverPath = RbSettings::subValue("festival-server",
221 RbSettings::TtsPath).toString();
222 QString clientPath = RbSettings::subValue("festival-client",
223 RbSettings::TtsPath).toString();
225 ret = QFileInfo(serverPath).isExecutable() &&
226 QFileInfo(clientPath).isExecutable();
227 if(RbSettings::subValue("festival",RbSettings::TtsVoice).toString().size() > 0
228 && voices.size() > 0)
229 ret = ret && (voices.indexOf(RbSettings::subValue("festival",
230 RbSettings::TtsVoice).toString()) != -1);
232 else /* If we're currently configuring the server, we need to know that
233 the entered path is valid */
234 ret = QFileInfo(currentPath).isExecutable();
236 return ret;
239 QStringList TTSFestival::getVoiceList()
241 if(!configOk())
242 return QStringList();
244 if(voices.size() > 0)
246 qDebug() << "[Festival] Using voice cache";
247 return voices;
250 QString response = queryServer("(voice.list)", 10000);
252 // get the 2nd line. It should be (<voice_name>, <voice_name>)
253 response = response.mid(response.indexOf('\n') + 1, -1);
254 response = response.left(response.indexOf('\n')).trimmed();
256 voices = response.mid(1, response.size()-2).split(' ');
258 voices.sort();
259 if (voices.size() == 1 && voices[0].size() == 0)
260 voices.removeAt(0);
261 if (voices.size() > 0)
262 qDebug() << "[Festival] Voices: " << voices;
263 else
264 qDebug() << "[Festival] No voices. Response was: " << response;
266 return voices;
269 QString TTSFestival::getVoiceInfo(QString voice)
271 if(!configOk())
272 return "";
274 if(!getVoiceList().contains(voice))
275 return "";
277 if(voiceDescriptions.contains(voice))
278 return voiceDescriptions[voice];
280 QString response = queryServer(QString("(voice.description '%1)").arg(voice),
281 10000);
283 if (response == "")
285 voiceDescriptions[voice]=tr("No description available");
287 else
289 response = response.remove(QRegExp("(description \"*\")",
290 Qt::CaseInsensitive, QRegExp::Wildcard));
291 qDebug() << "[Festival] voiceInfo w/o descr: " << response;
292 response = response.remove(')');
293 QStringList responseLines = response.split('(', QString::SkipEmptyParts);
294 responseLines.removeAt(0); // the voice name itself
296 QString description;
297 foreach(QString line, responseLines)
299 line = line.remove('(');
300 line = line.simplified();
302 line[0] = line[0].toUpper(); // capitalize the key
304 int firstSpace = line.indexOf(' ');
305 if (firstSpace > 0)
307 // add a colon between the key and the value
308 line = line.insert(firstSpace, ':');
309 // capitalize the value
310 line[firstSpace+2] = line[firstSpace+2].toUpper();
313 description += line + "\n";
315 voiceDescriptions[voice] = description.trimmed();
318 return voiceDescriptions[voice];
321 QString TTSFestival::queryServer(QString query, int timeout)
323 if(!configOk())
324 return "";
326 // this operation could take some time
327 emit busy();
329 qDebug() << "[Festival] queryServer with " << query;
331 if (!ensureServerRunning())
333 qDebug() << "[Festival] queryServer: ensureServerRunning failed";
334 emit busyEnd();
335 return "";
338 QString response;
340 QDateTime endTime;
341 if(timeout > 0)
342 endTime = QDateTime::currentDateTime().addMSecs(timeout);
344 /* Festival is *extremely* unreliable. Although at this
345 * point we are sure that SIOD is accepting commands,
346 * we might end up with an empty response. Hence, the loop.
348 while(true)
350 QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
351 QTcpSocket socket;
353 socket.connectToHost("localhost", 1314);
354 socket.waitForConnected();
356 if(socket.state() == QAbstractSocket::ConnectedState)
358 socket.write(QString("%1\n").arg(query).toAscii());
359 socket.waitForBytesWritten();
360 socket.waitForReadyRead();
362 response = socket.readAll().trimmed();
364 if (response != "LP" && response != "")
365 break;
367 socket.abort();
368 socket.disconnectFromHost();
370 if(timeout > 0 && QDateTime::currentDateTime() >= endTime)
372 emit busyEnd();
373 return "";
375 /* make sure we wait a little as we don't want to flood the server
376 * with requests */
377 QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500);
378 while(QDateTime::currentDateTime() < tmpEndTime)
379 QCoreApplication::processEvents(QEventLoop::AllEvents);
381 if(response == "nil")
383 emit busyEnd();
384 return "";
387 QStringList lines = response.split('\n');
388 if(lines.size() > 2)
390 lines.removeFirst(); /* should be LP */
391 lines.removeLast(); /* should be ft_StUfF_keyOK */
393 else
394 qDebug() << "[Festival] Response too short: " << response;
396 emit busyEnd();
397 return lines.join("\n");