diff options
Diffstat (limited to 'rbutil/rbutilqt/base/ttsfestival.cpp')
-rw-r--r-- | rbutil/rbutilqt/base/ttsfestival.cpp | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/rbutil/rbutilqt/base/ttsfestival.cpp b/rbutil/rbutilqt/base/ttsfestival.cpp new file mode 100644 index 0000000000..37d263a932 --- /dev/null +++ b/rbutil/rbutilqt/base/ttsfestival.cpp | |||
@@ -0,0 +1,325 @@ | |||
1 | /*************************************************************************** | ||
2 | * __________ __ ___. | ||
3 | * Open \______ \ ____ ____ | | _\_ |__ _______ ___ | ||
4 | * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / | ||
5 | * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < | ||
6 | * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ | ||
7 | * \/ \/ \/ \/ \/ | ||
8 | * | ||
9 | * Copyright (C) 2007 by Dominik Wenger | ||
10 | * $Id$ | ||
11 | * | ||
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. | ||
14 | * | ||
15 | * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY | ||
16 | * KIND, either express or implied. | ||
17 | * | ||
18 | ****************************************************************************/ | ||
19 | |||
20 | #include "ttsfestival.h" | ||
21 | #include "utils.h" | ||
22 | #include "rbsettings.h" | ||
23 | |||
24 | TTSFestival::~TTSFestival() | ||
25 | { | ||
26 | stop(); | ||
27 | } | ||
28 | |||
29 | void TTSFestival::generateSettings() | ||
30 | { | ||
31 | // server path | ||
32 | QString exepath = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString(); | ||
33 | if(exepath == "" ) exepath = findExecutable("festival"); | ||
34 | insertSetting(eSERVERPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,"Path to Festival server:",exepath,EncTtsSetting::eBROWSEBTN)); | ||
35 | |||
36 | // client path | ||
37 | QString clientpath = RbSettings::subValue("festival-client",RbSettings::TtsPath).toString(); | ||
38 | if(clientpath == "" ) clientpath = findExecutable("festival_client"); | ||
39 | insertSetting(eCLIENTPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING, | ||
40 | tr("Path to Festival client:"),clientpath,EncTtsSetting::eBROWSEBTN)); | ||
41 | |||
42 | // voice | ||
43 | EncTtsSetting* setting = new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST, | ||
44 | tr("Voice:"),RbSettings::subValue("festival",RbSettings::TtsVoice),getVoiceList(exepath),EncTtsSetting::eREFRESHBTN); | ||
45 | connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceList())); | ||
46 | connect(setting,SIGNAL(dataChanged()),this,SLOT(clearVoiceDescription())); | ||
47 | insertSetting(eVOICE,setting); | ||
48 | |||
49 | //voice description | ||
50 | setting = new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING, | ||
51 | tr("Voice description:"),"",EncTtsSetting::eREFRESHBTN); | ||
52 | connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceDescription())); | ||
53 | insertSetting(eVOICEDESC,setting); | ||
54 | } | ||
55 | |||
56 | void TTSFestival::saveSettings() | ||
57 | { | ||
58 | //save settings in user config | ||
59 | RbSettings::setSubValue("festival-server",RbSettings::TtsPath,getSetting(eSERVERPATH)->current().toString()); | ||
60 | RbSettings::setSubValue("festival-client",RbSettings::TtsPath,getSetting(eCLIENTPATH)->current().toString()); | ||
61 | RbSettings::setSubValue("festival",RbSettings::TtsVoice,getSetting(eVOICE)->current().toString()); | ||
62 | |||
63 | RbSettings::sync(); | ||
64 | } | ||
65 | |||
66 | void TTSFestival::updateVoiceDescription() | ||
67 | { | ||
68 | // get voice Info with current voice and path | ||
69 | QString info = getVoiceInfo(getSetting(eVOICE)->current().toString(),getSetting(eSERVERPATH)->current().toString()); | ||
70 | getSetting(eVOICEDESC)->setCurrent(info); | ||
71 | } | ||
72 | |||
73 | void TTSFestival::clearVoiceDescription() | ||
74 | { | ||
75 | getSetting(eVOICEDESC)->setCurrent(""); | ||
76 | } | ||
77 | |||
78 | void TTSFestival::updateVoiceList() | ||
79 | { | ||
80 | QStringList voiceList = getVoiceList(getSetting(eSERVERPATH)->current().toString()); | ||
81 | getSetting(eVOICE)->setList(voiceList); | ||
82 | if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0)); | ||
83 | else getSetting(eVOICE)->setCurrent(""); | ||
84 | } | ||
85 | |||
86 | void TTSFestival::startServer(QString path) | ||
87 | { | ||
88 | if(!configOk()) | ||
89 | return; | ||
90 | |||
91 | if(path == "") | ||
92 | path = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString(); | ||
93 | |||
94 | serverProcess.start(QString("%1 --server").arg(path)); | ||
95 | serverProcess.waitForStarted(); | ||
96 | |||
97 | queryServer("(getpid)",300,path); | ||
98 | if(serverProcess.state() == QProcess::Running) | ||
99 | qDebug() << "Festival is up and running"; | ||
100 | else | ||
101 | qDebug() << "Festival failed to start"; | ||
102 | } | ||
103 | |||
104 | void TTSFestival::ensureServerRunning(QString path) | ||
105 | { | ||
106 | if(serverProcess.state() != QProcess::Running) | ||
107 | { | ||
108 | startServer(path); | ||
109 | } | ||
110 | } | ||
111 | |||
112 | bool TTSFestival::start(QString* errStr) | ||
113 | { | ||
114 | (void) errStr; | ||
115 | ensureServerRunning(); | ||
116 | if (!RbSettings::subValue("festival",RbSettings::TtsVoice).toString().isEmpty()) | ||
117 | queryServer(QString("(voice.select '%1)") | ||
118 | .arg(RbSettings::subValue("festival", RbSettings::TtsVoice).toString())); | ||
119 | |||
120 | return true; | ||
121 | } | ||
122 | |||
123 | bool TTSFestival::stop() | ||
124 | { | ||
125 | serverProcess.terminate(); | ||
126 | serverProcess.kill(); | ||
127 | |||
128 | return true; | ||
129 | } | ||
130 | |||
131 | TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr) | ||
132 | { | ||
133 | qDebug() << text << "->" << wavfile; | ||
134 | |||
135 | QString path = RbSettings::subValue("festival-client",RbSettings::TtsPath).toString(); | ||
136 | QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp --output \"%2\" - ").arg(path).arg(wavfile); | ||
137 | qDebug() << cmd; | ||
138 | |||
139 | QProcess clientProcess; | ||
140 | clientProcess.start(cmd); | ||
141 | clientProcess.write(QString("%1.\n").arg(text).toAscii()); | ||
142 | clientProcess.waitForBytesWritten(); | ||
143 | clientProcess.closeWriteChannel(); | ||
144 | clientProcess.waitForReadyRead(); | ||
145 | QString response = clientProcess.readAll(); | ||
146 | response = response.trimmed(); | ||
147 | if(!response.contains("Utterance")) | ||
148 | { | ||
149 | qDebug() << "Could not voice string: " << response; | ||
150 | *errStr = tr("engine could not voice string"); | ||
151 | return Warning; | ||
152 | /* do not stop the voicing process because of a single string | ||
153 | TODO: needs proper settings */ | ||
154 | } | ||
155 | clientProcess.closeReadChannel(QProcess::StandardError); | ||
156 | clientProcess.closeReadChannel(QProcess::StandardOutput); | ||
157 | clientProcess.terminate(); | ||
158 | clientProcess.kill(); | ||
159 | |||
160 | return NoError; | ||
161 | } | ||
162 | |||
163 | bool TTSFestival::configOk() | ||
164 | { | ||
165 | QString serverPath = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString(); | ||
166 | QString clientPath = RbSettings::subValue("festival-client",RbSettings::TtsPath).toString(); | ||
167 | |||
168 | bool ret = QFileInfo(serverPath).isExecutable() && | ||
169 | QFileInfo(clientPath).isExecutable(); | ||
170 | if(RbSettings::subValue("festival",RbSettings::TtsVoice).toString().size() > 0 && voices.size() > 0) | ||
171 | ret = ret && (voices.indexOf(RbSettings::subValue("festival",RbSettings::TtsVoice).toString()) != -1); | ||
172 | return ret; | ||
173 | } | ||
174 | |||
175 | QStringList TTSFestival::getVoiceList(QString path) | ||
176 | { | ||
177 | if(!configOk()) | ||
178 | return QStringList(); | ||
179 | |||
180 | if(voices.size() > 0) | ||
181 | { | ||
182 | qDebug() << "Using voice cache"; | ||
183 | return voices; | ||
184 | } | ||
185 | |||
186 | QString response = queryServer("(voice.list)",3000,path); | ||
187 | |||
188 | // get the 2nd line. It should be (<voice_name>, <voice_name>) | ||
189 | response = response.mid(response.indexOf('\n') + 1, -1); | ||
190 | response = response.left(response.indexOf('\n')).trimmed(); | ||
191 | |||
192 | voices = response.mid(1, response.size()-2).split(' '); | ||
193 | |||
194 | voices.sort(); | ||
195 | if (voices.size() == 1 && voices[0].size() == 0) | ||
196 | voices.removeAt(0); | ||
197 | if (voices.size() > 0) | ||
198 | qDebug() << "Voices: " << voices; | ||
199 | else | ||
200 | qDebug() << "No voices."; | ||
201 | |||
202 | return voices; | ||
203 | } | ||
204 | |||
205 | QString TTSFestival::getVoiceInfo(QString voice,QString path) | ||
206 | { | ||
207 | if(!configOk()) | ||
208 | return ""; | ||
209 | |||
210 | if(!getVoiceList().contains(voice)) | ||
211 | return ""; | ||
212 | |||
213 | if(voiceDescriptions.contains(voice)) | ||
214 | return voiceDescriptions[voice]; | ||
215 | |||
216 | QString response = queryServer(QString("(voice.description '%1)").arg(voice), 3000,path); | ||
217 | |||
218 | if (response == "") | ||
219 | { | ||
220 | voiceDescriptions[voice]=tr("No description available"); | ||
221 | } | ||
222 | else | ||
223 | { | ||
224 | response = response.remove(QRegExp("(description \"*\")", Qt::CaseInsensitive, QRegExp::Wildcard)); | ||
225 | qDebug() << "voiceInfo w/o descr: " << response; | ||
226 | response = response.remove(')'); | ||
227 | QStringList responseLines = response.split('(', QString::SkipEmptyParts); | ||
228 | responseLines.removeAt(0); // the voice name itself | ||
229 | |||
230 | QString description; | ||
231 | foreach(QString line, responseLines) | ||
232 | { | ||
233 | line = line.remove('('); | ||
234 | line = line.simplified(); | ||
235 | |||
236 | line[0] = line[0].toUpper(); // capitalize the key | ||
237 | |||
238 | int firstSpace = line.indexOf(' '); | ||
239 | if (firstSpace > 0) | ||
240 | { | ||
241 | line = line.insert(firstSpace, ':'); // add a colon between the key and the value | ||
242 | line[firstSpace+2] = line[firstSpace+2].toUpper(); // capitalize the value | ||
243 | } | ||
244 | |||
245 | description += line + "\n"; | ||
246 | } | ||
247 | voiceDescriptions[voice] = description.trimmed(); | ||
248 | } | ||
249 | |||
250 | return voiceDescriptions[voice]; | ||
251 | } | ||
252 | |||
253 | QString TTSFestival::queryServer(QString query, int timeout,QString path) | ||
254 | { | ||
255 | if(!configOk()) | ||
256 | return ""; | ||
257 | |||
258 | // this operation could take some time | ||
259 | emit busy(); | ||
260 | |||
261 | ensureServerRunning(path); | ||
262 | |||
263 | qDebug() << "queryServer with " << query; | ||
264 | QString response; | ||
265 | |||
266 | QDateTime endTime; | ||
267 | if(timeout > 0) | ||
268 | endTime = QDateTime::currentDateTime().addMSecs(timeout); | ||
269 | |||
270 | /* Festival is *extremely* unreliable. Although at this | ||
271 | * point we are sure that SIOD is accepting commands, | ||
272 | * we might end up with an empty response. Hence, the loop. | ||
273 | */ | ||
274 | while(true) | ||
275 | { | ||
276 | QCoreApplication::processEvents(QEventLoop::AllEvents, 50); | ||
277 | QTcpSocket socket; | ||
278 | |||
279 | socket.connectToHost("localhost", 1314); | ||
280 | socket.waitForConnected(); | ||
281 | |||
282 | if(socket.state() == QAbstractSocket::ConnectedState) | ||
283 | { | ||
284 | socket.write(QString("%1\n").arg(query).toAscii()); | ||
285 | socket.waitForBytesWritten(); | ||
286 | socket.waitForReadyRead(); | ||
287 | |||
288 | response = socket.readAll().trimmed(); | ||
289 | |||
290 | if (response != "LP" && response != "") | ||
291 | break; | ||
292 | } | ||
293 | socket.abort(); | ||
294 | socket.disconnectFromHost(); | ||
295 | |||
296 | if(timeout > 0 && QDateTime::currentDateTime() >= endTime) | ||
297 | { | ||
298 | emit busyEnd(); | ||
299 | return ""; | ||
300 | } | ||
301 | /* make sure we wait a little as we don't want to flood the server with requests */ | ||
302 | QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500); | ||
303 | while(QDateTime::currentDateTime() < tmpEndTime) | ||
304 | QCoreApplication::processEvents(QEventLoop::AllEvents); | ||
305 | } | ||
306 | if(response == "nil") | ||
307 | { | ||
308 | emit busyEnd(); | ||
309 | return ""; | ||
310 | } | ||
311 | |||
312 | QStringList lines = response.split('\n'); | ||
313 | if(lines.size() > 2) | ||
314 | { | ||
315 | lines.removeFirst(); | ||
316 | lines.removeLast(); | ||
317 | } | ||
318 | else | ||
319 | qDebug() << "Response too short: " << response; | ||
320 | |||
321 | emit busyEnd(); | ||
322 | return lines.join("\n"); | ||
323 | |||
324 | } | ||
325 | |||