summaryrefslogtreecommitdiff
path: root/utils/rbutilqt/base/ttsfestival.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'utils/rbutilqt/base/ttsfestival.cpp')
-rw-r--r--utils/rbutilqt/base/ttsfestival.cpp412
1 files changed, 412 insertions, 0 deletions
diff --git a/utils/rbutilqt/base/ttsfestival.cpp b/utils/rbutilqt/base/ttsfestival.cpp
new file mode 100644
index 0000000000..d0ca400909
--- /dev/null
+++ b/utils/rbutilqt/base/ttsfestival.cpp
@@ -0,0 +1,412 @@
1/***************************************************************************
2* __________ __ ___.
3* Open \______ \ ____ ____ | | _\_ |__ _______ ___
4* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7* \/ \/ \/ \/ \/
8*
9* Copyright (C) 2007 by Dominik Wenger
10*
11* All files in this archive are subject to the GNU General Public License.
12* See the file COPYING in the source tree root for full license agreement.
13*
14* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
15* KIND, either express or implied.
16*
17****************************************************************************/
18
19#include <QtCore>
20#include <QTcpSocket>
21
22#include "ttsfestival.h"
23#include "utils.h"
24#include "rbsettings.h"
25#include "Logger.h"
26
27TTSFestival::~TTSFestival()
28{
29 LOG_INFO() << "Destroying instance";
30 stop();
31}
32
33TTSBase::Capabilities TTSFestival::capabilities()
34{
35 return RunInParallel;
36}
37
38void TTSFestival::generateSettings()
39{
40 // server path
41 QString exepath = RbSettings::subValue("festival-server",
42 RbSettings::TtsPath).toString();
43 if(exepath == "" ) exepath = Utils::findExecutable("festival");
44 insertSetting(eSERVERPATH,new EncTtsSetting(this,
45 EncTtsSetting::eSTRING, "Path to Festival server:",
46 exepath,EncTtsSetting::eBROWSEBTN));
47
48 // client path
49 QString clientpath = RbSettings::subValue("festival-client",
50 RbSettings::TtsPath).toString();
51 if(clientpath == "" ) clientpath = Utils::findExecutable("festival_client");
52 insertSetting(eCLIENTPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,
53 tr("Path to Festival client:"),
54 clientpath,EncTtsSetting::eBROWSEBTN));
55
56 // voice
57 EncTtsSetting* setting = new EncTtsSetting(this,
58 EncTtsSetting::eSTRINGLIST, tr("Voice:"),
59 RbSettings::subValue("festival", RbSettings::TtsVoice),
60 getVoiceList(), EncTtsSetting::eREFRESHBTN);
61 connect(setting, &EncTtsSetting::refresh,
62 this, &TTSFestival::updateVoiceList);
63 connect(setting, &EncTtsSetting::dataChanged,
64 this, &TTSFestival::clearVoiceDescription);
65 insertSetting(eVOICE,setting);
66
67 //voice description
68 setting = new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING,
69 tr("Voice description:"),"",EncTtsSetting::eREFRESHBTN);
70 connect(setting, &EncTtsSetting::refresh,
71 this, &TTSFestival::updateVoiceDescription);
72 insertSetting(eVOICEDESC,setting);
73}
74
75void TTSFestival::saveSettings()
76{
77 //save settings in user config
78 RbSettings::setSubValue("festival-server",
79 RbSettings::TtsPath,getSetting(eSERVERPATH)->current().toString());
80 RbSettings::setSubValue("festival-client",
81 RbSettings::TtsPath,getSetting(eCLIENTPATH)->current().toString());
82 RbSettings::setSubValue("festival",
83 RbSettings::TtsVoice,getSetting(eVOICE)->current().toString());
84
85 RbSettings::sync();
86}
87
88void TTSFestival::updateVoiceDescription()
89{
90 // get voice Info with current voice and path
91 currentPath = getSetting(eSERVERPATH)->current().toString();
92 QString info = getVoiceInfo(getSetting(eVOICE)->current().toString());
93 currentPath = "";
94
95 getSetting(eVOICEDESC)->setCurrent(info);
96}
97
98void TTSFestival::clearVoiceDescription()
99{
100 getSetting(eVOICEDESC)->setCurrent("");
101}
102
103void TTSFestival::updateVoiceList()
104{
105 currentPath = getSetting(eSERVERPATH)->current().toString();
106 QStringList voiceList = getVoiceList();
107 currentPath = "";
108
109 getSetting(eVOICE)->setList(voiceList);
110 if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0));
111 else getSetting(eVOICE)->setCurrent("");
112}
113
114void TTSFestival::startServer()
115{
116 if(!configOk())
117 return;
118
119 if(serverProcess.state() != QProcess::Running)
120 {
121 QString path;
122 /* currentPath is set by the GUI - if it's set, it is the currently set
123 path in the configuration GUI; if it's not set, use the saved path */
124 if (currentPath == "")
125 path = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString();
126 else
127 path = currentPath;
128
129 serverProcess.start(QString("%1 --server").arg(path));
130 serverProcess.waitForStarted();
131
132 /* A friendlier version of a spinlock */
133 while (serverProcess.processId() == 0 && serverProcess.state() != QProcess::Running)
134 QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
135
136 if(serverProcess.state() == QProcess::Running)
137 LOG_INFO() << "Server is up and running";
138 else
139 LOG_ERROR() << "Server failed to start, state:"
140 << serverProcess.state();
141 }
142}
143
144bool TTSFestival::ensureServerRunning()
145{
146 if(serverProcess.state() != QProcess::Running)
147 {
148 startServer();
149 }
150 return serverProcess.state() == QProcess::Running;
151}
152
153bool TTSFestival::start(QString* errStr)
154{
155 LOG_INFO() << "Starting server with voice"
156 << RbSettings::subValue("festival", RbSettings::TtsVoice).toString();
157
158 bool running = ensureServerRunning();
159 if (!RbSettings::subValue("festival",RbSettings::TtsVoice).toString().isEmpty())
160 {
161 /* There's no harm in using both methods to set the voice .. */
162 QString voiceSelect = QString("(voice.select '%1)\n")
163 .arg(RbSettings::subValue("festival", RbSettings::TtsVoice).toString());
164 queryServer(voiceSelect, 3000);
165
166 if(prologFile.open())
167 {
168 prologFile.write(voiceSelect.toLatin1());
169 prologFile.close();
170 prologPath = QFileInfo(prologFile).absoluteFilePath();
171 LOG_INFO() << "Prolog created at" << prologPath;
172 }
173
174 }
175
176 if (!running)
177 (*errStr) = "Festival could not be started";
178 return running;
179}
180
181bool TTSFestival::stop()
182{
183 serverProcess.terminate();
184 serverProcess.kill();
185
186 return true;
187}
188
189TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr)
190{
191 LOG_INFO() << "Voicing" << text << "->" << wavfile;
192
193 QString path = RbSettings::subValue("festival-client",
194 RbSettings::TtsPath).toString();
195 QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp"
196 " --output \"%2\" --prolog \"%3\" - ").arg(path).arg(wavfile).arg(prologPath);
197 LOG_INFO() << "Client cmd:" << cmd;
198
199 QProcess clientProcess;
200 clientProcess.start(cmd);
201 clientProcess.write(QString("%1.\n").arg(text).toLatin1());
202 clientProcess.waitForBytesWritten();
203 clientProcess.closeWriteChannel();
204 clientProcess.waitForReadyRead();
205 QString response = clientProcess.readAll();
206 response = response.trimmed();
207 if(!response.contains("Utterance"))
208 {
209 LOG_WARNING() << "Could not voice string: " << response;
210 *errStr = tr("engine could not voice string");
211 return Warning;
212 /* do not stop the voicing process because of a single string
213 TODO: needs proper settings */
214 }
215 clientProcess.closeReadChannel(QProcess::StandardError);
216 clientProcess.closeReadChannel(QProcess::StandardOutput);
217 clientProcess.terminate();
218 clientProcess.kill();
219
220 return NoError;
221}
222
223bool TTSFestival::configOk()
224{
225 bool ret;
226 if (currentPath == "")
227 {
228 QString serverPath = RbSettings::subValue("festival-server",
229 RbSettings::TtsPath).toString();
230 QString clientPath = RbSettings::subValue("festival-client",
231 RbSettings::TtsPath).toString();
232
233 ret = QFileInfo(serverPath).isExecutable() &&
234 QFileInfo(clientPath).isExecutable();
235 if(RbSettings::subValue("festival",RbSettings::TtsVoice).toString().size() > 0
236 && voices.size() > 0)
237 ret = ret && (voices.indexOf(RbSettings::subValue("festival",
238 RbSettings::TtsVoice).toString()) != -1);
239 }
240 else /* If we're currently configuring the server, we need to know that
241 the entered path is valid */
242 ret = QFileInfo(currentPath).isExecutable();
243
244 return ret;
245}
246
247QStringList TTSFestival::getVoiceList()
248{
249 if(!configOk())
250 return QStringList();
251
252 if(voices.size() > 0)
253 {
254 LOG_INFO() << "Using voice cache";
255 return voices;
256 }
257
258 QString response = queryServer("(voice.list)", 10000);
259
260 // get the 2nd line. It should be (<voice_name>, <voice_name>)
261 response = response.mid(response.indexOf('\n') + 1, -1);
262 response = response.left(response.indexOf('\n')).trimmed();
263
264 voices = response.mid(1, response.size()-2).split(' ');
265
266 voices.sort();
267 if (voices.size() == 1 && voices[0].size() == 0)
268 voices.removeAt(0);
269 if (voices.size() > 0)
270 LOG_INFO() << "Voices:" << voices;
271 else
272 LOG_WARNING() << "No voices. Response was:" << response;
273
274 return voices;
275}
276
277QString TTSFestival::getVoiceInfo(QString voice)
278{
279 if(!configOk())
280 return "";
281
282 if(!getVoiceList().contains(voice))
283 return "";
284
285 if(voiceDescriptions.contains(voice))
286 return voiceDescriptions[voice];
287
288 QString response = queryServer(QString("(voice.description '%1)").arg(voice),
289 10000);
290
291 if (response == "")
292 {
293 voiceDescriptions[voice]=tr("No description available");
294 }
295 else
296 {
297 response = response.remove(QRegExp("(description \"*\")",
298 Qt::CaseInsensitive, QRegExp::Wildcard));
299 LOG_INFO() << "voiceInfo w/o descr:" << response;
300 response = response.remove(')');
301#if QT_VERSION >= 0x050e00
302 QStringList responseLines = response.split('(', Qt::SkipEmptyParts);
303#else
304 QStringList responseLines = response.split('(', QString::SkipEmptyParts);
305#endif
306 responseLines.removeAt(0); // the voice name itself
307
308 QString description;
309 foreach(QString line, responseLines)
310 {
311 line = line.remove('(');
312 line = line.simplified();
313
314 line[0] = line[0].toUpper(); // capitalize the key
315
316 int firstSpace = line.indexOf(' ');
317 if (firstSpace > 0)
318 {
319 // add a colon between the key and the value
320 line = line.insert(firstSpace, ':');
321 // capitalize the value
322 line[firstSpace+2] = line[firstSpace+2].toUpper();
323 }
324
325 description += line + "\n";
326 }
327 voiceDescriptions[voice] = description.trimmed();
328 }
329
330 return voiceDescriptions[voice];
331}
332
333QString TTSFestival::queryServer(QString query, int timeout)
334{
335 if(!configOk())
336 return "";
337
338 // this operation could take some time
339 emit busy();
340
341 LOG_INFO() << "queryServer with" << query;
342
343 if (!ensureServerRunning())
344 {
345 LOG_ERROR() << "queryServer: ensureServerRunning failed";
346 emit busyEnd();
347 return "";
348 }
349
350 QString response;
351
352 QDateTime endTime;
353 if(timeout > 0)
354 endTime = QDateTime::currentDateTime().addMSecs(timeout);
355
356 /* Festival is *extremely* unreliable. Although at this
357 * point we are sure that SIOD is accepting commands,
358 * we might end up with an empty response. Hence, the loop.
359 */
360 while(true)
361 {
362 QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
363 QTcpSocket socket;
364
365 socket.connectToHost("localhost", 1314);
366 socket.waitForConnected();
367
368 if(socket.state() == QAbstractSocket::ConnectedState)
369 {
370 socket.write(QString("%1\n").arg(query).toLatin1());
371 socket.waitForBytesWritten();
372 socket.waitForReadyRead();
373
374 response = socket.readAll().trimmed();
375
376 if (response != "LP" && response != "")
377 break;
378 }
379 socket.abort();
380 socket.disconnectFromHost();
381
382 if(timeout > 0 && QDateTime::currentDateTime() >= endTime)
383 {
384 emit busyEnd();
385 return "";
386 }
387 /* make sure we wait a little as we don't want to flood the server
388 * with requests */
389 QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500);
390 while(QDateTime::currentDateTime() < tmpEndTime)
391 QCoreApplication::processEvents(QEventLoop::AllEvents);
392 }
393 if(response == "nil")
394 {
395 emit busyEnd();
396 return "";
397 }
398
399 QStringList lines = response.split('\n');
400 if(lines.size() > 2)
401 {
402 lines.removeFirst(); /* should be LP */
403 lines.removeLast(); /* should be ft_StUfF_keyOK */
404 }
405 else
406 LOG_ERROR() << "Response too short:" << response;
407
408 emit busyEnd();
409 return lines.join("\n");
410
411}
412