You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdeaccessibility/kttsd/plugins/command/commandproc.cpp

447 lines
15 KiB

/***************************************************** 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. *
* *
***************************************************************************/
// TQt includes.
#include <tqfile.h>
#include <tqstring.h>
#include <tqvaluelist.h>
#include <tqstringlist.h>
#include <tqregexp.h>
#include <tqtextcodec.h>
#include <tqvaluestack.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( 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<bool> 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; }