diff options
Diffstat (limited to 'kttsd/plugins/command/commandproc.cpp')
-rw-r--r-- | kttsd/plugins/command/commandproc.cpp | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/kttsd/plugins/command/commandproc.cpp b/kttsd/plugins/command/commandproc.cpp new file mode 100644 index 0000000..e8d3e72 --- /dev/null +++ b/kttsd/plugins/command/commandproc.cpp @@ -0,0 +1,446 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Main speaking functions for the Command Plug in + ------------------- + Copyright : (C) 2002 by Gunnar Schmi Dt and 2004 by Gary Cramblitt + ------------------- + Original author: Gunnar Schmi Dt <kmouth@schmi-dt.de> + Current Maintainer: Gary Cramblitt <garycramblitt@comcast.net> + ******************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; version 2 of the License. * + * * + ***************************************************************************/ + +// Qt includes. +#include <qfile.h> +#include <qstring.h> +#include <qvaluelist.h> +#include <qstringlist.h> +#include <qregexp.h> +#include <qtextcodec.h> +#include <qvaluestack.h> + +// KDE includes. +#include <kdebug.h> +#include <kconfig.h> +#include <kprocess.h> +#include <ktempfile.h> +#include <kstandarddirs.h> + +// KTTS includes. +#include <pluginproc.h> + +// Command Plugin includes. +#include "commandproc.h" +#include "commandproc.moc" + +/** Constructor */ +CommandProc::CommandProc( QObject* parent, const char* name, const QStringList& /*args*/) : + PlugInProc( parent, name ) +{ + kdDebug() << "CommandProc::CommandProc: Running" << endl; + m_commandProc = 0; + m_state = psIdle; + m_stdin = true; + m_supportsSynth = false; + m_waitingStop = false; +} + +/** Destructor */ +CommandProc::~CommandProc() +{ + kdDebug() << "CommandProc::~CommandProc: Running" << endl; + if (m_commandProc) + { + if (m_commandProc->isRunning()) m_commandProc->kill(); + delete m_commandProc; + // Don't delete synth file. That is responsibility of caller. + if (!m_textFilename.isNull()) QFile::remove(m_textFilename); + } +} + +/** Initialize */ +bool CommandProc::init(KConfig *config, const QString &configGroup){ + kdDebug() << "CommandProc::init: Initializing plug in: Command " << endl; + + config->setGroup(configGroup); + m_ttsCommand = config->readEntry("Command", "cat -"); + m_stdin = config->readBoolEntry("StdIn", true); + m_language = config->readEntry("LanguageCode", "en"); + + // Support separate synthesis if the TTS command contains %w macro. + m_supportsSynth = (m_ttsCommand.contains("%w")); + + QString codecString = config->readEntry("Codec", "Local"); + m_codec = codecNameToCodec(codecString); + kdDebug() << "CommandProc::init: Initialized with command: " << m_ttsCommand << " codec: " << codecString << endl; + return true; +} + +/** +* Say a text. Synthesize and audibilize it. +* @param text The text to be spoken. +* +* If the plugin supports asynchronous operation, it should return immediately. +*/ +void CommandProc::sayText(const QString &text) +{ + synth(text, QString::null, + m_ttsCommand, m_stdin, m_codec, m_language); +} + +/** +* Synthesize text into an audio file, but do not send to the audio device. +* @param text The text to be synthesized. +* @param suggestedFilename Full pathname of file to create. The plugin +* may ignore this parameter and choose its own +* filename. KTTSD will query the generated +* filename using getFilename(). +* +* If the plugin supports asynchronous operation, it should return immediately. +*/ +void CommandProc::synthText(const QString& text, const QString& suggestedFilename) +{ + synth(text, suggestedFilename, + m_ttsCommand, m_stdin, m_codec, m_language); +} + +/** +* Say or Synthesize text. +* @param inputText The text that shall be spoken +* @param suggestedFilename If not Null, synthesize only to this filename, otherwise +* synthesize and audibilize the text. +* @param userCmd The program that shall be executed for speaking +* @param stdIn True if the program shall recieve its data via standard input +* @param codec The QTextCodec if encoding==UseCodec +* @param language The language code (used for the %l macro) +*/ +void CommandProc::synth(const QString& inputText, const QString& suggestedFilename, + const QString& userCmd, bool stdIn, QTextCodec *codec, QString& language) +{ + if (m_commandProc) + { + if (m_commandProc->isRunning()) m_commandProc->kill(); + delete m_commandProc; + m_commandProc = 0; + m_synthFilename = QString::null; + if (!m_textFilename.isNull()) QFile::remove(m_textFilename); + m_textFilename = QString::null; + } + QString command = userCmd; + QString text = inputText.stripWhiteSpace(); + if (text.isEmpty()) return; + // 1. prepare the text: + // 1.a) encode the text + text += "\n"; + QCString encodedText; + if (codec) + encodedText = codec->fromUnicode(text); + else + encodedText = text.latin1(); // Should not happen, but just in case. + + // 1.b) quote the text as one parameter + QString escText = KShellProcess::quote(text); + + // 1.c) create a temporary file for the text, if %f macro is used. + if (command.contains("%f")) + { + KTempFile tempFile(locateLocal("tmp", "commandplugin-"), ".txt"); + QTextStream* fs = tempFile.textStream(); + fs->setCodec(codec); + *fs << text; + *fs << endl; + m_textFilename = tempFile.file()->name(); + tempFile.close(); + } else m_textFilename = QString::null; + + // 2. replace variables with values + QValueStack<bool> stack; + bool issinglequote=false; + bool isdoublequote=false; + int noreplace=0; + QRegExp re_noquote("(\"|'|\\\\|`|\\$\\(|\\$\\{|\\(|\\{|\\)|\\}|%%|%t|%f|%l|%w)"); + QRegExp re_singlequote("('|%%|%t|%f|%l|%w)"); + QRegExp re_doublequote("(\"|\\\\|`|\\$\\(|\\$\\{|%%|%t|%f|%l|%w)"); + + for ( int i = re_noquote.search(command); + i != -1; + i = (issinglequote?re_singlequote.search(command,i) + :isdoublequote?re_doublequote.search(command,i) + :re_noquote.search(command,i)) + ) + { + if ((command[i]=='(') || (command[i]=='{')) // (...) or {...} + { + // assert(isdoublequote == false) + stack.push(isdoublequote); + if (noreplace > 0) + // count nested braces when within ${...} + noreplace++; + i++; + } + else if (command[i]=='$') + { + stack.push(isdoublequote); + isdoublequote = false; + if ((noreplace > 0) || (command[i+1]=='{')) + // count nested braces when within ${...} + noreplace++; + i+=2; + } + else if ((command[i]==')') || (command[i]=='}')) + // $(...) or (...) or ${...} or {...} + { + if (!stack.isEmpty()) + isdoublequote = stack.pop(); + else + qWarning("Parse error."); + if (noreplace > 0) + // count nested braces when within ${...} + noreplace--; + i++; + } + else if (command[i]=='\'') + { + issinglequote=!issinglequote; + i++; + } + else if (command[i]=='"') + { + isdoublequote=!isdoublequote; + i++; + } + else if (command[i]=='\\') + i+=2; + else if (command[i]=='`') + { + // Replace all `...` with safer $(...) + command.replace (i, 1, "$("); + QRegExp re_backticks("(`|\\\\`|\\\\\\\\|\\\\\\$)"); + for ( int i2=re_backticks.search(command,i+2); + i2!=-1; + i2=re_backticks.search(command,i2) + ) + { + if (command[i2] == '`') + { + command.replace (i2, 1, ")"); + i2=command.length(); // leave loop + } + else + { // remove backslash and ignore following character + command.remove (i2, 1); + i2++; + } + } + // Leave i unchanged! We need to process "$(" + } + else if (noreplace == 0) // do not replace macros within ${...} + { + QString match, v; + + // get match + if (issinglequote) + match=re_singlequote.cap(); + else if (isdoublequote) + match=re_doublequote.cap(); + else + match=re_noquote.cap(); + + // substitue %variables + if (match=="%%") + v="%"; + else if (match=="%t") + v=escText; + else if (match=="%f") + v=m_textFilename; + else if (match=="%l") + v=language; + else if (match=="%w") + v = suggestedFilename; + + // %variable inside of a quote? + if (isdoublequote) + v='"'+v+'"'; + else if (issinglequote) + v="'"+v+"'"; + + command.replace (i, match.length(), v); + i+=v.length(); + } + else + { + if (issinglequote) + i+=re_singlequote.matchedLength(); + else if (isdoublequote) + i+=re_doublequote.matchedLength(); + else + i+=re_noquote.matchedLength(); + } + } + + // 3. create a new process + kdDebug() << "CommandProc::synth: running command: " << command << endl; + m_commandProc = new KProcess; + m_commandProc->setUseShell(true); + m_commandProc->setEnvironment("LANG", language + "." + codec->mimeName()); + m_commandProc->setEnvironment("LC_CTYPE", language + "." + codec->mimeName()); + *m_commandProc << command; + connect(m_commandProc, SIGNAL(processExited(KProcess*)), + this, SLOT(slotProcessExited(KProcess*))); + connect(m_commandProc, SIGNAL(receivedStdout(KProcess*, char*, int)), + this, SLOT(slotReceivedStdout(KProcess*, char*, int))); + connect(m_commandProc, SIGNAL(receivedStderr(KProcess*, char*, int)), + this, SLOT(slotReceivedStderr(KProcess*, char*, int))); + connect(m_commandProc, SIGNAL(wroteStdin(KProcess*)), + this, SLOT(slotWroteStdin(KProcess* ))); + + // 4. start the process + + if (suggestedFilename.isNull()) + m_state = psSaying; + else + { + m_synthFilename = suggestedFilename; + m_state = psSynthing; + } + if (stdIn) { + m_commandProc->start(KProcess::NotifyOnExit, KProcess::All); + if (encodedText.length() > 0) + m_commandProc->writeStdin(encodedText, encodedText.length()); + else + m_commandProc->closeStdin(); + } + else + m_commandProc->start(KProcess::NotifyOnExit, KProcess::AllOutput); +} + +/** +* Get the generated audio filename from synthText. +* @return Name of the audio file the plugin generated. +* Null if no such file. +* +* The plugin must not re-use the filename. +*/ +QString CommandProc::getFilename() +{ + kdDebug() << "CommandProc::getFilename: returning " << m_synthFilename << endl; + return m_synthFilename; +} + +/** +* Stop current operation (saying or synthesizing text). +* Important: This function may be called from a thread different from the +* one that called sayText or synthText. +* If the plugin cannot stop an in-progress @ref sayText or +* @ref synthText operation, it must not block waiting for it to complete. +* Instead, return immediately. +* +* If a plugin returns before the operation has actually been stopped, +* the plugin must emit the @ref stopped signal when the operation has +* actually stopped. +* +* The plugin should change to the psIdle state after stopping the +* operation. +*/ +void CommandProc::stopText(){ + kdDebug() << "CommandProc::stopText: Running" << endl; + if (m_commandProc) + { + if (m_commandProc->isRunning()) + { + kdDebug() << "CommandProc::stopText: killing Command." << endl; + m_waitingStop = true; + m_commandProc->kill(); + } else m_state = psIdle; + }else m_state = psIdle; + kdDebug() << "CommandProc::stopText: Command stopped." << endl; +} + +void CommandProc::slotProcessExited(KProcess*) +{ + kdDebug() << "CommandProc:slotProcessExited: Command process has exited." << endl; + pluginState prevState = m_state; + if (m_waitingStop) + { + m_waitingStop = false; + m_state = psIdle; + emit stopped(); + } else { + m_state = psFinished; + if (prevState == psSaying) + emit sayFinished(); + else + if (prevState == psSynthing) + emit synthFinished(); + } +} + +void CommandProc::slotReceivedStdout(KProcess*, char* buffer, int buflen) +{ + QString buf = QString::fromLatin1(buffer, buflen); + kdDebug() << "CommandProc::slotReceivedStdout: Received output from Command: " << buf << endl; +} + +void CommandProc::slotReceivedStderr(KProcess*, char* buffer, int buflen) +{ + QString buf = QString::fromLatin1(buffer, buflen); + kdDebug() << "CommandProc::slotReceivedStderr: Received error from Command: " << buf << endl; +} + +void CommandProc::slotWroteStdin(KProcess*) +{ + kdDebug() << "CommandProc::slotWroteStdin: closing Stdin" << endl; + m_commandProc->closeStdin(); +} + +/** +* Return the current state of the plugin. +* This function only makes sense in asynchronous mode. +* @return The pluginState of the plugin. +* +* @see pluginState +*/ +pluginState CommandProc::getState() { return m_state; } + +/** +* Acknowledges a finished state and resets the plugin state to psIdle. +* +* If the plugin is not in state psFinished, nothing happens. +* The plugin may use this call to do any post-processing cleanup, +* for example, blanking the stored filename (but do not delete the file). +* Calling program should call getFilename prior to ackFinished. +*/ +void CommandProc::ackFinished() +{ + if (m_state == psFinished) + { + m_state = psIdle; + m_synthFilename = QString::null; + if (!m_textFilename.isNull()) QFile::remove(m_textFilename); + m_textFilename = QString::null; + } +} + +/** +* Returns True if the plugin supports asynchronous processing, +* i.e., returns immediately from sayText or synthText. +* @return True if this plugin supports asynchronous processing. +* +* If the plugin returns True, it must also implement @ref getState . +* It must also emit @ref sayFinished or @ref synthFinished signals when +* saying or synthesis is completed. +*/ +bool CommandProc::supportsAsync() { return true; } + +/** +* Returns True if the plugin supports synthText method, +* i.e., is able to synthesize text to a sound file without +* audibilizing the text. +* @return True if this plugin supports synthText method. +*/ +bool CommandProc::supportsSynth() { return m_supportsSynth; } |