/***************************************************** 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 Current Maintainer: Gary Cramblitt ******************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ // TQt includes. #include #include #include #include #include #include #include // KDE includes. #include #include #include #include #include // KTTS includes. #include // Command Plugin includes. #include "commandproc.h" #include "commandproc.moc" /** Constructor */ CommandProc::CommandProc( TQObject* parent, const char* name, const TQStringList& /*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()) TQFile::remove(m_textFilename); } } /** Initialize */ bool CommandProc::init(KConfig *config, const TQString &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")); TQString 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 TQString &text) { synth(text, TQString(), 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 TQString& text, const TQString& 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 TQTextCodec if encoding==UseCodec * @param language The language code (used for the %l macro) */ void CommandProc::synth(const TQString& inputText, const TQString& suggestedFilename, const TQString& userCmd, bool stdIn, TQTextCodec *codec, TQString& language) { if (m_commandProc) { if (m_commandProc->isRunning()) m_commandProc->kill(); delete m_commandProc; m_commandProc = 0; m_synthFilename = TQString(); if (!m_textFilename.isNull()) TQFile::remove(m_textFilename); m_textFilename = TQString(); } TQString command = userCmd; TQString text = inputText.stripWhiteSpace(); if (text.isEmpty()) return; // 1. prepare the text: // 1.a) encode the text text += "\n"; TQCString 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 TQString 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"); TQTextStream* fs = tempFile.textStream(); fs->setCodec(codec); *fs << text; *fs << endl; m_textFilename = tempFile.file()->name(); tempFile.close(); } else m_textFilename = TQString(); // 2. replace variables with values TQValueStack stack; bool issinglequote=false; bool isdoublequote=false; int noreplace=0; TQRegExp re_noquote("(\"|'|\\\\|`|\\$\\(|\\$\\{|\\(|\\{|\\)|\\}|%%|%t|%f|%l|%w)"); TQRegExp re_singlequote("('|%%|%t|%f|%l|%w)"); TQRegExp 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, "$("); TQRegExp 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 ${...} { TQString 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, TQT_SIGNAL(processExited(KProcess*)), this, TQT_SLOT(slotProcessExited(KProcess*))); connect(m_commandProc, TQT_SIGNAL(receivedStdout(KProcess*, char*, int)), this, TQT_SLOT(slotReceivedStdout(KProcess*, char*, int))); connect(m_commandProc, TQT_SIGNAL(receivedStderr(KProcess*, char*, int)), this, TQT_SLOT(slotReceivedStderr(KProcess*, char*, int))); connect(m_commandProc, TQT_SIGNAL(wroteStdin(KProcess*)), this, TQT_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. */ TQString 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) { TQString buf = TQString::tqfromLatin1(buffer, buflen); kdDebug() << "CommandProc::slotReceivedStdout: Received output from Command: " << buf << endl; } void CommandProc::slotReceivedStderr(KProcess*, char* buffer, int buflen) { TQString buf = TQString::tqfromLatin1(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 = TQString(); if (!m_textFilename.isNull()) TQFile::remove(m_textFilename); m_textFilename = TQString(); } } /** * 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; }