Fix include
[kdeaccessibility.git] / kttsd / plugins / command / commandproc.cpp
blobd7ba278bcbd2b4496ea64141a2fb2b5f5ccf6a82
1 /***************************************************** vim:set ts=4 sw=4 sts=4:
2 Main speaking functions for the Command Plug in
3 -------------------
4 Copyright : (C) 2002 by Gunnar Schmi Dt and 2004 by Gary Cramblitt
5 -------------------
6 Original author: Gunnar Schmi Dt <kmouth@schmi-dt.de>
7 Current Maintainer: Gary Cramblitt <garycramblitt@comcast.net>
8 ******************************************************************************/
10 /***************************************************************************
11 * *
12 * This program is free software; you can redistribute it and/or modify *
13 * it under the terms of the GNU General Public License as published by *
14 * the Free Software Foundation; version 2 of the License. *
15 * *
16 ***************************************************************************/
18 // Qt includes.
19 #include <QFile>
21 #include <QRegExp>
22 #include <QTextCodec>
23 #include <QStack>
24 #include <QTextStream>
26 // KDE includes.
27 #include <kdebug.h>
28 #include <kconfig.h>
29 #include <k3process.h>
30 #include <ktemporaryfile.h>
31 #include <kstandarddirs.h>
33 // KTTS includes.
34 #include <pluginproc.h>
36 // Command Plugin includes.
37 #include "commandproc.h"
38 #include "commandproc.moc"
40 /** Constructor */
41 CommandProc::CommandProc( QObject* parent, const QStringList& /*args*/) :
42 PlugInProc( parent, "commandproc" )
44 kDebug() << "CommandProc::CommandProc: Running" << endl;
45 m_commandProc = 0;
46 m_state = psIdle;
47 m_stdin = true;
48 m_supportsSynth = false;
49 m_waitingStop = false;
52 /** Destructor */
53 CommandProc::~CommandProc()
55 kDebug() << "CommandProc::~CommandProc: Running" << endl;
56 if (m_commandProc)
58 if (m_commandProc->isRunning()) m_commandProc->kill();
59 delete m_commandProc;
60 // Don't delete synth file. That is responsibility of caller.
61 if (!m_textFilename.isNull()) QFile::remove(m_textFilename);
65 /** Initialize */
66 bool CommandProc::init(KConfig *c, const QString &configGroup){
67 kDebug() << "CommandProc::init: Initializing plug in: Command " << endl;
69 KConfigGroup config(c, configGroup);
70 m_ttsCommand = config.readEntry("Command", "cat -");
71 m_stdin = config.readEntry("StdIn", true);
72 m_language = config.readEntry("LanguageCode", "en");
74 // Support separate synthesis if the TTS command contains %w macro.
75 m_supportsSynth = (m_ttsCommand.contains("%w"));
77 QString codecString = config.readEntry("Codec", "Local");
78 m_codec = codecNameToCodec(codecString);
79 kDebug() << "CommandProc::init: Initialized with command: " << m_ttsCommand << " codec: " << codecString << endl;
80 return true;
83 /**
84 * Say a text. Synthesize and audibilize it.
85 * @param text The text to be spoken.
87 * If the plugin supports asynchronous operation, it should return immediately.
89 void CommandProc::sayText(const QString &text)
91 synth(text, QString(),
92 m_ttsCommand, m_stdin, m_codec, m_language);
95 /**
96 * Synthesize text into an audio file, but do not send to the audio device.
97 * @param text The text to be synthesized.
98 * @param suggestedFilename Full pathname of file to create. The plugin
99 * may ignore this parameter and choose its own
100 * filename. KTTSD will query the generated
101 * filename using getFilename().
103 * If the plugin supports asynchronous operation, it should return immediately.
105 void CommandProc::synthText(const QString& text, const QString& suggestedFilename)
107 synth(text, suggestedFilename,
108 m_ttsCommand, m_stdin, m_codec, m_language);
112 * Say or Synthesize text.
113 * @param inputText The text that shall be spoken
114 * @param suggestedFilename If not Null, synthesize only to this filename, otherwise
115 * synthesize and audibilize the text.
116 * @param userCmd The program that shall be executed for speaking
117 * @param stdIn True if the program shall receive its data via standard input
118 * @param codec The QTextCodec if encoding==UseCodec
119 * @param language The language code (used for the %l macro)
121 void CommandProc::synth(const QString& inputText, const QString& suggestedFilename,
122 const QString& userCmd, bool stdIn, QTextCodec *codec, QString& language)
124 if (m_commandProc)
126 if (m_commandProc->isRunning()) m_commandProc->kill();
127 delete m_commandProc;
128 m_commandProc = 0;
129 m_synthFilename.clear();
130 if (!m_textFilename.isNull()) QFile::remove(m_textFilename);
131 m_textFilename.clear();
133 QString command = userCmd;
134 QString text = inputText.trimmed();
135 if (text.isEmpty()) return;
136 // 1. prepare the text:
137 // 1.a) encode the text
138 QByteArray encText;
139 QTextStream ts (encText, QIODevice::WriteOnly);
140 ts.setCodec(codec);
141 ts << text;
142 ts << endl; // Some synths need this, eg. flite.
144 // 1.b) quote the text as one parameter
145 QString escText = K3ShellProcess::quote(text);
147 // 1.c) create a temporary file for the text, if %f macro is used.
148 if (command.contains("%f"))
150 KTemporaryFile tempFile;
151 tempFile.setPrefix("commandplugin-");
152 tempFile.setSuffix(".txt");
153 tempFile.setAutoRemove(false);
154 tempFile.open();
155 QTextStream fs (&tempFile);
156 fs.setCodec(codec);
157 fs << text;
158 fs << endl;
159 m_textFilename = tempFile.fileName();
160 } else m_textFilename.clear();
162 // 2. replace variables with values
163 QStack<bool> stack;
164 bool issinglequote=false;
165 bool isdoublequote=false;
166 int noreplace=0;
167 QRegExp re_noquote("(\"|'|\\\\|`|\\$\\(|\\$\\{|\\(|\\{|\\)|\\}|%%|%t|%f|%l|%w)");
168 QRegExp re_singlequote("('|%%|%t|%f|%l|%w)");
169 QRegExp re_doublequote("(\"|\\\\|`|\\$\\(|\\$\\{|%%|%t|%f|%l|%w)");
171 for ( int i = re_noquote.indexIn(command);
172 i != -1;
173 i = (issinglequote?re_singlequote.indexIn(command,i)
174 :isdoublequote?re_doublequote.indexIn(command,i)
175 :re_noquote.indexIn(command,i))
178 if ((command[i]=='(') || (command[i]=='{')) // (...) or {...}
180 // assert(isdoublequote == false)
181 stack.push(isdoublequote);
182 if (noreplace > 0)
183 // count nested braces when within ${...}
184 noreplace++;
185 i++;
187 else if (command[i]=='$')
189 stack.push(isdoublequote);
190 isdoublequote = false;
191 if ((noreplace > 0) || (command[i+1]=='{'))
192 // count nested braces when within ${...}
193 noreplace++;
194 i+=2;
196 else if ((command[i]==')') || (command[i]=='}'))
197 // $(...) or (...) or ${...} or {...}
199 if (!stack.isEmpty())
200 isdoublequote = stack.pop();
201 else
202 qWarning("Parse error.");
203 if (noreplace > 0)
204 // count nested braces when within ${...}
205 noreplace--;
206 i++;
208 else if (command[i]=='\'')
210 issinglequote=!issinglequote;
211 i++;
213 else if (command[i]=='"')
215 isdoublequote=!isdoublequote;
216 i++;
218 else if (command[i]=='\\')
219 i+=2;
220 else if (command[i]=='`')
222 // Replace all `...` with safer $(...)
223 command.replace (i, 1, "$(");
224 QRegExp re_backticks("(`|\\\\`|\\\\\\\\|\\\\\\$)");
225 for ( int i2=re_backticks.indexIn(command,i+2);
226 i2!=-1;
227 i2=re_backticks.indexIn(command,i2)
230 if (command[i2] == '`')
232 command.replace (i2, 1, ")");
233 i2=command.length(); // leave loop
235 else
236 { // remove backslash and ignore following character
237 command.remove (i2, 1);
238 i2++;
241 // Leave i unchanged! We need to process "$("
243 else if (noreplace == 0) // do not replace macros within ${...}
245 QString match, v;
247 // get match
248 if (issinglequote)
249 match=re_singlequote.cap();
250 else if (isdoublequote)
251 match=re_doublequote.cap();
252 else
253 match=re_noquote.cap();
255 // substitue %variables
256 if (match=="%%")
257 v="%";
258 else if (match=="%t")
259 v=escText;
260 else if (match=="%f")
261 v=m_textFilename;
262 else if (match=="%l")
263 v=language;
264 else if (match=="%w")
265 v = suggestedFilename;
267 // %variable inside of a quote?
268 if (isdoublequote)
269 v='"'+v+'"';
270 else if (issinglequote)
271 v='\''+v+'\'';
273 command.replace (i, match.length(), v);
274 i+=v.length();
276 else
278 if (issinglequote)
279 i+=re_singlequote.matchedLength();
280 else if (isdoublequote)
281 i+=re_doublequote.matchedLength();
282 else
283 i+=re_noquote.matchedLength();
287 // 3. create a new process
288 kDebug() << "CommandProc::synth: running command: " << command << endl;
289 m_commandProc = new K3Process;
290 m_commandProc->setUseShell(true);
291 m_commandProc->setEnvironment("LANG", language + '.' + codec->name());
292 m_commandProc->setEnvironment("LC_CTYPE", language + '.' + codec->name());
293 *m_commandProc << command;
294 connect(m_commandProc, SIGNAL(processExited(K3Process*)),
295 this, SLOT(slotProcessExited(K3Process*)));
296 connect(m_commandProc, SIGNAL(receivedStdout(K3Process*, char*, int)),
297 this, SLOT(slotReceivedStdout(K3Process*, char*, int)));
298 connect(m_commandProc, SIGNAL(receivedStderr(K3Process*, char*, int)),
299 this, SLOT(slotReceivedStderr(K3Process*, char*, int)));
300 connect(m_commandProc, SIGNAL(wroteStdin(K3Process*)),
301 this, SLOT(slotWroteStdin(K3Process* )));
303 // 4. start the process
305 if (suggestedFilename.isNull())
306 m_state = psSaying;
307 else
309 m_synthFilename = suggestedFilename;
310 m_state = psSynthing;
312 if (stdIn) {
313 m_commandProc->start(K3Process::NotifyOnExit, K3Process::All);
314 if (encText.size() > 0)
315 m_commandProc->writeStdin(encText, encText.size());
316 else
317 m_commandProc->closeStdin();
319 else
320 m_commandProc->start(K3Process::NotifyOnExit, K3Process::AllOutput);
324 * Get the generated audio filename from synthText.
325 * @return Name of the audio file the plugin generated.
326 * Null if no such file.
328 * The plugin must not re-use the filename.
330 QString CommandProc::getFilename()
332 kDebug() << "CommandProc::getFilename: returning " << m_synthFilename << endl;
333 return m_synthFilename;
337 * Stop current operation (saying or synthesizing text).
338 * Important: This function may be called from a thread different from the
339 * one that called sayText or synthText.
340 * If the plugin cannot stop an in-progress @ref sayText or
341 * @ref synthText operation, it must not block waiting for it to complete.
342 * Instead, return immediately.
344 * If a plugin returns before the operation has actually been stopped,
345 * the plugin must emit the @ref stopped signal when the operation has
346 * actually stopped.
348 * The plugin should change to the psIdle state after stopping the
349 * operation.
351 void CommandProc::stopText(){
352 kDebug() << "CommandProc::stopText: Running" << endl;
353 if (m_commandProc)
355 if (m_commandProc->isRunning())
357 kDebug() << "CommandProc::stopText: killing Command." << endl;
358 m_waitingStop = true;
359 m_commandProc->kill();
360 } else m_state = psIdle;
361 }else m_state = psIdle;
362 kDebug() << "CommandProc::stopText: Command stopped." << endl;
365 void CommandProc::slotProcessExited(K3Process*)
367 kDebug() << "CommandProc:slotProcessExited: Command process has exited." << endl;
368 pluginState prevState = m_state;
369 if (m_waitingStop)
371 m_waitingStop = false;
372 m_state = psIdle;
373 emit stopped();
374 } else {
375 m_state = psFinished;
376 if (prevState == psSaying)
377 emit sayFinished();
378 else
379 if (prevState == psSynthing)
380 emit synthFinished();
384 void CommandProc::slotReceivedStdout(K3Process*, char* buffer, int buflen)
386 QString buf = QString::fromLatin1(buffer, buflen);
387 kDebug() << "CommandProc::slotReceivedStdout: Received output from Command: " << buf << endl;
390 void CommandProc::slotReceivedStderr(K3Process*, char* buffer, int buflen)
392 QString buf = QString::fromLatin1(buffer, buflen);
393 kDebug() << "CommandProc::slotReceivedStderr: Received error from Command: " << buf << endl;
396 void CommandProc::slotWroteStdin(K3Process*)
398 kDebug() << "CommandProc::slotWroteStdin: closing Stdin" << endl;
399 m_commandProc->closeStdin();
403 * Return the current state of the plugin.
404 * This function only makes sense in asynchronous mode.
405 * @return The pluginState of the plugin.
407 * @see pluginState
409 pluginState CommandProc::getState() { return m_state; }
412 * Acknowledges a finished state and resets the plugin state to psIdle.
414 * If the plugin is not in state psFinished, nothing happens.
415 * The plugin may use this call to do any post-processing cleanup,
416 * for example, blanking the stored filename (but do not delete the file).
417 * Calling program should call getFilename prior to ackFinished.
419 void CommandProc::ackFinished()
421 if (m_state == psFinished)
423 m_state = psIdle;
424 m_synthFilename.clear();
425 if (!m_textFilename.isNull()) QFile::remove(m_textFilename);
426 m_textFilename.clear();
431 * Returns True if the plugin supports asynchronous processing,
432 * i.e., returns immediately from sayText or synthText.
433 * @return True if this plugin supports asynchronous processing.
435 * If the plugin returns True, it must also implement @ref getState .
436 * It must also emit @ref sayFinished or @ref synthFinished signals when
437 * saying or synthesis is completed.
439 bool CommandProc::supportsAsync() { return true; }
442 * Returns True if the plugin supports synthText method,
443 * i.e., is able to synthesize text to a sound file without
444 * audibilizing the text.
445 * @return True if this plugin supports synthText method.
447 bool CommandProc::supportsSynth() { return m_supportsSynth; }