diff options
Diffstat (limited to 'kttsd/plugins')
74 files changed, 11940 insertions, 0 deletions
diff --git a/kttsd/plugins/Makefile.am b/kttsd/plugins/Makefile.am new file mode 100644 index 0000000..7bf657e --- /dev/null +++ b/kttsd/plugins/Makefile.am @@ -0,0 +1,37 @@ +#kdevelop: festivalint_DIR = festivalint +if include_kttsd_festivalint + festivalint_DIR = festivalint +endif + +#kdevelop: freetts_DIR = freetts +if include_kttsd_freetts + freetts_DIR = freetts +endif + +#kdevelop: command_DIR = command +if include_kttsd_command + command_DIR = command +endif + +#kdevelop: hadifix_DIR = hadifix +if include_kttsd_hadifix + hadifix_DIR = hadifix +endif + +#kdevelop: flite_DIR = flite +if include_kttsd_flite + flite_DIR = flite +endif + +#kdevelop: epos_DIR = epos +if include_kttsd_epos + epos_DIR = epos +endif + +SUBDIRS = \ + $(festivalint_DIR) \ + $(freetts_DIR) \ + $(command_DIR) \ + $(hadifix_DIR) \ + $(flite_DIR) \ + $(epos_DIR) diff --git a/kttsd/plugins/command/Makefile.am b/kttsd/plugins/command/Makefile.am new file mode 100644 index 0000000..b180955 --- /dev/null +++ b/kttsd/plugins/command/Makefile.am @@ -0,0 +1,18 @@ +INCLUDES = \ + -I$(top_srcdir)/kttsd/libkttsd -I$(top_builddir)/kttsd/libkttsd \ + $(all_includes) + +METASOURCES = AUTO + +kde_module_LTLIBRARIES = libkttsd_commandplugin.la + +libkttsd_commandplugin_la_SOURCES = \ + commandconfwidget.ui \ + commandconf.cpp \ + commandproc.cpp \ + commandplugin.cpp +libkttsd_commandplugin_la_LDFLAGS = $(KDE_PLUGIN) $(all_libraries) +libkttsd_commandplugin_la_LIBADD = $(top_builddir)/kttsd/libkttsd/libkttsd.la + +services_DATA = kttsd_commandplugin.desktop +servicesdir = $(kde_servicesdir) diff --git a/kttsd/plugins/command/README b/kttsd/plugins/command/README new file mode 100644 index 0000000..55839ae --- /dev/null +++ b/kttsd/plugins/command/README @@ -0,0 +1,8 @@ +This is the directory containing the Command plug in. +This plug in is developed and maintained by Gunnar Schmi Dt. +-- This is no longer necesary for this plug in -- +If you intend to edit the GUI (commandconfwidget.ui) you will have to first +copy kttsd/libkttsd/pluginconf.h to a standard include directory +like $KDEDIR/include/ since the interface has been tweaked to derive +from PlugInConf instead of QWidget and Qt Designer refuses to open it +if the header is not properly placed. diff --git a/kttsd/plugins/command/commandconf.cpp b/kttsd/plugins/command/commandconf.cpp new file mode 100644 index 0000000..9d70ff2 --- /dev/null +++ b/kttsd/plugins/command/commandconf.cpp @@ -0,0 +1,217 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Configuration for the Command Plug in + ------------------- + Copyright : (C) 2002,2004 by Gunnar Schmi Dt and 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 <qlayout.h> +#include <qcheckbox.h> +#include <qfile.h> +#include <qapplication.h> +#include <qtextcodec.h> + +// KDE includes. +#include <kdialog.h> +#include <kdebug.h> +#include <klocale.h> +#include <kdialog.h> +#include <kcombobox.h> +#include <ktempfile.h> +#include <kstandarddirs.h> +#include <kprogress.h> + +// KTTS includes. +#include <pluginconf.h> +#include <testplayer.h> + +// Command Plugin includes. +#include "commandproc.h" +#include "commandconf.h" + +/** Constructor */ +CommandConf::CommandConf( QWidget* parent, const char* name, const QStringList& /*args*/) : + PlugInConf(parent, name) +{ + // kdDebug() << "CommandConf::CommandConf: Running" << endl; + m_commandProc = 0; + m_progressDlg = 0; + + QVBoxLayout *layout = new QVBoxLayout(this, KDialog::marginHint(), + KDialog::spacingHint(), "CommandConfigWidgetLayout"); + layout->setAlignment (Qt::AlignTop); + m_widget = new CommandConfWidget(this, "CommandConfigWidget"); + layout->addWidget(m_widget); + + // Build codec list and fill combobox. + m_codecList = PlugInProc::buildCodecList(); + m_widget->characterCodingBox->clear(); + m_widget->characterCodingBox->insertStringList(m_codecList); + + defaults(); + connect(m_widget->characterCodingBox, SIGNAL(textChanged(const QString&)), + this, SLOT(configChanged())); + connect(m_widget->characterCodingBox, SIGNAL(activated(const QString&)), + this, SLOT(configChanged())); + connect(m_widget->stdInButton, SIGNAL(toggled(bool)), + this, SLOT(configChanged())); + connect(m_widget->urlReq, SIGNAL(textChanged(const QString&)), + this, SLOT(configChanged())); + connect(m_widget->commandTestButton, SIGNAL(clicked()), + this, SLOT(slotCommandTest_clicked())); +} + +/** Destructor */ +CommandConf::~CommandConf() +{ + // kdDebug() << "CommandConf::~CommandConf: Running" << endl; + if (!m_waveFile.isNull()) QFile::remove(m_waveFile); + delete m_commandProc; + delete m_progressDlg; +} + +void CommandConf::load(KConfig *config, const QString &configGroup) { + // kdDebug() << "CommandConf::load: Running" << endl; + config->setGroup(configGroup); + m_widget->urlReq->setURL (config->readEntry("Command", "cat -")); + m_widget->stdInButton->setChecked(config->readBoolEntry("StdIn", false)); + QString codecString = config->readEntry("Codec", "Local"); + m_languageCode = config->readEntry("LanguageCode", m_languageCode); + int codec = PlugInProc::codecNameToListIndex(codecString, m_codecList); + m_widget->characterCodingBox->setCurrentItem(codec); +} + +void CommandConf::save(KConfig *config, const QString &configGroup) { + // kdDebug() << "CommandConf::save: Running" << endl; + config->setGroup(configGroup); + config->writeEntry("Command", m_widget->urlReq->url()); + config->writeEntry("StdIn", m_widget->stdInButton->isChecked()); + int codec = m_widget->characterCodingBox->currentItem(); + config->writeEntry("Codec", PlugInProc::codecIndexToCodecName(codec, m_codecList)); +} + +void CommandConf::defaults(){ + // kdDebug() << "CommandConf::defaults: Running" << endl; + m_widget->urlReq->setURL("cat -"); + m_widget->stdInButton->setChecked(false); + m_widget->urlReq->setShowLocalProtocol (false); + m_widget->characterCodingBox->setCurrentItem(0); +} + +void CommandConf::setDesiredLanguage(const QString &lang) +{ + m_languageCode = lang; +} + +QString CommandConf::getTalkerCode() +{ + QString url = m_widget->urlReq->url(); + if (!url.isEmpty()) + { + // Must contain either text or file parameter, or StdIn checkbox must be checked, + // otherwise, does nothing! + if ((url.contains("%t") > 0) || (url.contains("%f") > 0) || m_widget->stdInButton->isChecked()) + { + return QString( + "<voice lang=\"%1\" name=\"%2\" gender=\"%3\" />" + "<prosody volume=\"%4\" rate=\"%5\" />" + "<kttsd synthesizer=\"%6\" />") + .arg(m_languageCode) + .arg("fixed") + .arg("neutral") + .arg("medium") + .arg("medium") + .arg("Command"); + } + } + return QString::null; +} + +void CommandConf::slotCommandTest_clicked() +{ + // kdDebug() << "CommandConf::slotCommandTest_clicked(): " << endl; + // If currently synthesizing, stop it. + if (m_commandProc) + m_commandProc->stopText(); + else + { + m_commandProc = new CommandProc(); + connect (m_commandProc, SIGNAL(stopped()), this, SLOT(slotSynthStopped())); + } + + // Create a temp file name for the wave file. + KTempFile tempFile (locateLocal("tmp", "commandplugin-"), ".wav"); + QString tmpWaveFile = tempFile.file()->name(); + tempFile.close(); + + // Get test message in the language of the voice. + QString testMsg = testMessage(m_languageCode); + + // Tell user to wait. + m_progressDlg = new KProgressDialog(m_widget, "kttsmgr_command_testdlg", + i18n("Testing"), + i18n("Testing."), + true); + m_progressDlg->progressBar()->hide(); + m_progressDlg->setAllowCancel(true); + + // TODO: Do codec names contain non-ASCII characters? + connect (m_commandProc, SIGNAL(synthFinished()), this, SLOT(slotSynthFinished())); + m_commandProc->synth( + testMsg, + tmpWaveFile, + m_widget->urlReq->url(), + m_widget->stdInButton->isChecked(), + PlugInProc::codecIndexToCodec(m_widget->characterCodingBox->currentItem(), m_codecList), + m_languageCode); + + // Display progress dialog modally. Processing continues when plugin signals synthFinished, + // or if user clicks Cancel button. + m_progressDlg->exec(); + disconnect (m_commandProc, SIGNAL(synthFinished()), this, SLOT(slotSynthFinished())); + if (m_progressDlg->wasCancelled()) m_commandProc->stopText(); + delete m_progressDlg; + m_progressDlg = 0; +} + +void CommandConf::slotSynthFinished() +{ + // If user canceled, progress dialog is gone, so exit. + if (!m_progressDlg) + { + m_commandProc->ackFinished(); + return; + } + // Hide the Cancel button so user can't cancel in the middle of playback. + m_progressDlg->showCancelButton(false); + // Get new wavefile name. + m_waveFile = m_commandProc->getFilename(); + // Tell synth we're done. + m_commandProc->ackFinished(); + // Play the wave file (possibly adjusting its Speed). + // Player object deletes the wave file when done. + if (m_player) m_player->play(m_waveFile); + QFile::remove(m_waveFile); + m_waveFile = QString::null; + if (m_progressDlg) m_progressDlg->close(); +} + +void CommandConf::slotSynthStopped() +{ + // Clean up after canceling test. + QString filename = m_commandProc->getFilename(); + if (!filename.isNull()) QFile::remove(filename); +} + +#include "commandconf.moc" diff --git a/kttsd/plugins/command/commandconf.h b/kttsd/plugins/command/commandconf.h new file mode 100644 index 0000000..7e9a573 --- /dev/null +++ b/kttsd/plugins/command/commandconf.h @@ -0,0 +1,121 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Configuration for the Command Plug in + ------------------- + Copyright : (C) 2002,2004 by Gunnar Schmi Dt and 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. * + * * + ***************************************************************************/ + +#ifndef _COMMANDCONF_H_ +#define _COMMANDCONF_H_ + +// Qt includes. +#include <qstring.h> +#include <qstringlist.h> + +// KDE includes. +#include <kconfig.h> + +// KTTS includes. +#include <pluginconf.h> + +// Command Plugin includes. +#include "commandconfwidget.h" + +class CommandProc; +class KProgressDialog; + +class CommandConf : public PlugInConf { + Q_OBJECT + + public: + /** Constructor */ + CommandConf( QWidget* parent = 0, const char* name = 0, const QStringList &args = QStringList()); + + /** Destructor */ + ~CommandConf(); + + /** This method is invoked whenever the module should read its + * configuration (most of the times from a config file) and update the + * user interface. This happens when the user clicks the "Reset" button in + * the control center, to undo all of his changes and restore the currently + * valid settings. NOTE that this is not called after the modules is loaded, + * so you probably want to call this method in the constructor. + */ + void load(KConfig *config, const QString &configGroup); + + /** This function gets called when the user wants to save the settings in + * the user interface, updating the config files or wherever the + * configuration is stored. The method is called when the user clicks "Apply" + * or "Ok". + */ + void save(KConfig *config, const QString &configGroup); + + /** This function is called to set the settings in the module to sensible + * default values. It gets called when hitting the "Default" button. The + * default values should probably be the same as the ones the application + * uses when started without a config file. + */ + void defaults(); + + /** + * This function informs the plugin of the desired language to be spoken + * by the plugin. The plugin should attempt to adapt itself to the + * specified language code, choosing sensible defaults if necessary. + * If the passed-in code is QString::null, no specific language has + * been chosen. + * @param lang The desired language code or Null if none. + * + * If the plugin is unable to support the desired language, that is OK. + * Language codes are given by ISO 639-1 and are in lowercase. + * The code may also include an ISO 3166 country code in uppercase + * separated from the language code by underscore (_). For + * example, en_GB. If your plugin supports the given language, but + * not the given country, treat it as though the country + * code were not specified, i.e., adapt to the given language. + */ + void setDesiredLanguage(const QString &lang); + + /** + * Return fully-specified talker code for the configured plugin. This code + * uniquely identifies the configured instance of the plugin and distinquishes + * one instance from another. If the plugin has not been fully configured, + * i.e., cannot yet synthesize, return QString::null. + * @return Fully-specified talker code. + */ + QString getTalkerCode(); + + private slots: + void configChanged(){ + // kdDebug() << "CommandConf::configChanged: Running" << endl; + emit changed(true); + }; + void slotCommandTest_clicked(); + void slotSynthFinished(); + void slotSynthStopped(); + + private: + QString m_languageCode; + + // Configuration Widget. + CommandConfWidget* m_widget; + + // Command synthesizer. + CommandProc* m_commandProc; + // Synthesized wave file name. + QString m_waveFile; + // Progress dialog. + KProgressDialog* m_progressDlg; + // Codec list. + QStringList m_codecList; +}; +#endif // _COMMANDCONF_H_ diff --git a/kttsd/plugins/command/commandconfwidget.ui b/kttsd/plugins/command/commandconfwidget.ui new file mode 100644 index 0000000..b0b749c --- /dev/null +++ b/kttsd/plugins/command/commandconfwidget.ui @@ -0,0 +1,227 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>CommandConfWidget</class> +<widget class="QWidget"> + <property name="name"> + <cstring>CommandConfWidget</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>582</width> + <height>322</height> + </rect> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QGroupBox" row="0" column="0"> + <property name="name"> + <cstring>confiurationBox</cstring> + </property> + <property name="title"> + <string>Co&mmand Configuration</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>11</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="Line" row="1" column="0"> + <property name="name"> + <cstring>line</cstring> + </property> + <property name="frameShape"> + <enum>HLine</enum> + </property> + <property name="frameShadow"> + <enum>Sunken</enum> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + </widget> + <widget class="QLabel" row="2" column="0"> + <property name="name"> + <cstring>urlLabel</cstring> + </property> + <property name="text"> + <string>Command &for speaking texts:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>urlReq</cstring> + </property> + </widget> + <widget class="KURLRequester" row="3" column="0"> + <property name="name"> + <cstring>urlReq</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>This field specifies both the command used for speaking texts and its parameters. If you want to pass the text as a parameter, write %t at the place where the text should be inserted. To pass a file of the text, write %f. To synthesize only and let KTTSD play the synthesized text, write %w for the generated audio file.</string> + </property> + </widget> + <widget class="QLayoutWidget" row="5" column="0"> + <property name="name"> + <cstring>layout1</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QCheckBox"> + <property name="name"> + <cstring>stdInButton</cstring> + </property> + <property name="text"> + <string>&Send the data as standard input</string> + </property> + <property name="whatsThis" stdset="0"> + <string>This check box specifies whether the text is sent as standard input (stdin) to the speech synthesizer.</string> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>spacer1</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>201</width> + <height>20</height> + </size> + </property> + </spacer> + <widget class="QPushButton"> + <property name="name"> + <cstring>commandTestButton</cstring> + </property> + <property name="text"> + <string>&Test</string> + </property> + </widget> + </hbox> + </widget> + <widget class="QLayoutWidget" row="4" column="0"> + <property name="name"> + <cstring>layout5</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>characterCodingLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Character &encoding:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>characterCodingBox</cstring> + </property> + </widget> + <widget class="KComboBox"> + <property name="name"> + <cstring>characterCodingBox</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="whatsThis" stdset="0"> + <string>This combo box specifies which character encoding is used for passing the text.</string> + </property> + </widget> + </hbox> + </widget> + <widget class="QLabel" row="0" column="0"> + <property name="name"> + <cstring>explanationLabel</cstring> + </property> + <property name="text"> + <string>Parameters: + %t: Text to be spoken + %f: Filename of a temporary file containing the text + %l: Language (two letter code) + %w: Filename of a temporary file for generated audio</string> + </property> + </widget> + </grid> + </widget> + <spacer row="1" column="0"> + <property name="name"> + <cstring>spacer2</cstring> + </property> + <property name="orientation"> + <enum>Vertical</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>20</width> + <height>30</height> + </size> + </property> + </spacer> + </grid> +</widget> +<customwidgets> +</customwidgets> +<tabstops> + <tabstop>urlReq</tabstop> + <tabstop>characterCodingBox</tabstop> + <tabstop>stdInButton</tabstop> + <tabstop>commandTestButton</tabstop> +</tabstops> +<includes> + <include location="global" impldecl="in declaration">kurlrequester.h</include> + <include location="global" impldecl="in declaration">klineedit.h</include> + <include location="global" impldecl="in declaration">kpushbutton.h</include> + <include location="global" impldecl="in declaration">kcombobox.h</include> + <include location="global" impldecl="in implementation">kurlrequester.h</include> + <include location="global" impldecl="in implementation">klineedit.h</include> + <include location="global" impldecl="in implementation">kpushbutton.h</include> + <include location="global" impldecl="in implementation">kcombobox.h</include> +</includes> +<signals> + <signal>configurationChanged()</signal> +</signals> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>kurlrequester.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>kcombobox.h</includehint> +</includehints> +</UI> diff --git a/kttsd/plugins/command/commandplugin.cpp b/kttsd/plugins/command/commandplugin.cpp new file mode 100644 index 0000000..119ca47 --- /dev/null +++ b/kttsd/plugins/command/commandplugin.cpp @@ -0,0 +1,24 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + ------------------- + 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. * + * * + ***************************************************************************/ + +#include <kgenericfactory.h> + +#include "commandconf.h" +#include "commandproc.h" + +typedef K_TYPELIST_2( CommandProc, CommandConf ) Command; +K_EXPORT_COMPONENT_FACTORY( libkttsd_commandplugin, KGenericFactory<Command>("kttsd_command") ) + 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; } diff --git a/kttsd/plugins/command/commandproc.h b/kttsd/plugins/command/commandproc.h new file mode 100644 index 0000000..4b46fe5 --- /dev/null +++ b/kttsd/plugins/command/commandproc.h @@ -0,0 +1,201 @@ +/***************************************************** 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. * + * * + ***************************************************************************/ + +#ifndef _COMMANDPROC_H_ +#define _COMMANDPROC_H_ + +// Qt includes. +#include <qstringlist.h> + +// KTTS includes. +#include <pluginproc.h> + +class KProcess; +class QTextCodec; + +class CommandProc : public PlugInProc{ + Q_OBJECT + + public: + /** Constructor */ + CommandProc( QObject* parent = 0, const char* name = 0, + const QStringList &args = QStringList()); + + /** Destructor */ + ~CommandProc(); + + /** Initializate the speech */ + bool init (KConfig *config, const QString &configGroup); + + /** + * Say a text string. + * @param text The text to speak. + */ + virtual void sayText(const QString &text); + + /** + * 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. + */ + virtual void synthText(const QString& text, const QString& suggestedFilename); + + /** + * 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. + */ + virtual QString getFilename(); + + /** + * 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. + */ + virtual void stopText(); + + /** + * Return the current state of the plugin. + * This function only makes sense in asynchronous mode. + * @return The pluginState of the plugin. + * + * @see pluginState + */ + virtual pluginState getState(); + + /** + * 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. + */ + virtual void ackFinished(); + + /** + * 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. + */ + virtual bool supportsAsync(); + + /** + * 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. + */ + virtual bool supportsSynth(); + + /** + * 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 Codec for encoding the text. + * @param language The language code (used for the %l macro) + */ + void synth(const QString& inputText, const QString& suggestedFilename, + const QString& userCmd, bool stdIn, + QTextCodec *codec, QString& language); + + private slots: + void slotProcessExited(KProcess* proc); + void slotReceivedStdout(KProcess* proc, char* buffer, int buflen); + void slotReceivedStderr(KProcess* proc, char* buffer, int buflen); + void slotWroteStdin(KProcess* proc); + + private: + + /** + * True if the plugin supports separate synthesis (option set by user). + */ + bool m_supportsSynth; + + /** + * TTS command + */ + QString m_ttsCommand; + + /** + * True if process should use Stdin. + */ + bool m_stdin; + + /** + * Language Group. + */ + QString m_language; + + /** + * Codec. + */ + QTextCodec* m_codec; + + /** + * Flite process + */ + KProcess* m_commandProc; + + /** + * Name of temporary file containing text. + */ + QString m_textFilename; + + /** + * Synthesis filename. + */ + QString m_synthFilename; + + /** + * Plugin state. + */ + pluginState m_state; + + /** + * True when stopText has been called. Used to force transition to psIdle when + * Flite exits. + */ + bool m_waitingStop; +}; + +#endif // _COMMANDPROC_H_ diff --git a/kttsd/plugins/command/configure.in.in b/kttsd/plugins/command/configure.in.in new file mode 100644 index 0000000..125ee26 --- /dev/null +++ b/kttsd/plugins/command/configure.in.in @@ -0,0 +1,17 @@ +dnl ========================== +dnl checks for Command plug in +dnl ========================== + +AC_ARG_ENABLE(kttsd-command, + AC_HELP_STRING([--enable-kttsd-command], + [build KTTSD Command Plugin [default=yes]]), + command_plugin=$enableval, + command_plugin=yes) + +compile_command_plugin="no" + +if test "x$command_plugin" = "xyes"; then + compile_command_plugin="yes" +fi + +AM_CONDITIONAL(include_kttsd_command, test "x$compile_command_plugin" = "xyes") diff --git a/kttsd/plugins/command/kttsd_commandplugin.desktop b/kttsd/plugins/command/kttsd_commandplugin.desktop new file mode 100644 index 0000000..c141790 --- /dev/null +++ b/kttsd/plugins/command/kttsd_commandplugin.desktop @@ -0,0 +1,92 @@ +[Desktop Entry] +Name=Command +Name[br]=Urzhiad +Name[bs]=Naredba +Name[ca]=Odre +Name[cs]=Příkaz +Name[cy]=Gorchymyn +Name[da]=Kommando +Name[de]=Befehl +Name[el]=Εντολή +Name[es]=Orden +Name[et]=Käsk +Name[fa]=فرمان +Name[fi]=Komento +Name[fr]=Commande +Name[ga]=Ordú +Name[gl]=Comando +Name[he]=פקודה +Name[hu]=Parancs +Name[is]=Skipun +Name[it]=Comando +Name[ja]=コマンド +Name[ka]=ბრძანება +Name[km]=ពាក្យបញ្ជា +Name[mk]=Командна линија +Name[ms]=Arahan +Name[nb]=Kommando +Name[nds]=Befehl +Name[ne]=आदेश +Name[pa]=ਕਮਾਂਡ +Name[pl]=Polecenie +Name[pt]=Comando +Name[ru]=Командная строка +Name[sk]=Príkaz +Name[sl]=Ukaz +Name[sr]=Наредба +Name[sr@Latn]=Naredba +Name[sv]=Kommando +Name[ta]=கட்டளை +Name[tg]=Сатри фармоишӣ +Name[tr]=Komut +Name[uk]=Команда +Name[vi]=Ra lệnh +Name[zh_TW]=命令 +Comment=Generic speech synthesizer from command line +Comment[bg]=Общ синтезатор на глас от командния ред +Comment[bs]=Generalna sinteza govora sa komandne linije +Comment[ca]=Sintetitzador de veu genèric de línia d'ordres +Comment[cs]=Hlasový syntetizátor pro příkazovou řádku +Comment[da]=Generisk tale-synthesizer fra kommandolinjen +Comment[de]=Generischer Sprachsynthesizer in der Befehlszeile +Comment[el]=Γενικός συνθέτης ομιλίας για τη γραμμή εντολών +Comment[es]=Sintetizador genérico de texto a voz para la línea de órdenes +Comment[et]=Üldine käsurea-kõnesüntesaator +Comment[eu]=Komando-lerroko hizketa-sintetizadore generikoa +Comment[fa]=ترکیبدهندۀ گفتار عمومی از خط فرمان +Comment[fi]=Yleinen komentorivipohjainen puhesyntetisaattori +Comment[fr]=Synthèse vocale générique en ligne de commande +Comment[ga]=Sintéiseoir cainte ginearálta ó líne na n-orduithe +Comment[gl]=Sintetizados de fala xenérico para a liña de comandos +Comment[hu]=Parancssoros kezelőprogram szövegfelolvasáshoz +Comment[is]=Almennur talgerfill frá skipanalínu +Comment[it]=Sintetizzatore vocale generico dalla riga di comando +Comment[ja]=コマンドラインからの汎用スピーチシンセサイザ +Comment[ka]=საზოგადო ხმის სინქრონიზატორი ბრძანების ველიდან +Comment[km]=កម្មវិធីសង្គ្រោះការនិយាយទូទៅពីបន្ទាត់ពាក្យបញ្ជា +Comment[mk]=Синтисајзер на општ говор од командната линија +Comment[ms]=Pensintesis tutur generik dari baris arahan +Comment[nb]=Generisk talesyntetisering fra kommandolinje +Comment[nds]=Blicksnuut för de Befehlsreeg +Comment[ne]=आदेश रेखाबाट जेनेरिक संवाद सिन्थेसाइजर +Comment[nl]=Generieke spraaksynthesizer voor de commandoregel +Comment[pa]=ਕਮਾਂਡ ਲਾਈਨ ਤੋਂ ਸਧਾਰਨ ਬੋਲੀ ਸੰਸਲੇਸ਼ਕ +Comment[pl]=Program syntezatora mowy uruchamiany z linii poleceń +Comment[pt]=Sintetizador de fala genérico através de uma linha de comandos +Comment[pt_BR]=Interface de linha de comando para o sintetizador de fala genérico +Comment[ru]=Консольный интерфейс к движкам синтеза речи +Comment[sk]=Všeobecný syntetizátor reči z príkazového riadka +Comment[sl]=Generični sintetizator govora iz ukazne vrstice +Comment[sr]=Генерички синтетизатор говора из командне линије +Comment[sr@Latn]=Generički sintetizator govora iz komandne linije +Comment[sv]=Generell talsyntes från kommandoraden +Comment[ta]=கட்டளை வரியில் இருந்து பொது பேச்சு கூட்டிணைப்பாளர் +Comment[tg]=Консоли интерфейс ба микшерҳо барои таҳлили овоз +Comment[tr]=Komut satırından genel konuşma bireştirici +Comment[uk]=Загальний синтезатор мовлення з командного рядка +Comment[vi]=Tổng hợp giọng nói chung cho việc ra lệnh +Comment[zh_TW]=從命令列的一般語音合成器 +Type=Service +ServiceTypes=KTTSD/SynthPlugin +X-KDE-Library=libkttsd_commandplugin +X-KDE-Languages=other diff --git a/kttsd/plugins/epos/Makefile.am b/kttsd/plugins/epos/Makefile.am new file mode 100644 index 0000000..8f40ca7 --- /dev/null +++ b/kttsd/plugins/epos/Makefile.am @@ -0,0 +1,20 @@ +INCLUDES = \ + -I$(top_srcdir)/kttsd/libkttsd -I$(top_builddir)/kttsd/libkttsd \ + $(all_includes) + +METASOURCES = AUTO + +kde_module_LTLIBRARIES = libkttsd_eposplugin.la + +libkttsd_eposplugin_la_SOURCES = \ + eposconfwidget.ui \ + eposconf.cpp \ + eposproc.cpp \ + eposplugin.cpp +libkttsd_eposplugin_la_LDFLAGS = $(KDE_PLUGIN) $(all_libraries) +libkttsd_eposplugin_la_LIBADD = $(top_builddir)/kttsd/libkttsd/libkttsd.la + +services_DATA = kttsd_eposplugin.desktop +servicesdir = $(kde_servicesdir) + +noinst_HEADERS = eposproc.h eposconf.h eposconfwidget.h diff --git a/kttsd/plugins/epos/README b/kttsd/plugins/epos/README new file mode 100644 index 0000000..2f61a9c --- /dev/null +++ b/kttsd/plugins/epos/README @@ -0,0 +1,3 @@ +This is the directory containing the Epos plug in. +This plugin is developed and maintained by Gary Cramblitt. +<garycramblitt@comcast.net> diff --git a/kttsd/plugins/epos/configure.in.bot b/kttsd/plugins/epos/configure.in.bot new file mode 100644 index 0000000..59f4fdc --- /dev/null +++ b/kttsd/plugins/epos/configure.in.bot @@ -0,0 +1,15 @@ +if test "x$epos_bindir" = "xno"; then + if test "$compile_epos_plugin" = "yes"; then + echo "" + echo "=======================================================" + echo "The Epos program does not appear to be" + echo "installed on this system. The epos plugin will" + echo "be built, but you need to install epos before you" + echo "can use it. You can get it at" + echo " http://epos.ure.cas.cz/" + echo "Debian users: apt-get install epos" + echo "======================================================" + all_tests=bad + fi +fi + diff --git a/kttsd/plugins/epos/configure.in.in b/kttsd/plugins/epos/configure.in.in new file mode 100644 index 0000000..5a0f655 --- /dev/null +++ b/kttsd/plugins/epos/configure.in.in @@ -0,0 +1,22 @@ +dnl ========================================= +dnl checks for Festival Lite (epos) Plug In +dnl ========================================= + +AC_ARG_ENABLE(kttsd-epos, + AC_HELP_STRING([--enable-kttsd-epos], + [build KTTSD Epos plugin [default=yes]]), + epos_plugin=$enableval, + epos_plugin=yes) + +compile_epos_plugin="yes" + +if test "x$epos_plugin" = "xno"; then + compile_epos_plugin="no" +fi + +dnl Check for epos executable. +dnl Note that epos plugin is always built, unless +dnl user overrides on configure command line. +AC_PATH_PROG(epos_bindir, "epos", "no") + +AM_CONDITIONAL(include_kttsd_epos, test "x$compile_epos_plugin" != "xno") diff --git a/kttsd/plugins/epos/eposconf.cpp b/kttsd/plugins/epos/eposconf.cpp new file mode 100644 index 0000000..063c9f9 --- /dev/null +++ b/kttsd/plugins/epos/eposconf.cpp @@ -0,0 +1,319 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Configuration widget and functions for Epos plug in + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +// C++ includes. +#include <math.h> + +// Qt includes. +#include <qfile.h> +#include <qapplication.h> +#include <qtextcodec.h> +#include <qlayout.h> +#include <qslider.h> + +// KDE includes. +#include <kdialog.h> +#include <ktempfile.h> +#include <kstandarddirs.h> +#include <kcombobox.h> +#include <klocale.h> +#include <knuminput.h> + +// KTTS includes. +#include <testplayer.h> + +// Epos Plugin includes. +#include "eposproc.h" +#include "eposconf.h" +#include "eposconf.moc" + +/** Constructor */ +EposConf::EposConf( QWidget* parent, const char* name, const QStringList& /*args*/) : + PlugInConf(parent, name) +{ + // kdDebug() << "EposConf::EposConf: Running" << endl; + m_eposProc = 0; + m_progressDlg = 0; + + QVBoxLayout *layout = new QVBoxLayout(this, KDialog::marginHint(), + KDialog::spacingHint(), "EposConfigWidgetLayout"); + layout->setAlignment (Qt::AlignTop); + m_widget = new EposConfWidget(this, "EposConfigWidget"); + layout->addWidget(m_widget); + + // Build codec list and fill combobox. + m_codecList = PlugInProc::buildCodecList(); + m_widget->characterCodingBox->clear(); + m_widget->characterCodingBox->insertStringList(m_codecList); + + defaults(); + + connect(m_widget->eposServerPath, SIGNAL(textChanged(const QString&)), + this, SLOT(configChanged())); + connect(m_widget->eposClientPath, SIGNAL(textChanged(const QString&)), + this, SLOT(configChanged())); + connect(m_widget->timeBox, SIGNAL(valueChanged(int)), + this, SLOT(timeBox_valueChanged(int))); + connect(m_widget->frequencyBox, SIGNAL(valueChanged(int)), + this, SLOT(frequencyBox_valueChanged(int))); + connect(m_widget->timeSlider, SIGNAL(valueChanged(int)), + this, SLOT(timeSlider_valueChanged(int))); + connect(m_widget->frequencySlider, SIGNAL(valueChanged(int)), + this, SLOT(frequencySlider_valueChanged(int))); + connect(m_widget->timeBox, SIGNAL(valueChanged(int)), this, SLOT(configChanged())); + connect(m_widget->timeSlider, SIGNAL(valueChanged(int)), this, SLOT(configChanged())); + connect(m_widget->frequencyBox, SIGNAL(valueChanged(int)), this, SLOT(configChanged())); + connect(m_widget->frequencySlider, SIGNAL(valueChanged(int)), this, SLOT(configChanged())); + connect(m_widget->characterCodingBox, SIGNAL(activated(const QString&)), + this, SLOT(configChanged())); + connect(m_widget->eposServerOptions, SIGNAL(textChanged(const QString&)), + this, SLOT(configChanged())); + connect(m_widget->eposClientOptions, SIGNAL(textChanged(const QString&)), + this, SLOT(configChanged())); + connect(m_widget->eposTest, SIGNAL(clicked()), + this, SLOT(slotEposTest_clicked())); +} + +/** Destructor */ +EposConf::~EposConf(){ + // kdDebug() << "Running: EposConf::~EposConf()" << endl; + if (!m_waveFile.isNull()) QFile::remove(m_waveFile); + delete m_eposProc; + delete m_progressDlg; +} + +void EposConf::load(KConfig *config, const QString &configGroup){ + // kdDebug() << "EposConf::load: Running " << endl; + + config->setGroup(configGroup); + m_widget->eposServerPath->setURL(config->readEntry("EposServerExePath", "eposd")); + m_widget->eposClientPath->setURL(config->readEntry("EposClientExePath", "say-epos")); + m_widget->eposServerOptions->setText(config->readEntry("EposServerOptions", "")); + m_widget->eposClientOptions->setText(config->readEntry("EposClientOptions", "")); + QString codecString = config->readEntry("Codec", "ISO 8859-2"); + int codec = PlugInProc::codecNameToListIndex(codecString, m_codecList); + m_widget->timeBox->setValue(config->readNumEntry("time", 100)); + m_widget->frequencyBox->setValue(config->readNumEntry("pitch", 100)); + m_widget->characterCodingBox->setCurrentItem(codec); +} + +/** +* Converts a language code into the language setting passed to Epos synth. +*/ +QString EposConf::languageCodeToEposLanguage(const QString &languageCode) +{ + QString eposLanguage; + if (languageCode.left(2) == "cs") eposLanguage = "czech"; + if (languageCode.left(2) == "sk") eposLanguage = "slovak"; + return eposLanguage; +} + +void EposConf::save(KConfig *config, const QString &configGroup){ + // kdDebug() << "EposConf::save: Running" << endl; + + config->setGroup("Epos"); + config->writeEntry("EposServerExePath", + realFilePath(m_widget->eposServerPath->url())); + config->writeEntry("EposClientExePath", + realFilePath(m_widget->eposClientPath->url())); + config->writeEntry("Language", languageCodeToEposLanguage(m_languageCode)); + config->setGroup(configGroup); + config->writeEntry("EposServerExePath", + realFilePath(m_widget->eposServerPath->url())); + config->writeEntry("EposClientExePath", + realFilePath(m_widget->eposClientPath->url())); + config->writeEntry("EposServerOptions", m_widget->eposServerOptions->text()); + config->writeEntry("EposClientOptions", m_widget->eposClientOptions->text()); + config->writeEntry("time", m_widget->timeBox->value()); + config->writeEntry("pitch", m_widget->frequencyBox->value()); + int codec = m_widget->characterCodingBox->currentItem(); + config->writeEntry("Codec", PlugInProc::codecIndexToCodecName(codec, m_codecList)); +} + +void EposConf::defaults(){ + // kdDebug() << "EposConf::defaults: Running" << endl; + // Epos server command changed from epos to eposd. Epos client command changed from + // say to say-epos. These changes appeared around Epos v2.5.35. Try for these automatically. + QString exeName = "eposd"; + if (realFilePath(exeName).isEmpty()) + if (!realFilePath("epos").isEmpty()) + exeName = "epos"; + m_widget->eposServerPath->setURL(exeName); + exeName = "say-epos"; + if (realFilePath(exeName).isEmpty()) + if (!realFilePath("say").isEmpty()) + exeName = "say"; + m_widget->eposClientPath->setURL(exeName); + m_widget->eposServerOptions->setText(""); + m_widget->eposClientOptions->setText(""); + m_widget->timeBox->setValue(100); + timeBox_valueChanged(100); + m_widget->frequencyBox->setValue(100); + frequencyBox_valueChanged(100); + int codec = PlugInProc::codecNameToListIndex("ISO 8859-2", m_codecList); + m_widget->characterCodingBox->setCurrentItem(codec); +} + +void EposConf::setDesiredLanguage(const QString &lang) +{ + m_languageCode = lang; +} + +QString EposConf::getTalkerCode() +{ + QString eposServerExe = realFilePath(m_widget->eposServerPath->url()); + QString eposClientExe = realFilePath(m_widget->eposClientPath->url()); + if (!eposServerExe.isEmpty() && !eposClientExe.isEmpty()) + { + if (!getLocation(eposServerExe).isEmpty() && !getLocation(eposClientExe).isEmpty()) + { + QString rate = "medium"; + if (m_widget->timeBox->value() < 75) rate = "slow"; + if (m_widget->timeBox->value() > 125) rate = "fast"; + return QString( + "<voice lang=\"%1\" name=\"%2\" gender=\"%3\" />" + "<prosody volume=\"%4\" rate=\"%5\" />" + "<kttsd synthesizer=\"%6\" />") + .arg(m_languageCode) + .arg("fixed") + .arg("neutral") + .arg("medium") + .arg(rate) + .arg("Epos TTS Synthesis System"); + } + } + return QString::null; +} + +void EposConf::slotEposTest_clicked() +{ + // kdDebug() << "EposConf::slotEposTest_clicked(): Running" << endl; + // If currently synthesizing, stop it. + if (m_eposProc) + m_eposProc->stopText(); + else + { + m_eposProc = new EposProc(); + connect (m_eposProc, SIGNAL(stopped()), this, SLOT(slotSynthStopped())); + } + // Create a temp file name for the wave file. + KTempFile tempFile (locateLocal("tmp", "eposplugin-"), ".wav"); + QString tmpWaveFile = tempFile.file()->name(); + tempFile.close(); + + // Get test message in the language of the voice. + QString testMsg = testMessage(m_languageCode); + + // Tell user to wait. + m_progressDlg = new KProgressDialog(m_widget, "kttsmgr_epos_testdlg", + i18n("Testing"), + i18n("Testing."), + true); + m_progressDlg->progressBar()->hide(); + m_progressDlg->setAllowCancel(true); + + // TODO: Whenever server options change, the server must be restarted. + // TODO: Do codec names contain non-ASCII characters? + connect (m_eposProc, SIGNAL(synthFinished()), this, SLOT(slotSynthFinished())); + m_eposProc->synth( + testMsg, + tmpWaveFile, + realFilePath(m_widget->eposServerPath->url()), + realFilePath(m_widget->eposClientPath->url()), + m_widget->eposServerOptions->text(), + m_widget->eposClientOptions->text(), + PlugInProc::codecIndexToCodec(m_widget->characterCodingBox->currentItem(), m_codecList), + languageCodeToEposLanguage(m_languageCode), + m_widget->timeBox->value(), + m_widget->frequencyBox->value() + ); + + // Display progress dialog modally. Processing continues when plugin signals synthFinished, + // or if user clicks Cancel button. + m_progressDlg->exec(); + disconnect (m_eposProc, SIGNAL(synthFinished()), this, SLOT(slotSynthFinished())); + if (m_progressDlg->wasCancelled()) m_eposProc->stopText(); + delete m_progressDlg; + m_progressDlg = 0; +} + +void EposConf::slotSynthFinished() +{ + // If user canceled, progress dialog is gone, so exit. + if (!m_progressDlg) + { + m_eposProc->ackFinished(); + return; + } + // Hide the Cancel button so user can't cancel in the middle of playback. + m_progressDlg->showCancelButton(false); + // Get new wavefile name. + m_waveFile = m_eposProc->getFilename(); + // Tell synth we're done. + m_eposProc->ackFinished(); + // Play the wave file (possibly adjusting its Speed). + // Player object deletes the wave file when done. + if (m_player) m_player->play(m_waveFile); + QFile::remove(m_waveFile); + m_waveFile = QString::null; + if (m_progressDlg) m_progressDlg->close(); +} + +void EposConf::slotSynthStopped() +{ + // Clean up after canceling test. + QString filename = m_eposProc->getFilename(); + if (!filename.isNull()) QFile::remove(filename); +} + +// Basically the slider values are logarithmic (0,...,1000) whereas percent +// values are linear (50%,...,200%). +// +// slider = alpha * (log(percent)-log(50)) +// with alpha = 1000/(log(200)-log(50)) + +int EposConf::percentToSlider(int percentValue) { + double alpha = 1000 / (log(200) - log(50)); + return (int)floor (0.5 + alpha * (log(percentValue)-log(50))); +} + +int EposConf::sliderToPercent(int sliderValue) { + double alpha = 1000 / (log(200) - log(50)); + return (int)floor(0.5 + exp (sliderValue/alpha + log(50))); +} + +void EposConf::timeBox_valueChanged(int percentValue) { + m_widget->timeSlider->setValue (percentToSlider (percentValue)); +} + +void EposConf::frequencyBox_valueChanged(int percentValue) { + m_widget->frequencySlider->setValue(percentToSlider(percentValue)); +} + +void EposConf::timeSlider_valueChanged(int sliderValue) { + m_widget->timeBox->setValue (sliderToPercent (sliderValue)); +} + +void EposConf::frequencySlider_valueChanged(int sliderValue) { + m_widget->frequencyBox->setValue(sliderToPercent(sliderValue)); +} diff --git a/kttsd/plugins/epos/eposconf.h b/kttsd/plugins/epos/eposconf.h new file mode 100644 index 0000000..5c3fbe3 --- /dev/null +++ b/kttsd/plugins/epos/eposconf.h @@ -0,0 +1,141 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Configuration widget and functions for Epos plug in + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#ifndef _EPOSCONF_H_ +#define _EPOSCONF_H_ + +// Qt includes. +#include <qstring.h> + +// KDE includes. +#include <kconfig.h> +#include <kdebug.h> +#include <kprogress.h> + +// KTTS includes. +#include <pluginconf.h> + +// Epos plugin includes. +#include "eposconfwidget.h" + +class EposProc; +class KProgressDialog; + +class EposConf : public PlugInConf { + Q_OBJECT + + public: + /** Constructor */ + EposConf( QWidget* parent = 0, const char* name = 0, const QStringList &args = QStringList()); + + /** Destructor */ + ~EposConf(); + + /** This method is invoked whenever the module should read its + * configuration (most of the times from a config file) and update the + * user interface. This happens when the user clicks the "Reset" button in + * the control center, to undo all of his changes and restore the currently + * valid settings. NOTE that this is not called after the modules is loaded, + * so you probably want to call this method in the constructor. + */ + void load(KConfig *config, const QString &configGroup); + + /** This function gets called when the user wants to save the settings in + * the user interface, updating the config files or wherever the + * configuration is stored. The method is called when the user clicks "Apply" + * or "Ok". + */ + void save(KConfig *config, const QString &configGroup); + + /** This function is called to set the settings in the module to sensible + * default values. It gets called when hitting the "Default" button. The + * default values should probably be the same as the ones the application + * uses when started without a config file. + */ + void defaults(); + + /** + * This function informs the plugin of the desired language to be spoken + * by the plugin. The plugin should attempt to adapt itself to the + * specified language code, choosing sensible defaults if necessary. + * If the passed-in code is QString::null, no specific language has + * been chosen. + * @param lang The desired language code or Null if none. + * + * If the plugin is unable to support the desired language, that is OK. + * Language codes are given by ISO 639-1 and are in lowercase. + * The code may also include an ISO 3166 country code in uppercase + * separated from the language code by underscore (_). For + * example, en_GB. If your plugin supports the given language, but + * not the given country, treat it as though the country + * code were not specified, i.e., adapt to the given language. + */ + void setDesiredLanguage(const QString &lang); + + /** + * Return fully-specified talker code for the configured plugin. This code + * uniquely identifies the configured instance of the plugin and distinquishes + * one instance from another. If the plugin has not been fully configured, + * i.e., cannot yet synthesize, return QString::null. + * @return Fully-specified talker code. + */ + QString getTalkerCode(); + + private slots: + void configChanged(){ + kdDebug() << "EposConf::configChanged: Running" << endl; + emit changed(true); + }; + void slotEposTest_clicked(); + void slotSynthFinished(); + void slotSynthStopped(); + void timeBox_valueChanged(int percentValue); + void frequencyBox_valueChanged(int percentValue); + void timeSlider_valueChanged(int sliderValue); + void frequencySlider_valueChanged(int sliderValue); + + private: + /** + * Converts a language code into the language setting passed to Epos synth. + */ + QString languageCodeToEposLanguage(const QString &languageCode); + + int percentToSlider(int percentValue); + int sliderToPercent(int sliderValue); + + // Language code. + QString m_languageCode; + + // Configuration widget. + EposConfWidget* m_widget; + + // Epos synthesizer. + EposProc* m_eposProc; + // Synthesized wave file name. + QString m_waveFile; + // Progress dialog. + KProgressDialog* m_progressDlg; + // List of displayed codec names. + QStringList m_codecList; +}; +#endif // _EPOSCONF_H_ diff --git a/kttsd/plugins/epos/eposconfwidget.ui b/kttsd/plugins/epos/eposconfwidget.ui new file mode 100644 index 0000000..23e47da --- /dev/null +++ b/kttsd/plugins/epos/eposconfwidget.ui @@ -0,0 +1,610 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>EposConfWidget</class> +<author>Gary Cramblitt</author> +<widget class="QWidget"> + <property name="name"> + <cstring>EposConfWidget</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>535</width> + <height>381</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>7</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="caption"> + <string>Epos Config UI</string> + </property> + <property name="whatsThis" stdset="0"> + <string>This is the configuration dialog for the Epos Czech and Slovak speech synthesizer.</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QGroupBox" row="0" column="0"> + <property name="name"> + <cstring>eposConfigurationBox</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>GroupBoxPanel</enum> + </property> + <property name="frameShadow"> + <enum>Sunken</enum> + </property> + <property name="title"> + <string>E&pos Configuration</string> + </property> + <property name="whatsThis" stdset="0"> + <string>This is the configuration dialog for the Epos Czech and Slovak speech synthesizer.</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>11</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QLayoutWidget" row="2" column="0"> + <property name="name"> + <cstring>layout13</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>characterCodingLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Character &encoding:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>characterCodingBox</cstring> + </property> + </widget> + <widget class="KComboBox"> + <property name="name"> + <cstring>characterCodingBox</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="whatsThis" stdset="0"> + <string>Specifies which character encoding is used for passing the text.</string> + </property> + </widget> + </hbox> + </widget> + <widget class="QLayoutWidget" row="1" column="0"> + <property name="name"> + <cstring>layout17</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout14</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>timeLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Speed:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>timeBox</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the speed of speech. Slide the slider to the left to slow speech down; to the right to increase talking speed. Anything less than 75 percent is considered "slow", and anything greater than 125 percent is considered "fast".</string> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>frequencyLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Pitch:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>frequencyBox</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the tone (frequency) of speech. Slide the slider to the left to lower the voice tone; to the right to increase tone. Anything less than 75 percent is considered "low", and anything greater than 125 percent is considered "high".</string> + </property> + </widget> + </vbox> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout15</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="KIntSpinBox"> + <property name="name"> + <cstring>timeBox</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string> %</string> + </property> + <property name="maxValue"> + <number>200</number> + </property> + <property name="minValue"> + <number>50</number> + </property> + <property name="value"> + <number>100</number> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the speed of speech. Slide the slider to the left to slow speech down; to the right to increase talking speed. Anything less than 75 percent is considered "slow", and anything greater than 125 percent is considered "fast".</string> + </property> + </widget> + <widget class="KIntSpinBox"> + <property name="name"> + <cstring>frequencyBox</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string> %</string> + </property> + <property name="maxValue"> + <number>200</number> + </property> + <property name="minValue"> + <number>50</number> + </property> + <property name="value"> + <number>100</number> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the tone (frequency) of speech. Slide the slider to the left to lower the voice tone; to the right to increase tone. Anything less than 75 percent is considered "low", and anything greater than 125 percent is considered "high".</string> + </property> + </widget> + </vbox> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout16</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QSlider"> + <property name="name"> + <cstring>timeSlider</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="focusPolicy"> + <enum>NoFocus</enum> + </property> + <property name="maxValue"> + <number>1000</number> + </property> + <property name="lineStep"> + <number>10</number> + </property> + <property name="pageStep"> + <number>100</number> + </property> + <property name="value"> + <number>500</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the speed of speech. Slide the slider to the left to slow speech down; to the right to increase talking speed. Anything less than 75 percent is considered "slow", and anything greater than 125 percent is considered "fast".</string> + </property> + </widget> + <widget class="QSlider"> + <property name="name"> + <cstring>frequencySlider</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="focusPolicy"> + <enum>NoFocus</enum> + </property> + <property name="maxValue"> + <number>1000</number> + </property> + <property name="lineStep"> + <number>10</number> + </property> + <property name="pageStep"> + <number>100</number> + </property> + <property name="value"> + <number>500</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the tone (frequency) of speech. Slide the slider to the left to lower the voice tone; to the right to increase tone. Anything less than 75 percent is considered "low", and anything greater than 125 percent is considered "high".</string> + </property> + </widget> + </vbox> + </widget> + </hbox> + </widget> + <widget class="QLayoutWidget" row="0" column="0"> + <property name="name"> + <cstring>layout13</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout11</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>eposServerPathLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Epos server executable path:</string> + </property> + <property name="alignment"> + <set>AlignVCenter</set> + </property> + <property name="buddy" stdset="0"> + <cstring>festivalVoicesPath</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>If the Epos server program will be found due to your PATH environment variable, simply enter "epos", otherwise enter the full path to the Epos server executable program.</string> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>eposClientPathLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Epos client executable path:</string> + </property> + <property name="alignment"> + <set>AlignVCenter</set> + </property> + <property name="buddy" stdset="0"> + <cstring>festivalVoicesPath</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>If the Epos client program will be found due to the PATH environment variable, simply enter "say" here. Otherwise, specify the full path to the Epos client program.</string> + </property> + </widget> + </vbox> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout12</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="KURLRequester"> + <property name="name"> + <cstring>eposServerPath</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="url" stdset="0"> + <string>epos</string> + </property> + <property name="whatsThis" stdset="0"> + <string>If the Epos server program will be found due to your PATH environment variable, simply enter "epos", otherwise enter the full path to the Epos server executable program.</string> + </property> + </widget> + <widget class="KURLRequester"> + <property name="name"> + <cstring>eposClientPath</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="url" stdset="0"> + <string>say</string> + </property> + <property name="whatsThis" stdset="0"> + <string>If the Epos client program will be found due to the PATH environment variable, simply enter "say" here. Otherwise, specify the full path to the Epos client program.</string> + </property> + </widget> + </vbox> + </widget> + </hbox> + </widget> + <widget class="QGroupBox" row="3" column="0"> + <property name="name"> + <cstring>advancedGroupBox</cstring> + </property> + <property name="title"> + <string>Additional Options (advanced)</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLayoutWidget" row="0" column="0"> + <property name="name"> + <cstring>layout14</cstring> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLayoutWidget" row="0" column="1"> + <property name="name"> + <cstring>layout13</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLineEdit"> + <property name="name"> + <cstring>eposServerOptions</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Optional. Enter any server command line options here. To see available options, enter "epos -h" in a terminal. Do not use "-o".</string> + </property> + </widget> + <widget class="QLineEdit"> + <property name="name"> + <cstring>eposClientOptions</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Specify options to be passed to Epos client. To see available options, enter "say -h" in a terminal. Do not use "-o".</string> + </property> + </widget> + </vbox> + </widget> + <widget class="QLayoutWidget" row="0" column="0"> + <property name="name"> + <cstring>layout12</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>eposServerOptionsLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Epos server:</string> + </property> + <property name="alignment"> + <set>AlignVCenter|AlignLeft</set> + </property> + <property name="buddy" stdset="0"> + <cstring>festivalVoicesPath</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Optional. Enter any server command line options here. To see available options, enter "epos -h" in a terminal. Do not use "-o".</string> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>eposClientOptionsLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Epos client:</string> + </property> + <property name="alignment"> + <set>AlignVCenter|AlignLeft</set> + </property> + <property name="buddy" stdset="0"> + <cstring>festivalVoicesPath</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Specify options to be passed to Epos client. Do not use -o. To see available options, enter "say -h" in a terminal. Do not use "-o".</string> + </property> + </widget> + </vbox> + </widget> + </grid> + </widget> + </grid> + </widget> + <widget class="QLayoutWidget" row="4" column="0"> + <property name="name"> + <cstring>layout5</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <spacer> + <property name="name"> + <cstring>spacer2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>410</width> + <height>20</height> + </size> + </property> + </spacer> + <widget class="QPushButton"> + <property name="name"> + <cstring>eposTest</cstring> + </property> + <property name="text"> + <string>&Test</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Click to test the configuration. If correct, you will hear a sentence spoken.</string> + </property> + </widget> + </hbox> + </widget> + </grid> + </widget> + </grid> +</widget> +<customwidgets> +</customwidgets> +<tabstops> + <tabstop>eposServerPath</tabstop> + <tabstop>eposClientPath</tabstop> + <tabstop>characterCodingBox</tabstop> + <tabstop>eposTest</tabstop> +</tabstops> +<includes> + <include location="global" impldecl="in declaration">kurlrequester.h</include> + <include location="global" impldecl="in implementation">kurlrequester.h</include> +</includes> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>kcombobox.h</includehint> + <includehint>knuminput.h</includehint> + <includehint>knuminput.h</includehint> + <includehint>kurlrequester.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>kurlrequester.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> +</includehints> +</UI> diff --git a/kttsd/plugins/epos/eposplugin.cpp b/kttsd/plugins/epos/eposplugin.cpp new file mode 100644 index 0000000..9bc0bee --- /dev/null +++ b/kttsd/plugins/epos/eposplugin.cpp @@ -0,0 +1,31 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Generating the factories so Epos can be used as plug in. + ------------------- + Copyright: + (C) 2005 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#include <kgenericfactory.h> + +#include "eposconf.h" +#include "eposproc.h" + +typedef K_TYPELIST_2( EposProc, EposConf ) Epos; +K_EXPORT_COMPONENT_FACTORY( libkttsd_eposplugin, KGenericFactory<Epos>("kttsd_epos") ) + diff --git a/kttsd/plugins/epos/eposproc.cpp b/kttsd/plugins/epos/eposproc.cpp new file mode 100644 index 0000000..35bc1d3 --- /dev/null +++ b/kttsd/plugins/epos/eposproc.cpp @@ -0,0 +1,389 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + eposproc.cpp + Main speaking functions for the Epos Plug in + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +// C++ includes. +#include <math.h> + +// Qt includes. +#include <qstring.h> +#include <qstringlist.h> +#include <qtextcodec.h> +#include <qfile.h> + +// KDE includes. +#include <kdebug.h> +#include <kconfig.h> +#include <ktempfile.h> +#include <kstandarddirs.h> +#include <kprocess.h> + +// Epos Plugin includes. +#include "eposproc.h" +#include "eposproc.moc" + +/** Constructor */ +EposProc::EposProc( QObject* parent, const char* name, const QStringList& ) : + PlugInProc( parent, name ){ + kdDebug() << "EposProc::EposProc: Running" << endl; + m_state = psIdle; + m_waitingStop = false; + m_eposServerProc = 0; + m_eposProc = 0; +} + +/** Destructor */ +EposProc::~EposProc(){ + kdDebug() << "EposProc::~EposProc:: Running" << endl; + if (m_eposProc) + { + stopText(); + delete m_eposProc; + } + delete m_eposServerProc; +} + +/** Initialize the speech */ +bool EposProc::init(KConfig* config, const QString& configGroup) +{ + // kdDebug() << "EposProc::init: Running" << endl; + // kdDebug() << "Initializing plug in: Epos" << endl; + // Retrieve path to epos executable. + config->setGroup(configGroup); + m_eposServerExePath = config->readEntry("EposServerExePath", "epos"); + m_eposClientExePath = config->readEntry("EposClientExePath", "say"); + m_eposLanguage = config->readEntry("Language", QString::null); + m_time = config->readNumEntry("time", 100); + m_pitch = config->readNumEntry("pitch", 100); + m_eposServerOptions = config->readEntry("EposServerOptions", QString::null); + m_eposClientOptions = config->readEntry("EposClientOptions", QString::null); + kdDebug() << "EposProc::init: path to epos server: " << m_eposServerExePath << endl; + kdDebug() << "EposProc::init: path to epos client: " << m_eposClientExePath << endl; + + QString codecString = config->readEntry("Codec", "Local"); + m_codec = codecNameToCodec(codecString); + // Start the Epos server if not already started. + if (!m_eposServerProc) + { + m_eposServerProc = new KProcess; + *m_eposServerProc << m_eposServerExePath; + if (!m_eposServerOptions.isEmpty()) + *m_eposServerProc << m_eposServerOptions; + connect(m_eposServerProc, SIGNAL(receivedStdout(KProcess*, char*, int)), + this, SLOT(slotReceivedStdout(KProcess*, char*, int))); + connect(m_eposServerProc, SIGNAL(receivedStderr(KProcess*, char*, int)), + this, SLOT(slotReceivedStderr(KProcess*, char*, int))); + m_eposServerProc->start(KProcess::DontCare, KProcess::AllOutput); + } + + kdDebug() << "EposProc::init: Initialized with 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 EposProc::sayText(const QString &text) +{ + synth(text, QString::null, m_eposServerExePath, m_eposClientExePath, + m_eposServerOptions, m_eposClientOptions, + m_codec, m_eposLanguage, m_time, m_pitch); +} + +/** +* 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 EposProc::synthText(const QString& text, const QString& suggestedFilename) +{ + synth(text, suggestedFilename, m_eposServerExePath, m_eposClientExePath, + m_eposServerOptions, m_eposClientOptions, + m_codec, m_eposLanguage, m_time, m_pitch); +} + +/** +* Say or Synthesize text. +* @param text The text to be synthesized. +* @param suggestedFilename If not Null, synthesize only to this filename, otherwise +* synthesize and audibilize the text. +* @param eposServerExePath Path to the Epos server executable. +* @param eposClientExePath Path to the Epos client executable. +* @param eposServerOptions Options passed to Epos server executable. +* @param eposClientOptions Options passed to Epos client executable (don't include -o). +* @param codec Codec for encoding of text. +* @param eposLanguage Epos language setting. "czech", "slovak", +* or null (default language). +* @param time Speed percentage. 50 to 200. 200% = 2x normal. +* @param pitch Pitch persentage. 50 to 200. +*/ +void EposProc::synth( + const QString &text, + const QString &suggestedFilename, + const QString& eposServerExePath, + const QString& eposClientExePath, + const QString& eposServerOptions, + const QString& eposClientOptions, + QTextCodec *codec, + const QString& eposLanguage, + int time, + int pitch) +{ + // kdDebug() << "Running: EposProc::synth(const QString &text)" << endl; + + if (m_eposProc) + { + if (m_eposProc->isRunning()) m_eposProc->kill(); + delete m_eposProc; + m_eposProc = 0; + } + // Start the Epos server if not already started. + if (!m_eposServerProc) + { + m_eposServerProc = new KProcess; + *m_eposServerProc << eposServerExePath; + if (!eposServerOptions.isEmpty()) + *m_eposServerProc << eposServerOptions; + connect(m_eposServerProc, SIGNAL(receivedStdout(KProcess*, char*, int)), + this, SLOT(slotReceivedStdout(KProcess*, char*, int))); + connect(m_eposServerProc, SIGNAL(receivedStderr(KProcess*, char*, int)), + this, SLOT(slotReceivedStderr(KProcess*, char*, int))); + m_eposServerProc->start(KProcess::DontCare, KProcess::AllOutput); + kdDebug() << "EposProc:: Epos server process started" << endl; + } + +// // Encode the text. +// // 1.a) encode the text +// m_encText = QCString(); +// QTextStream ts (m_encText, IO_WriteOnly); +// ts.setCodec(codec); +// ts << text; +// ts << endl; // Some synths need this, eg. flite. + + if (codec) + m_encText = codec->fromUnicode(text); + else + m_encText = text.latin1(); // Should not happen, but just in case. + + // kdDebug()<< "EposProc::synth: Creating Epos object" << endl; + m_eposProc = new KProcess; + m_eposProc->setUseShell(true); + QString languageCode; + if (eposLanguage == "czech") + languageCode == "cz"; + else if (eposLanguage == "slovak") + languageCode == "sk"; + if (!languageCode.isEmpty()) + { + m_eposProc->setEnvironment("LANG", languageCode + "." + codec->mimeName()); + m_eposProc->setEnvironment("LC_CTYPE", languageCode + "." + codec->mimeName()); + } + *m_eposProc << eposClientExePath; + // Language. + if (!eposLanguage.isEmpty()) + *m_eposProc << QString("--language=%1").arg(eposLanguage); + // Rate (speed). + // Map 50% to 200% onto 0 to 1000. + // slider = alpha * (log(percent)-log(50)) + // with alpha = 1000/(log(200)-log(50)) + double alpha = 1000 / (log(200) - log(50)); + int slider = (int)floor (0.5 + alpha * (log(time)-log(50))); + // Center at 0. + slider = slider - 500; + // Map -500 to 500 onto 45 to -45 then shift to 130 to 40 (85 midpoint). + float stretchValue = (-float(slider) * 45.0 / 500.0) + 85.0; + QString timeMsg = QString("--init_t=%1").arg(stretchValue, 0, 'f', 3); + *m_eposProc << timeMsg; + // Pitch. Map 50% to 200% onto 50 to 200. easy. + QString pitchMsg = QString("--init_f=%1").arg(pitch); + *m_eposProc << pitchMsg; + // Output file. + if (!suggestedFilename.isEmpty()) + *m_eposProc << "-o"; + if (!eposClientOptions.isEmpty()) + *m_eposProc << eposClientOptions; + *m_eposProc << "-"; // Read from StdIn. + if (!suggestedFilename.isEmpty()) + *m_eposProc << " >" + suggestedFilename; + connect(m_eposProc, SIGNAL(processExited(KProcess*)), + this, SLOT(slotProcessExited(KProcess*))); + connect(m_eposProc, SIGNAL(receivedStdout(KProcess*, char*, int)), + this, SLOT(slotReceivedStdout(KProcess*, char*, int))); + connect(m_eposProc, SIGNAL(receivedStderr(KProcess*, char*, int)), + this, SLOT(slotReceivedStderr(KProcess*, char*, int))); + connect(m_eposProc, SIGNAL(wroteStdin(KProcess*)), + this, SLOT(slotWroteStdin(KProcess* ))); + if (suggestedFilename.isEmpty()) + m_state = psSaying; + else + m_state = psSynthing; + + // Ok, let's rock. + m_synthFilename = suggestedFilename; + // kdDebug() << "EposProc::synth: Synthing text: '" << text << "' using Epos plug in" << endl; + if (!m_eposProc->start(KProcess::NotifyOnExit, KProcess::All)) + { + kdDebug() << "EposProc::synth: Error starting Epos process. Is epos in the PATH?" << endl; + m_state = psIdle; + return; + } + // kdDebug() << "EposProc:synth: Epos initialized" << endl; + // kdDebug() << "EposProc::synth: m_encText.length() = " << m_encText.length() << " text.length() = " + // << text.length() << endl; + if (!m_eposProc->writeStdin(m_encText, m_encText.length())) + kdDebug() << "EposProc::synth: Error writing to Epos client StdIn." << endl; +} + +/** +* 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 EposProc::getFilename() +{ + kdDebug() << "EposProc::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 EposProc::stopText(){ + kdDebug() << "EposProc::stopText:: Running" << endl; + if (m_eposProc) + { + if (m_eposProc->isRunning()) + { + kdDebug() << "EposProc::stopText: killing Epos." << endl; + m_waitingStop = true; + m_eposProc->kill(); + } else m_state = psIdle; + } else m_state = psIdle; + kdDebug() << "EposProc::stopText: Epos stopped." << endl; +} + +void EposProc::slotProcessExited(KProcess*) +{ + // kdDebug() << "EposProc:slotProcessExited: Epos 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 EposProc::slotReceivedStdout(KProcess*, char* buffer, int buflen) +{ + QString buf = QString::fromLatin1(buffer, buflen); + kdDebug() << "EposProc::slotReceivedStdout: Received output from Epos: " << buf << endl; +} + +void EposProc::slotReceivedStderr(KProcess*, char* buffer, int buflen) +{ + QString buf = QString::fromLatin1(buffer, buflen); + kdDebug() << "EposProc::slotReceivedStderr: Received error from Epos: " << buf << endl; +} + +void EposProc::slotWroteStdin(KProcess*) +{ + // kdDebug() << "EposProc::slotWroteStdin: closing Stdin" << endl; + m_eposProc->closeStdin(); + m_encText = QCString(); +} + +/** +* Return the current state of the plugin. +* This function only makes sense in asynchronous mode. +* @return The pluginState of the plugin. +* +* @see pluginState +*/ +pluginState EposProc::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 EposProc::ackFinished() +{ + if (m_state == psFinished) + { + m_state = psIdle; + m_synthFilename = 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 EposProc::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 EposProc::supportsSynth() { return true; } diff --git a/kttsd/plugins/epos/eposproc.h b/kttsd/plugins/epos/eposproc.h new file mode 100644 index 0000000..b2d1d90 --- /dev/null +++ b/kttsd/plugins/epos/eposproc.h @@ -0,0 +1,241 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + eposproc.h + Main speaking functions for the Epos Plug in + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#ifndef _EPOSPROC_H_ +#define _EPOSPROC_H_ + +// Qt includes. +#include <qstringlist.h> +#include <qmutex.h> + +// KTTS includes. +#include <pluginproc.h> + +class KProcess; +class QTextCodec; + +class EposProc : public PlugInProc{ + Q_OBJECT + + public: + /** + * Constructor + */ + EposProc( QObject* parent = 0, const char* name = 0, const QStringList &args = QStringList()); + + /** + * Destructor + */ + virtual ~EposProc(); + + /** + * Initializate the speech engine. + * @param config Settings object. + * @param configGroup Settings group. + */ + virtual bool init(KConfig *config, const QString &configGroup); + + /** + * Say a text string. + * @param text The text to speak. + */ + virtual void sayText(const QString &text); + + /** + * 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. + */ + virtual void synthText(const QString& text, const QString& suggestedFilename); + + /** + * 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. + */ + virtual QString getFilename(); + + /** + * 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. + */ + virtual void stopText(); + + /** + * Return the current state of the plugin. + * This function only makes sense in asynchronous mode. + * @return The pluginState of the plugin. + * + * @see pluginState + */ + virtual pluginState getState(); + + /** + * 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. + */ + virtual void ackFinished(); + + /** + * 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. + */ + virtual bool supportsAsync(); + + /** + * 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. + */ + virtual bool supportsSynth(); + + /** + * Say or Synthesize text. + * @param text The text to be synthesized. + * @param suggestedFilename If not Null, synthesize only to this filename, otherwise + * synthesize and audibilize the text. + * @param eposServerExePath Path to the Epos server executable. + * @param eposClientExePath Path to the Epos client executable. + * @param eposServerOptions Options passed to Epos server executable. + * @param eposClientOptions Options passed to Epos client executable (don't include -o). + * @param codec Codec for encoding of text. + * @param eposLanguage Epos language setting. "czech", "slovak", + * or null (default language). + * @param time Speed percentage. 50 to 200. 200% = 2x normal. + * @param pitch Pitch persentage. 50 to 200. + */ + void synth( + const QString &text, + const QString &suggestedFilename, + const QString& eposServerExePath, + const QString& eposClientExePath, + const QString& eposServerOptions, + const QString& eposClientOptions, + QTextCodec *codec, + const QString& eposLanguage, + int time, + int pitch); + + private slots: + void slotProcessExited(KProcess* proc); + void slotReceivedStdout(KProcess* proc, char* buffer, int buflen); + void slotReceivedStderr(KProcess* proc, char* buffer, int buflen); + void slotWroteStdin(KProcess* proc); + + private: + + /** + * Path to epos executables (from config). + */ + QString m_eposServerExePath; + QString m_eposClientExePath; + + /** + * User options passed to executables (from config). + */ + QString m_eposServerOptions; + QString m_eposClientOptions; + + /** + * Epos Server process. + */ + KProcess* m_eposServerProc; + + /** + * Epos Client process + */ + KProcess* m_eposProc; + + /** + * Epos language setting. "czech", "slovak", or Null (use default language). + */ + QString m_eposLanguage; + + /** + * Rate (speed) from config file. + */ + int m_time; + + /** + * Pitch from the config file. + */ + int m_pitch; + + /** + * Codec. + */ + QTextCodec* m_codec; + + /** + * Encoded buffer to be sent to Epos client. + */ + QCString m_encText; + + /** + * Synthesis filename. + */ + QString m_synthFilename; + + /** + * Plugin state. + */ + pluginState m_state; + + /** + * True when stopText has been called. Used to force transition to psIdle when + * Epos exits. + */ + bool m_waitingStop; + +}; + +#endif // _EPOSPROC_H_ diff --git a/kttsd/plugins/epos/kttsd_eposplugin.desktop b/kttsd/plugins/epos/kttsd_eposplugin.desktop new file mode 100644 index 0000000..7673929 --- /dev/null +++ b/kttsd/plugins/epos/kttsd_eposplugin.desktop @@ -0,0 +1,87 @@ +[Desktop Entry] +Name=Epos TTS Synthesis System +Name[ca]=Sistema de síntesi Epos TTS +Name[cs]=Epos TTS +Name[da]=Epos TTS Synthesis-system +Name[de]=Epos TTS-Synthese-System +Name[el]=Σύστημα σύνθεσης Epos TTS +Name[es]=Sistema de síntesis Epos TTS +Name[et]=Kõnesünteesisüsteem Epos TTS +Name[eu]=Epos TTS sintesi-sistema +Name[fa]=سیستم ترکیبدهی Epos TTS +Name[fi]=Epos TTS -syntetisoijasysteemi +Name[fr]=Système de synthèse Epos TTS +Name[ga]=Córas Sintéise TTS Epos +Name[gl]=Sistema de Síntese de TTS Epos +Name[hu]=Epos szövegfelolvasó motor +Name[it]=Sistema di sintesi TTS Epos +Name[ja]=Epos TTS シンセサイズシステム +Name[ka]=Epos TTS სინთეზის სისტემა +Name[km]=ប្រព័ន្ធសង្គ្រោះ Epos TTS +Name[mk]=Epos TTS систем за синтеза +Name[ms]=Sistem Sintesis Epos TTS +Name[nb]=Epos TTT syntesesystem +Name[nds]=Epos Blicksnuut +Name[ne]=Epos TTS सिन्थेसिस प्रणाली +Name[nl]=Epos TTS Synthesis-systeem +Name[pa]=Epos TTS ਸੰਸਲੇਸ਼ਣ ਸਿਸਟਮ +Name[pl]=System syntezy mowy Epos +Name[pt]=Sistema de Síntese Epos TTS +Name[pt_BR]=Sistema de Sintetizador de Fala Epos +Name[sk]=Systém Epos TTS Synthesis +Name[sl]=Sistem sinteze besedila v govor Epos +Name[sr]=Систем за синтезу Epos TTS +Name[sr@Latn]=Sistem za sintezu Epos TTS +Name[sv]=Epos TTS syntessystem +Name[ta]=Epos TTS கூட்டிணைப்பு அமைப்பு +Name[tg]=Системаи Синтезиси Epos TTS +Name[tr]=Epos TTS Sentezleme Sistemi +Name[vi]=Hệ thống Tổng hợp Văn bản sang Tiếng nói Epos +Name[zh_TW]=Epos TTS 合成系統 +Comment=Epos TTS speech synthesizer +Comment[bg]=Синтезатор на глас Epos TTS +Comment[ca]=Sintetitzador de veu Epos TTS +Comment[cs]=Hlasový syntetizér Epos TTS +Comment[da]=Epos TTS tale-synthesizer +Comment[de]=Epos TTS-Sprachsynthesizer +Comment[el]=Συνθέτης ομιλίας Epos TTS +Comment[es]=Sintetizador de texto a voz Epos TTS +Comment[et]=Kõnesüntesaator Epos TTS +Comment[eu]=Epos TTS hizketa-sintetizadorea +Comment[fa]=ترکیبدهندۀ گفتار Epos TTS +Comment[fi]=Epos TTS -puhesyntetisaattori +Comment[fr]=Synthèse vocale Epos TTS +Comment[ga]=Sintéiseoir cainte TTS Epos +Comment[gl]=Sintetizador de fala TTS Epos +Comment[hu]=Epos TTS beszédszintetizátor +Comment[is]=Epos TTS talgerfill +Comment[it]=Sintetizzatore vocale TTS Epos +Comment[ja]=Epos TTS スピーチシンセサイザ +Comment[ka]=Epos TTS სიტყვის სინთეზატორი +Comment[km]=កម្មវិធីសង្គ្រោះការនិយាយ Epos TTS +Comment[mk]=Epos TTS синтетизатор на говор +Comment[ms]=Pensintesis tutur Epos TTS +Comment[nb]=Epos TTT talesyntetisering +Comment[nds]=Blicksnuut vun Epos +Comment[ne]=Epos TTS संवाद सिन्थेसाइजर +Comment[nl]=Epos TTS spraaksynthesizer +Comment[pa]=Epos TTS ਬੋਲੀ ਸੰਸਲੇਸ਼ਣ +Comment[pl]=Syntezator mowy Epos +Comment[pt]=O sintetizador de fala Epos TTS +Comment[pt_BR]=Sistema de Sintetizador de Fala Epos +Comment[ru]=Синтезатор речи Epos TTS +Comment[sk]=Syntetizátor reči Epos TTS +Comment[sl]=Sintetizator besedila v govor Epos +Comment[sr]=Синтетизатор говора Epos TTS +Comment[sr@Latn]=Sintetizator govora Epos TTS +Comment[sv]=Epos TTS talsyntes +Comment[ta]=Epos TTS பேச்சு கூட்டிணைப்பான் +Comment[tg]=Таҳлилгари овози Epos TTS +Comment[tr]=Epos TTS konuşma sentezleyicisi +Comment[uk]=Синтезатор мовлення Epos TTS +Comment[vi]=Trình tổng hợp Văn bản sang Tiếng nói Epos +Comment[zh_TW]=Epos TTS 語音合成器 +Type=Service +ServiceTypes=KTTSD/SynthPlugin +X-KDE-Library=libkttsd_eposplugin +X-KDE-Languages=cs,sk diff --git a/kttsd/plugins/festivalint/Makefile.am b/kttsd/plugins/festivalint/Makefile.am new file mode 100644 index 0000000..7c6ca0e --- /dev/null +++ b/kttsd/plugins/festivalint/Makefile.am @@ -0,0 +1,27 @@ +INCLUDES = \ + -I$(top_srcdir)/kttsd/libkttsd -I$(top_builddir)/kttsd/libkttsd \ + $(all_includes) + +METASOURCES = AUTO + +kde_module_LTLIBRARIES = libkttsd_festivalintplugin.la + +libkttsd_festivalintplugin_la_SOURCES = \ + festivalintconfwidget.ui \ + festivalintconf.cpp \ + festivalintproc.cpp \ + festivalintplugin.cpp +libkttsd_festivalintplugin_la_LDFLAGS = $(KDE_PLUGIN) $(all_libraries) +libkttsd_festivalintplugin_la_LIBADD = $(top_builddir)/kttsd/libkttsd/libkttsd.la + +services_DATA = kttsd_festivalintplugin.desktop +servicesdir = $(kde_servicesdir) + +# Install data files. +festivalintdatadir = $(kde_datadir)/kttsd/festivalint/ +festivalintdata_DATA = voices sabletowave.scm + +festivalintxsltdatadir = $(kde_datadir)/kttsd/festivalint/xslt/ +festivalintxsltdata_DATA = SSMLtoSable.xsl + +noinst_HEADERS = festivalintconfwidget.h diff --git a/kttsd/plugins/festivalint/README b/kttsd/plugins/festivalint/README new file mode 100644 index 0000000..3903dd0 --- /dev/null +++ b/kttsd/plugins/festivalint/README @@ -0,0 +1,9 @@ +This is the directory containing the Festival (Interactive) plug in. +This plugin is developed and maintained by Gary Cramblitt. +<garycramblitt@comcast.net> + +Make sure that Festival has write access to the audio device + + chmod a+rw /dev/dsp* + + diff --git a/kttsd/plugins/festivalint/SSMLtoSable.xsl b/kttsd/plugins/festivalint/SSMLtoSable.xsl new file mode 100644 index 0000000..48c2fd0 --- /dev/null +++ b/kttsd/plugins/festivalint/SSMLtoSable.xsl @@ -0,0 +1,272 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- *********************************************************************** + SSMLtoSable.xsl + Stylesheet for transforming SSML into SABLE markup. + ============ + Copyright : (C) 2004 by Paul Giannaros + ============ + Original author: Paul Giannaros <ceruleanblaze@gmail.com> + *************************************************************************** + + *************************************************************************** + * * + * 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. * + * * + *************************************************************************** --> +<!-- @todo create a doc detailing which parts of SSML this sheet can handle --> + +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> +<xsl:output method="html" indent="no"/> + +<!-- speak: Indicates SSML markup. --> +<xsl:template match="/speak"> + <SABLE><xsl:apply-templates/></SABLE> +</xsl:template> + +<!-- sub: The word that the text sounds like as abbreviations + can be pronounced differently. For example, + <sub alias="doctor">Dr.</sub> smith lives at 32 johnson <sub alias="drive">dr.</sub> --> +<xsl:template match="//sub"> + <xsl:choose> + <xsl:when test="@alias"> + <xsl:value-of select="@alias"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="."/> + </xsl:otherwise> + </xsl:choose> +</xsl:template> + +<!-- p: Indicate a paragraph of text --> +<xsl:template match="//p"> + <DIV TYPE="paragraph"><xsl:apply-templates/></DIV> +</xsl:template> +<!-- s: Forceefully indicate a sentence (Does not need to be used + if full stops are present) --> +<xsl:template match="//s"> + <DIV TYPE="sentence"><xsl:apply-templates/></DIV> +</xsl:template> + +<!-- emphasis: Emphasize a word or group of words. --> +<xsl:template match="//emphasis"> + <!-- SSML and SABLE both take the same values for their attributes - + strong, moderate, none, reduced --> + <EM TYPE="{@level}"><xsl:apply-templates/></EM> +</xsl:template> + +<xsl:template match="//voice"> + <!-- This is turned off because if Festival lacks an installed voice to match a gender + or voice name, it bombs out. argh! + <xsl:call-template name="voice"><xsl:with-param name="a" select="@*" /></xsl:call-template> --> + <xsl:apply-templates/> +</xsl:template> + +<xsl:template name="voice"> + <xsl:param name="a" /> + <!-- Get the name of the tag we're creating and convert to a SABLE tag. --> + <xsl:variable name="tag"> + <xsl:choose> + <xsl:when test="name($a[1])='gender'">SPEAKER</xsl:when> + <xsl:when test="name($a[1])='age'">SPEAKER</xsl:when> + <xsl:when test="name($a[1])='name'">SPEAKER</xsl:when> + <xsl:otherwise><xsl:value-of select="$a[1]"/></xsl:otherwise> + </xsl:choose> + </xsl:variable> + + <xsl:element name="{$tag}"> + <!-- Create the right attribute to go with element $tag. --> + + <xsl:choose> + <!-- gender: + The gender of the voice. + Values such as male, female, and neutral are supported. --> + <xsl:when test="name($a[1])='gender' and $a[1]='male'"> + <xsl:attribute name="GENDER">male1</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='gender' and $a[1]='female'"> + <xsl:attribute name="GENDER">female1</xsl:attribute> + </xsl:when> + <!-- If none of the above match, take the users selected value. --> + <xsl:when test="name($a[1])='gender'"> + <xsl:attribute name="GENDER"><xsl:value-of select=" $a[1]"/></xsl:attribute> + </xsl:when> + + <!-- age: + The age of the voice. + Positive integer values are supported. --> + <xsl:when test="name($a[1])='age' and number($a[1]) < 10"> + <xsl:attribute name="AGE">child</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='age' and number($a[1]) < 20"> + <xsl:attribute name="AGE">teen</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='age' and number($a[1]) < 30"> + <xsl:attribute name="AGE">younger</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='age' and number($a[1]) < 50"> + <xsl:attribute name="AGE">middle</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='age'"> + <xsl:attribute name="AGE">older</xsl:attribute> + </xsl:when> + + <!-- name: + Voice name. Synth dependent. --> + <xsl:when test="name($a[1])='name'"> + <xsl:attribute name="NAME"><xsl:value-of select=" $a[1]"/></xsl:attribute> + </xsl:when> + </xsl:choose> + + <!-- Recursively call ourself. --> + <xsl:choose> + <xsl:when test="$a[2]"> + <xsl:call-template name="voice"><xsl:with-param name="a" select="$a[position()>1]" /></xsl:call-template> + </xsl:when> + <xsl:otherwise> + <xsl:apply-templates/> + </xsl:otherwise> + </xsl:choose> + + </xsl:element> +</xsl:template> + +<xsl:template match="prosody"> + <!-- contour and duration not supported. --> + <xsl:call-template name="prosody"><xsl:with-param name="a" + select="@pitch|@rate|@volume|@range" /> + </xsl:call-template> +</xsl:template> + +<xsl:template name="prosody"> + <xsl:param name="a" /> + <!-- Get the name of the tag we're creating and convert to a SABLE tag. --> + <xsl:variable name="tag"> + <xsl:choose> + <xsl:when test="name($a[1])='pitch'">PITCH</xsl:when> + <xsl:when test="name($a[1])='rate'">RATE</xsl:when> + <xsl:when test="name($a[1])='volume'">VOLUME</xsl:when> + <xsl:when test="name($a[1])='range'">PITCH</xsl:when> + <xsl:otherwise><xsl:value-of select="$a[1]"/></xsl:otherwise> + </xsl:choose> + </xsl:variable> + + <xsl:element name="{$tag}"> + <!-- Create the right attribute to go with element $tag. --> + + <xsl:choose> + <!-- pitch: + The pitch with which the text is spoken. + Values such as x-high, high, low, etc. and percentages (+ or -) + are supported. --> + <xsl:when test="name($a[1])='pitch' and $a[1]='x-high'"> + <xsl:attribute name="BASE">70%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='pitch' and $a[1]='high'"> + <xsl:attribute name="BASE">40%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='pitch' and $a[1]='medium'"> + <xsl:attribute name="BASE">0%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='pitch' and $a[1]='low'"> + <xsl:attribute name="BASE">-40%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='pitch' and $a[1]='x-low'"> + <xsl:attribute name="BASE">-70%</xsl:attribute> + </xsl:when> + <!-- If none of the above match, take the users selected value. --> + <xsl:when test="name($a[1])='pitch'"> + <xsl:attribute name="BASE"><xsl:value-of select=" $a[1]"/></xsl:attribute> + </xsl:when> + + <!-- rate: + The speed at which the text is spoken. + Values such as x-fast, fast, slow, etc. and percentages (+ or -) + are supported. --> + <xsl:when test="name($a[1])='rate' and $a[1]='x-fast'"> + <xsl:attribute name="SPEED">70%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='rate' and $a[1]='fast'"> + <xsl:attribute name="SPEED">40%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='rate' and $a[1]='medium'"> + <xsl:attribute name="SPEED">0%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='rate' and $a[1]='slow'"> + <xsl:attribute name="SPEED">-40%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='rate' and $a[1]='x-slow'"> + <xsl:attribute name="SPEED">-70%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='rate'"> + <xsl:attribute name="SPEED"><xsl:value-of select=" $a[1]"/></xsl:attribute> + </xsl:when> + + <!-- volume: + The volume at which the text is spoken. + Values such as x-loud, loud, quiet, etc. and percentages (+ or -) + are supported. --> + <xsl:when test="name($a[1])='volume' and $a[1]='x-loud'"> + <xsl:attribute name="LEVEL">70%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='volume' and $a[1]='loud'"> + <xsl:attribute name="LEVEL">50%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='volume' and $a[1]='medium'"> + <xsl:attribute name="LEVEL">0%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='volume' and $a[1]='soft'"> + <xsl:attribute name="LEVEL">-50%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='volume' and $a[1]='x-soft'"> + <xsl:attribute name="LEVEL">-70%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='volume' and $a[1]='silent'"> + <xsl:attribute name="LEVEL">-100%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='volume'"> + <xsl:attribute name="LEVEL"><xsl:value-of select=" $a[1]"/></xsl:attribute> + </xsl:when> + + <!-- range: + The volume at which the text is spoken. + Values such as x-high, high, medium, low, x-low, etc. and percentages (+ or -) + are supported. --> + <xsl:when test="name($a[1])='range' and $a[1]='x-high'"> + <xsl:attribute name="RANGE">70%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='range' and $a[1]='high'"> + <xsl:attribute name="RANGE">40%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='range' and $a[1]='medium'"> + <xsl:attribute name="RANGE">0%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='range' and $a[1]='low'"> + <xsl:attribute name="RANGE">-40%</xsl:attribute> + </xsl:when> + <xsl:when test="name($a[1])='range' and $a[1]='x-low'"> + <xsl:attribute name="RANGE">-70%</xsl:attribute> + </xsl:when> + <!-- If none of the above match, take the users selected value. --> + <xsl:when test="name($a[1])='range'"> + <xsl:attribute name="RANGE"><xsl:value-of select=" $a[1]"/></xsl:attribute> + </xsl:when> + + </xsl:choose> + + <!-- Recursively call ourself. --> + <xsl:choose> + <xsl:when test="$a[2]"> + <xsl:call-template name="prosody"><xsl:with-param name="a" select="$a[position()>1]" /></xsl:call-template> + </xsl:when> + <xsl:otherwise> + <xsl:apply-templates/> + </xsl:otherwise> + </xsl:choose> + </xsl:element> +</xsl:template> + +</xsl:stylesheet> + diff --git a/kttsd/plugins/festivalint/configure.in.bot b/kttsd/plugins/festivalint/configure.in.bot new file mode 100644 index 0000000..fee52df --- /dev/null +++ b/kttsd/plugins/festivalint/configure.in.bot @@ -0,0 +1,16 @@ +if test "x$festival_bindir" = "xno"; then + if test "$compile_festivalint_plugin" = "yes"; then + echo "" + echo "======================================================" + echo "The festival program does not appear to be installed" + echo "on this system. The Festival Interactive plugin will" + echo "be built, but you need to install festival before you" + echo "can use it. Festival comes on most Linux distribution" + echo "CDs, or you can get it at" + echo " http://www.cstr.ed.ac.uk/projects/festival/" + echo "Debian users: apt-get install festival" + echo "=====================================================" + all_tests=bad + fi +fi + diff --git a/kttsd/plugins/festivalint/configure.in.in b/kttsd/plugins/festivalint/configure.in.in new file mode 100644 index 0000000..c22f422 --- /dev/null +++ b/kttsd/plugins/festivalint/configure.in.in @@ -0,0 +1,22 @@ +dnl ============================== +dnl checks for FestivalInt Plug In +dnl ============================== + +AC_ARG_ENABLE(kttsd-festivalint, + AC_HELP_STRING([--enable-kttsd-festivalint], + [build KTTSD Festival Interactive plugin [default=yes]]), + festivalint_plugin=$enableval, + festivalint_plugin=yes) + +compile_festivalint_plugin="yes" + +if test "x$festivalint_plugin" = "xno"; then + compile_festivalint_plugin="no" +fi + +dnl Check for festival executable. +dnl Note that Festival Interactive plugin is always built +dnl whether binary is found or not, unless user overrides with -disable-festivalint. +AC_PATH_PROG(festival_bindir, "festival", "no") + +AM_CONDITIONAL(include_kttsd_festivalint, test "x$compile_festivalint_plugin" = "xyes") diff --git a/kttsd/plugins/festivalint/festivalintconf.cpp b/kttsd/plugins/festivalint/festivalintconf.cpp new file mode 100644 index 0000000..91cd24f --- /dev/null +++ b/kttsd/plugins/festivalint/festivalintconf.cpp @@ -0,0 +1,730 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Configuration widget and functions for Festival (Interactive) plug in + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +// C++ includes. +#include <math.h> + +// Qt includes. +#include <qlayout.h> +#include <qlabel.h> +#include <qstring.h> +#include <qstringlist.h> +#include <qcheckbox.h> +#include <qdir.h> +#include <qslider.h> +#include <qdom.h> +#include <qtextcodec.h> + +// KDE includes. +#include <kdialog.h> +#include <kdebug.h> +#include <klocale.h> +#include <kcombobox.h> +#include <kglobal.h> +#include <ktempfile.h> +#include <kstandarddirs.h> +#include <knuminput.h> +#include <kprocio.h> +#include <kprogress.h> +#include <kiconloader.h> + +// KTTS includes. +#include "testplayer.h" + +// FestivalInt includes. +#include "festivalintproc.h" +#include "festivalintconf.h" +#include "festivalintconf.moc" + +/** Constructor */ +FestivalIntConf::FestivalIntConf( QWidget* parent, const char* name, const QStringList& /*args*/) : + PlugInConf(parent, name) +{ + // kdDebug() << "FestivalIntConf::FestivalIntConf: Running" << endl; + m_festProc = 0; + m_progressDlg = 0; + m_supportsSSML = FestivalIntProc::ssUnknown; + + QVBoxLayout *layout = new QVBoxLayout(this, KDialog::marginHint(), + KDialog::spacingHint(), "FestivalIntConfigWidgetLayout"); + layout->setAlignment (Qt::AlignTop); + m_widget = new FestivalIntConfWidget(this, "FestivalIntConfigWidget"); + layout->addWidget(m_widget); + + m_widget->festivalPath->setMode(KFile::File | KFile::ExistingOnly); + m_widget->festivalPath->setFilter("*"); + + // Build codec list and fill combobox. + m_codecList = PlugInProc::buildCodecList(); + m_widget->characterCodingBox->clear(); + m_widget->characterCodingBox->insertStringList(m_codecList); + + // defaults(); + + connect(m_widget->festivalPath, SIGNAL(textChanged(const QString&)), + this, SLOT(slotFestivalPath_textChanged())); + connect(m_widget->selectVoiceCombo, SIGNAL(activated(const QString&)), + this, SLOT(slotSelectVoiceCombo_activated())); + connect(m_widget->selectVoiceCombo, SIGNAL(activated(const QString&)), + this, SLOT(configChanged())); + connect(m_widget->testButton, SIGNAL(clicked()), this, SLOT(slotTest_clicked())); + connect(m_widget->rescan, SIGNAL(clicked()), this, SLOT(scanVoices())); + connect(m_widget->volumeBox, SIGNAL(valueChanged(int)), + this, SLOT(volumeBox_valueChanged(int))); + connect(m_widget->timeBox, SIGNAL(valueChanged(int)), + this, SLOT(timeBox_valueChanged(int))); + connect(m_widget->frequencyBox, SIGNAL(valueChanged(int)), + this, SLOT(frequencyBox_valueChanged(int))); + connect(m_widget->volumeSlider, SIGNAL(valueChanged(int)), + this, SLOT(volumeSlider_valueChanged(int))); + connect(m_widget->timeSlider, SIGNAL(valueChanged(int)), + this, SLOT(timeSlider_valueChanged(int))); + connect(m_widget->frequencySlider, SIGNAL(valueChanged(int)), + this, SLOT(frequencySlider_valueChanged(int))); + connect(m_widget->volumeBox, SIGNAL(valueChanged(int)), this, SLOT(configChanged())); + connect(m_widget->volumeSlider, SIGNAL(valueChanged(int)), this, SLOT(configChanged())); + connect(m_widget->timeBox, SIGNAL(valueChanged(int)), this, SLOT(configChanged())); + connect(m_widget->timeSlider, SIGNAL(valueChanged(int)), this, SLOT(configChanged())); + connect(m_widget->frequencyBox, SIGNAL(valueChanged(int)), this, SLOT(configChanged())); + connect(m_widget->frequencySlider, SIGNAL(valueChanged(int)), this, SLOT(configChanged())); + connect(m_widget->preloadCheckBox, SIGNAL(clicked()), this, SLOT(configChanged())); + connect(m_widget->characterCodingBox, SIGNAL(textChanged(const QString&)), + this, SLOT(configChanged())); + connect(m_widget->characterCodingBox, SIGNAL(activated(const QString&)), + this, SLOT(configChanged())); +} + +/** Destructor */ +FestivalIntConf::~FestivalIntConf(){ + // kdDebug() << "FestivalIntConf::~FestivalIntConf: Running" << endl; + if (!m_waveFile.isNull()) QFile::remove(m_waveFile); + delete m_festProc; + delete m_progressDlg; +} + +/** +* Given a voice code, returns index into m_voiceList array (and voiceCombo box). +* -1 if not found. +*/ +int FestivalIntConf::voiceCodeToListIndex(const QString& voiceCode) const +{ + const int voiceListCount = m_voiceList.count(); + for(int index = 0; index < voiceListCount; ++index){ + // kdDebug() << "Testing: " << voiceCode << " == " << m_voiceList[index].code << endl; + if(voiceCode == m_voiceList[index].code) + return index; + } + return -1; +} + +void FestivalIntConf::load(KConfig *config, const QString &configGroup){ + //kdDebug() << "FestivalIntConf::load: Running" << endl; + config->setGroup("FestivalInt"); + QString exePath = config->readEntry("FestivalExecutablePath", "festival"); + QString exeLocation = getLocation(exePath); + if (!exeLocation.isEmpty()) exePath = exeLocation; + exePath = realFilePath(exePath); + config->setGroup(configGroup); + m_widget->festivalPath->setURL(config->readEntry("FestivalExecutablePath", exePath)); + m_widget->preloadCheckBox->setChecked(false); + scanVoices(); + QString voiceSelected(config->readEntry("Voice")); + int index = voiceCodeToListIndex(voiceSelected); + if (index >= 0) + { + m_widget->selectVoiceCombo->setCurrentItem(index); + m_widget->preloadCheckBox->setChecked(m_voiceList[index].preload); + } + m_widget->volumeBox->setValue(config->readNumEntry("volume", 100)); + m_widget->timeBox->setValue(config->readNumEntry("time", 100)); + m_widget->frequencyBox->setValue(config->readNumEntry("pitch", 100)); + m_widget->preloadCheckBox->setChecked(config->readBoolEntry( + "Preload", m_widget->preloadCheckBox->isChecked())); + m_languageCode = config->readEntry("LanguageCode", m_languageCode); + m_supportsSSML = static_cast<FestivalIntProc::SupportsSSML>( + config->readNumEntry("SupportsSSML", FestivalIntProc::ssUnknown)); + QString codecName = PlugInProc::codecIndexToCodecName( + m_widget->characterCodingBox->currentItem(), m_codecList); + codecName = config->readEntry("Codec", codecName); + int codecNdx = PlugInProc::codecNameToListIndex(codecName, m_codecList); + m_widget->characterCodingBox->setCurrentItem(codecNdx); +} + +void FestivalIntConf::save(KConfig *config, const QString &configGroup){ + // kdDebug() << "FestivalIntConf::save: Running" << endl; + config->setGroup("FestivalInt"); + config->writeEntry("FestivalExecutablePath", realFilePath(m_widget->festivalPath->url())); + config->setGroup(configGroup); + config->writeEntry("FestivalExecutablePath", realFilePath(m_widget->festivalPath->url())); + config->writeEntry("Voice", m_voiceList[m_widget->selectVoiceCombo->currentItem()].code); + config->writeEntry("volume", m_widget->volumeBox->value()); + config->writeEntry("time", m_widget->timeBox->value()); + config->writeEntry("pitch", m_widget->frequencyBox->value()); + config->writeEntry("Preload", m_widget->preloadCheckBox->isChecked()); + config->writeEntry("LanguageCode", m_voiceList[m_widget->selectVoiceCombo->currentItem()].languageCode); + config->writeEntry("SupportsSSML", m_supportsSSML); + int codec = m_widget->characterCodingBox->currentItem(); + config->writeEntry("Codec", PlugInProc::codecIndexToCodecName(codec, m_codecList)); +} + +void FestivalIntConf::defaults(){ + // kdDebug() << "FestivalIntConf::defaults: Running" << endl; + m_widget->festivalPath->setURL("festival"); + m_widget->timeBox->setValue(100); + timeBox_valueChanged(100); + m_widget->volumeBox->setValue(100); + volumeBox_valueChanged(100); + m_widget->frequencyBox->setValue(100); + frequencyBox_valueChanged(100); + m_widget->preloadCheckBox->setChecked(false); + m_widget->characterCodingBox->setCurrentItem( + PlugInProc::codecNameToListIndex("ISO 8859-1", m_codecList)); + scanVoices(); +} + +void FestivalIntConf::setDesiredLanguage(const QString &lang) +{ + // kdDebug() << "FestivalIntConf::setDesiredLanguage: Running" << endl; + m_languageCode = splitLanguageCode(lang, m_countryCode); +} + +QString FestivalIntConf::getTalkerCode() +{ + if (!m_widget->selectVoiceCombo->isEnabled()) return QString::null; + QString exePath = realFilePath(m_widget->festivalPath->url()); + if (exePath.isEmpty()) return QString::null; + if (getLocation(exePath).isEmpty()) return QString::null; + if (m_voiceList.count() == 0) return QString::null; + QString normalTalkerCode; + voiceStruct voiceTemp = m_voiceList[m_widget->selectVoiceCombo->currentItem()]; + // Determine volume attribute. soft < 75% <= medium <= 125% < loud. + QString volume = "medium"; + if (m_widget->volumeBox->value() < 75) volume = "soft"; + if (m_widget->volumeBox->value() > 125) volume = "loud"; + // Determine rate attribute. slow < 75% <= medium <= 125% < fast. + QString rate = "medium"; + if (m_widget->timeBox->value() < 75) rate = "slow"; + if (m_widget->timeBox->value() > 125) rate = "fast"; + normalTalkerCode = QString( + "<voice lang=\"%1\" name=\"%2\" gender=\"%3\" />" + "<prosody volume=\"%4\" rate=\"%5\" />" + "<kttsd synthesizer=\"%6\" />") + .arg(voiceTemp.languageCode) + .arg(voiceTemp.code) + .arg(voiceTemp.gender) + .arg(volume) + .arg(rate) + .arg("Festival Interactive"); + return normalTalkerCode; +} + +/** + * Chooses a default voice given scanned list of voices in m_voiceList and current + * language and country code, and updates controls. + * @param currentVoiceIndex This voice is preferred if it matches. + */ +void FestivalIntConf::setDefaultVoice(int currentVoiceIndex) +{ + // kdDebug() << "FestivalIntCont::setDefaultVoice: Running" << endl; + // If language code is known, auto pick first voice that matches the language code. + if (!m_languageCode.isEmpty()) + { + bool found = false; + // First search for a match on both language code and country code. + QString languageCode = m_languageCode; + if (!m_countryCode.isNull()) languageCode += "_" + m_countryCode; + // kdDebug() << "FestivalIntConf::setDefaultVoice:: looking for default voice to match language code " << languageCode << endl; + uint index = 0; + // Prefer existing voice if it matches. + if (currentVoiceIndex >= 0) + { + QString vlCode = m_voiceList[currentVoiceIndex].languageCode.left(languageCode.length()); + if (languageCode == vlCode) + { + found = true; + index = currentVoiceIndex; + } + } + if (!found) + { + for(index = 0 ; index < m_voiceList.count(); ++index) + { + QString vlCode = m_voiceList[index].languageCode.left(languageCode.length()); + // kdDebug() << "FestivalIntConf::setDefaultVoice: testing " << vlCode << endl; + if(languageCode == vlCode) + { + found = true; + break; + } + } + } + // If not found, search for a match on just the language code. + if (!found) + { + languageCode = m_languageCode; + // Prefer existing voice if it matches. + if (currentVoiceIndex >= 0) + { + QString vlCode = m_voiceList[currentVoiceIndex].languageCode.left(languageCode.length()); + if (languageCode == vlCode) + { + found = true; + index = currentVoiceIndex; + } + } + if (!found) + { + for(index = 0 ; index < m_voiceList.count(); ++index) + { + QString vlCode = m_voiceList[index].languageCode.left(languageCode.length()); + // kdDebug() << "FestivalIntConf::setDefaultVoice: testing " << vlCode << endl; + if(languageCode == vlCode) + { + found = true; + break; + } + } + } + } + // If not found, pick first voice that is not "Unknown". + if (!found) + { + for(index = 0 ; index < m_voiceList.count(); ++index) + { + if (m_voiceList[index].name != i18n("Unknown")) + { + found = true; + break; + } + } + } + if (found) + { + // kdDebug() << "FestivalIntConf::setDefaultVoice: auto picking voice code " << m_voiceList[index].code << endl; + m_widget->selectVoiceCombo->setCurrentItem(index); + m_widget->preloadCheckBox->setChecked(m_voiceList[index].preload); + QString codecName = m_voiceList[index].codecName; + int codecNdx = PlugInProc::codecNameToListIndex(codecName, m_codecList); + m_widget->characterCodingBox->setCurrentItem(codecNdx); + if (m_voiceList[index].volumeAdjustable) + { + m_widget->volumeBox->setEnabled(true); + m_widget->volumeSlider->setEnabled(true); + } + else + { + m_widget->volumeBox->setValue(100); + volumeBox_valueChanged(100); + m_widget->volumeBox->setEnabled(false); + m_widget->volumeSlider->setEnabled(false); + } + if (m_voiceList[index].rateAdjustable) + { + m_widget->timeBox->setEnabled(true); + m_widget->timeSlider->setEnabled(true); + } + else + { + m_widget->timeBox->setValue(100); + timeBox_valueChanged(100); + m_widget->timeBox->setEnabled(false); + m_widget->timeSlider->setEnabled(false); + } + if (m_voiceList[index].pitchAdjustable) + { + m_widget->frequencyBox->setEnabled(true); + m_widget->frequencySlider->setEnabled(true); + } + else + { + m_widget->frequencyBox->setValue(100); + frequencyBox_valueChanged(100); + m_widget->frequencyBox->setEnabled(false); + m_widget->frequencySlider->setEnabled(false); + } + if ((int)index != currentVoiceIndex) configChanged(); + } + } +} + +/** + * Given an XML node and child element name, returns the string value from the child element. + * If no such child element, returns def. + */ +QString FestivalIntConf::readXmlString(QDomNode &node, const QString &elementName, const QString &def) +{ + QDomNode childNode = node.namedItem(elementName); + if (!childNode.isNull()) + return childNode.toElement().text(); + else + return def; +} + +/** + * Given an XML node and child element name, returns the boolean value from the child element. + * If no such child element, returns def. + */ +bool FestivalIntConf::readXmlBool(QDomNode &node, const QString &elementName, bool def) +{ + QDomNode childNode = node.namedItem(elementName); + if (!childNode.isNull()) + return (childNode.toElement().text() == "true"); + else + return def; +} + +void FestivalIntConf::scanVoices() +{ + // kdDebug() << "FestivalIntConf::scanVoices: Running" << endl; + // Get existing voice code (if any). + QString currentVoiceCode; + int index = m_widget->selectVoiceCombo->currentItem(); + if (index < (int)m_voiceList.count()) currentVoiceCode = m_voiceList[index].code; + + m_voiceList.clear(); + m_widget->selectVoiceCombo->clear(); + m_widget->selectVoiceCombo->insertItem(i18n("Scanning... Please wait.")); + + // Save current state of selectVoiceCombo box and disable. + bool selectVoiceComboEnabled = m_widget->selectVoiceCombo->isEnabled(); + m_widget->selectVoiceCombo->setEnabled(false); + + // Clear existing list of supported voice codes. + // m_supportedVoiceCodes.clear(); + m_widget->selectVoiceCombo->clear(); + + QString exePath = realFilePath(m_widget->festivalPath->url()); + if (!getLocation(exePath).isEmpty()) + { + // Set up a progress dialog. + m_progressDlg = new KProgressDialog(m_widget, "kttsmgr_queryvoices", + i18n("Query Voices"), + i18n("Querying Festival for available voices. This could take up to 15 seconds."), + true); + m_progressDlg->progressBar()->hide(); + m_progressDlg->setAllowCancel(true); + + // Create Festival process and request a list of voice codes. + if (m_festProc) + m_festProc->stopText(); + else + { + m_festProc = new FestivalIntProc(); + connect (m_festProc, SIGNAL(stopped()), this, SLOT(slotSynthStopped())); + } + connect (m_festProc, SIGNAL(queryVoicesFinished(const QStringList&)), + this, SLOT(slotQueryVoicesFinished(const QStringList&))); + m_festProc->queryVoices(exePath); + + // Display progress dialog modally. + m_progressDlg->exec(); + // kdDebug() << "FestivalIntConf::scanVoices: back from progressDlg->exec()" << endl; + + // Processing continues until either user clicks Cancel button, or until + // Festival responds with the list. When Festival responds with list, + // the progress dialog is closed. + + disconnect (m_festProc, SIGNAL(queryVoicesFinished(const QStringList&)), + this, SLOT(slotQueryVoicesFinished(const QStringList&))); + if (!m_progressDlg->wasCancelled()) m_festProc->stopText(); + delete m_progressDlg; + m_progressDlg = 0; + m_supportsSSML = m_festProc->supportsSSML(); + } + + if (!m_supportedVoiceCodes.isEmpty()) + { + // User's desktop language setting. + QString desktopLanguageCode = KGlobal::locale()->language(); + QString twoAlpha; + QString countryCode; + QString charSet; + KGlobal::locale()->splitLocale(desktopLanguageCode, twoAlpha, countryCode, charSet); + desktopLanguageCode = twoAlpha.lower(); + + // Festival known voices list. + QString voicesFilename = KGlobal::dirs()->resourceDirs("data").last() + "/kttsd/festivalint/voices"; + QDomDocument voicesDoc("Festival Voices"); + QFile voicesFile(voicesFilename); + if (voicesFile.open(IO_ReadOnly)) voicesDoc.setContent(&voicesFile); + voicesFile.close(); + QDomNodeList voices = voicesDoc.elementsByTagName("voice"); + uint voicesCount = voices.count(); + if (voicesCount == 0) + kdDebug() << "FestivalIntConf::scanVoices: Unable to open " << voicesFilename << ". Is KDEDIR defined?" << endl; + + // Iterate thru list of voice codes returned by Festival, + // find matching entry in voices.xml file, and add to list of supported voices. + QPixmap maleIcon = KGlobal::iconLoader()->loadIcon("male", KIcon::Small); + QPixmap femaleIcon = KGlobal::iconLoader()->loadIcon("female", KIcon::Small); + QStringList::ConstIterator itEnd = m_supportedVoiceCodes.constEnd(); + for(QStringList::ConstIterator it = m_supportedVoiceCodes.begin(); it != itEnd; ++it ) + { + QString code = *it; + bool found = false; + for (uint index=0; index < voicesCount; ++index) + { + QDomNode voiceNode = voices.item(index); + QString voiceCode = readXmlString(voiceNode, "code", QString::null); + // kdDebug() << "FestivalIntConf::scanVoices: Comparing code " << code << " to " << voiceCode << endl; + if (voiceCode == code) + { + found = true; + voiceStruct voiceTemp; + voiceTemp.code = code; + voiceTemp.name = i18n("FestivalVoiceName", + readXmlString(voiceNode, "name", "Unknown").utf8()); + voiceTemp.languageCode = readXmlString(voiceNode, "language", m_languageCode); + voiceTemp.codecName = readXmlString(voiceNode, "codec", "ISO 8859-1"); + voiceTemp.gender = readXmlString(voiceNode, "gender", "neutral"); + voiceTemp.preload = readXmlBool(voiceNode, "preload", false); + voiceTemp.volumeAdjustable = readXmlBool(voiceNode, "volume-adjustable", true); + voiceTemp.rateAdjustable = readXmlBool(voiceNode, "rate-adjustable", true); + voiceTemp.pitchAdjustable = readXmlBool(voiceNode, "pitch-adjustable", true); + m_voiceList.append(voiceTemp); + QString voiceDisplayName = voiceTemp.name + " (" + voiceTemp.code + ")"; + if (voiceTemp.gender == "male") + m_widget->selectVoiceCombo->insertItem(maleIcon, voiceDisplayName); + else if (voiceTemp.gender == "female") + m_widget->selectVoiceCombo->insertItem(femaleIcon, voiceDisplayName); + else + m_widget->selectVoiceCombo->insertItem(voiceDisplayName); + break; + } + } + if (!found) + { + voiceStruct voiceTemp; + voiceTemp.code = code; + voiceTemp.name = i18n("Unknown"); + voiceTemp.languageCode = m_languageCode; + voiceTemp.codecName = "ISO 8858-1"; + voiceTemp.gender = "neutral"; + voiceTemp.preload = false; + voiceTemp.volumeAdjustable = true; + voiceTemp.rateAdjustable = true; + voiceTemp.pitchAdjustable = true; + m_voiceList.append(voiceTemp); + m_widget->selectVoiceCombo->insertItem(voiceTemp.name + " (" + voiceTemp.code + ")"); + } + } + m_widget->selectVoiceCombo->setEnabled(true); + } else kdDebug() << "FestivalIntConf::scanVoices: No voices found" << endl; + setDefaultVoice(voiceCodeToListIndex(currentVoiceCode)); + // Emit configChanged if the enabled state of the selectVoiceCombo has changed. + // This occurs when user changes Festival EXE path, then clicks Rescan. + if (selectVoiceComboEnabled != m_widget->selectVoiceCombo->isEnabled()) configChanged(); +} + +void FestivalIntConf::slotQueryVoicesFinished(const QStringList &voiceCodes) +{ + // kdDebug() << "FestivalIntConf::slotQueryVoicesFinished: voiceCodes.count() = " << voiceCodes.count() << endl; + m_supportedVoiceCodes = voiceCodes; + if (m_progressDlg) m_progressDlg->close(); +} + +void FestivalIntConf::slotTest_clicked() +{ + // kdDebug() << "FestivalIntConf::slotTest_clicked: Running " << endl; + // If currently synthesizing, stop it. + if (m_festProc) + m_festProc->stopText(); + else + { + m_festProc = new FestivalIntProc(); + connect (m_festProc, SIGNAL(stopped()), this, SLOT(slotSynthStopped())); + } + // Create a temp file name for the wave file. + KTempFile tempFile (locateLocal("tmp", "festivalintplugin-"), ".wav"); + QString tmpWaveFile = tempFile.file()->name(); + tempFile.close(); + + // Get the code for the selected voice. + QString voiceCode = m_voiceList[m_widget->selectVoiceCombo->currentItem()].code; + + // Get language code for the selected voice. + QString languageCode = m_voiceList[m_widget->selectVoiceCombo->currentItem()].languageCode; + + // Get test message in the language of the voice. + QString testMsg = testMessage(languageCode); + + // Get codec. + QTextCodec* codec = PlugInProc::codecIndexToCodec( + m_widget->characterCodingBox->currentItem(), m_codecList); + + // Tell user to wait. + m_progressDlg = new KProgressDialog(m_widget, "ktts_festivalint_testdlg", + i18n("Testing"), + i18n("Testing. MultiSyn voices require several seconds to load. Please be patient."), + true); + m_progressDlg->progressBar()->hide(); + m_progressDlg->setAllowCancel(true); + + // kdDebug() << "FestivalIntConf::slotTest_clicked: calling synth with voiceCode: " << voiceCode << " time percent: " << m_widget->timeBox->value() << endl; + connect (m_festProc, SIGNAL(synthFinished()), this, SLOT(slotSynthFinished())); + m_festProc->synth( + realFilePath(m_widget->festivalPath->url()), + testMsg, + tmpWaveFile, + voiceCode, + m_widget->timeBox->value(), + m_widget->frequencyBox->value(), + m_widget->volumeBox->value(), + languageCode, + codec); + + // Display progress dialog modally. Processing continues when plugin signals synthFinished, + // or if user clicks Cancel button. + m_progressDlg->exec(); + disconnect (m_festProc, SIGNAL(synthFinished()), this, SLOT(slotSynthFinished())); + if (m_progressDlg->wasCancelled()) m_festProc->stopText(); + delete m_progressDlg; + m_progressDlg = 0; +} + +void FestivalIntConf::slotSynthFinished() +{ + // kdDebug() << "FestivalIntConf::slotSynthFinished: Running" << endl; + // If user canceled, progress dialog is gone, so exit. + if (!m_progressDlg) + { + m_festProc->ackFinished(); + return; + } + // Hide the Cancel button so user can't cancel in the middle of playback. + m_progressDlg->showCancelButton(false); + // Get new wavefile name. + m_waveFile = m_festProc->getFilename(); + // Tell synth we're done. + m_festProc->ackFinished(); + // Play the wave file (possibly adjusting its Speed). + // Player object deletes the wave file when done. + if (m_player) m_player->play(m_waveFile); + QFile::remove(m_waveFile); + m_waveFile = QString::null; + if (m_progressDlg) m_progressDlg->close(); +} + +void FestivalIntConf::slotSynthStopped() +{ + // Clean up after canceling test. + QString filename = m_festProc->getFilename(); + // kdDebug() << "FestivalIntConf::slotSynthStopped: filename = " << filename << endl; + if (!filename.isNull()) QFile::remove(filename); +} + +void FestivalIntConf::slotFestivalPath_textChanged() +{ + QString exePath = realFilePath(m_widget->festivalPath->url()); + m_widget->selectVoiceCombo->setEnabled(false); + if (!exePath.isEmpty() && !getLocation(exePath).isEmpty()) + { + m_widget->rescan->setEnabled(true); + } else m_widget->rescan->setEnabled(false); +} + +void FestivalIntConf::slotSelectVoiceCombo_activated() +{ + int index = m_widget->selectVoiceCombo->currentItem(); + QString codecName = m_voiceList[index].codecName; + int codecNdx = PlugInProc::codecNameToListIndex(codecName, m_codecList); + m_widget->characterCodingBox->setCurrentItem(codecNdx); + m_widget->preloadCheckBox->setChecked( + m_voiceList[index].preload); + if (m_voiceList[index].volumeAdjustable) + { + m_widget->volumeBox->setEnabled(true); + m_widget->volumeSlider->setEnabled(true); + } + else + { + m_widget->volumeBox->setValue(100); + volumeBox_valueChanged(100); + m_widget->volumeBox->setEnabled(false); + m_widget->volumeSlider->setEnabled(false); + } + if (m_voiceList[index].rateAdjustable) + { + m_widget->timeBox->setEnabled(true); + m_widget->timeSlider->setEnabled(true); + } + else + { + m_widget->timeBox->setValue(100); + timeBox_valueChanged(100); + m_widget->timeBox->setEnabled(false); + m_widget->timeSlider->setEnabled(false); + } + if (m_voiceList[index].pitchAdjustable) + { + m_widget->frequencyBox->setEnabled(true); + m_widget->frequencySlider->setEnabled(true); + } + else + { + m_widget->frequencyBox->setValue(100); + frequencyBox_valueChanged(100); + m_widget->frequencyBox->setEnabled(false); + m_widget->frequencySlider->setEnabled(false); + } +} + +// Basically the slider values are logarithmic (0,...,1000) whereas percent +// values are linear (50%,...,200%). +// +// slider = alpha * (log(percent)-log(50)) +// with alpha = 1000/(log(200)-log(50)) + +int FestivalIntConf::percentToSlider(int percentValue) { + double alpha = 1000 / (log(200) - log(50)); + return (int)floor (0.5 + alpha * (log(percentValue)-log(50))); +} + +int FestivalIntConf::sliderToPercent(int sliderValue) { + double alpha = 1000 / (log(200) - log(50)); + return (int)floor(0.5 + exp (sliderValue/alpha + log(50))); +} + +void FestivalIntConf::volumeBox_valueChanged(int percentValue) { + m_widget->volumeSlider->setValue(percentToSlider(percentValue)); +} + +void FestivalIntConf::timeBox_valueChanged(int percentValue) { + m_widget->timeSlider->setValue (percentToSlider (percentValue)); +} + +void FestivalIntConf::frequencyBox_valueChanged(int percentValue) { + m_widget->frequencySlider->setValue(percentToSlider(percentValue)); +} + +void FestivalIntConf::volumeSlider_valueChanged(int sliderValue) { + m_widget->volumeBox->setValue(sliderToPercent(sliderValue)); +} + +void FestivalIntConf::timeSlider_valueChanged(int sliderValue) { + m_widget->timeBox->setValue (sliderToPercent (sliderValue)); +} + +void FestivalIntConf::frequencySlider_valueChanged(int sliderValue) { + m_widget->frequencyBox->setValue(sliderToPercent(sliderValue)); +} diff --git a/kttsd/plugins/festivalint/festivalintconf.h b/kttsd/plugins/festivalint/festivalintconf.h new file mode 100644 index 0000000..a367682 --- /dev/null +++ b/kttsd/plugins/festivalint/festivalintconf.h @@ -0,0 +1,187 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Configuration widget and functions for Festival (Interactive) plug in + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#ifndef _FESTIVALINTCONF_H_ +#define _FESTIVALINTCONF_H_ + +// Qt includes. +#include <qstringlist.h> +#include <qvaluelist.h> + +// KDE includes. +#include <kconfig.h> +#include <kdebug.h> + +// KTTS includes. +#include "pluginconf.h" + +// FestivalInt includes. +#include "festivalintconfwidget.h" +#include "festivalintproc.h" + +class KProgressDialog; +class QDomNode; + +typedef struct voiceStruct{ + QString code; // Code as sent to Festival + QString name; // Name as displayed and returned in Talker Code. + QString languageCode; // Language code (en, es, etc) + QString codecName; // Character encoding codec name (eg. ISO 8859-1) + QString gender; // male, female, or neutral + bool preload; // Start Festival and load this language when KTTSD is started. + bool volumeAdjustable; // True if the voice supports volume adjustments. + bool rateAdjustable; // True if the voice supports rate adjustments. + bool pitchAdjustable; // True if the voice supports pitch adjustments. +} voice; + +class FestivalIntConf : public PlugInConf { + Q_OBJECT + + public: + /** Constructor */ + FestivalIntConf( QWidget* parent = 0, const char* name = 0, const QStringList &args = QStringList()); + + /** Destructor */ + ~FestivalIntConf(); + + /** This method is invoked whenever the module should read its + * configuration (most of the times from a config file) and update the + * user interface. This happens when the user clicks the "Reset" button in + * the control center, to undo all of his changes and restore the currently + * valid settings. NOTE that this is not called after the modules is loaded, + * so you probably want to call this method in the constructor. + */ + void load(KConfig *config, const QString &configGroup); + + /** This function gets called when the user wants to save the settings in + * the user interface, updating the config files or wherever the + * configuration is stored. The method is called when the user clicks "Apply" + * or "Ok". + */ + void save(KConfig *config, const QString &configGroup); + + /** This function is called to set the settings in the module to sensible + * default values. It gets called when hitting the "Default" button. The + * default values should probably be the same as the ones the application + * uses when started without a config file. */ + void defaults(); + + /** + * This function informs the plugin of the desired language to be spoken + * by the plugin. The plugin should attempt to adapt itself to the + * specified language code, choosing sensible defaults if necessary. + * If the passed-in code is QString::null, no specific language has + * been chosen. + * @param lang The desired language code or Null if none. + * + * If the plugin is unable to support the desired language, that is OK. + * Language codes are given by ISO 639-1 and are in lowercase. + * The code may also include an ISO 3166 country code in uppercase + * separated from the language code by underscore (_). For + * example, en_GB. If your plugin supports the given language, but + * not the given country, treat it as though the country + * code were not specified, i.e., adapt to the given language. + */ + void setDesiredLanguage(const QString &lang); + + /** + * Return fully-specified talker code for the configured plugin. This code + * uniquely identifies the configured instance of the plugin and distinquishes + * one instance from another. If the plugin has not been fully configured, + * i.e., cannot yet synthesize, return QString::null. + * @return Fully-specified talker code. + */ + QString getTalkerCode(); + + private slots: + /** Scan for the different voices in festivalPath/lib */ + void scanVoices(); + void configChanged(){ + // kdDebug() << "FestivalIntConf::configChanged: Running" << endl; + emit changed(true); + }; + void slotTest_clicked(); + void slotSynthFinished(); + void slotSynthStopped(); + void volumeBox_valueChanged(int percentValue); + void timeBox_valueChanged(int percentValue); + void frequencyBox_valueChanged(int percentValue); + void volumeSlider_valueChanged(int sliderValue); + void timeSlider_valueChanged(int sliderValue); + void frequencySlider_valueChanged(int sliderValue); + void slotFestivalPath_textChanged(); + void slotSelectVoiceCombo_activated(); + void slotQueryVoicesFinished(const QStringList &voiceCodes); + + private: + int percentToSlider(int percentValue); + int sliderToPercent(int sliderValue); + + /** + * Given an XML node and child element name, returns the string value from the child element. + * If no such child element, returns def. + */ + QString readXmlString(QDomNode &node, const QString &elementName, const QString &def); + + /** + * Given an XML node and child element name, returns the boolean value from the child element. + * If no such child element, returns def. + */ + bool readXmlBool(QDomNode &node, const QString &elementName, bool def); + + /** + * Given a voice code, returns index into m_voiceList array (and voiceCombo box). + * -1 if not found. + */ + int voiceCodeToListIndex(const QString& voiceCode) const; + + /** + * Chooses a default voice given scanned list of voices in m_voiceList and current + * language and country code, and updates controls. + * @param currentVoiceIndex This voice is preferred if it matches. + */ + void setDefaultVoice(int currentVoiceIndex); + + // Configuration Widget. + FestivalIntConfWidget* m_widget; + + // Language code. + QString m_languageCode; + // Language country code (if any). + QString m_countryCode; + // List of voices */ + QValueList<voice> m_voiceList; + // Festival synthesizer. + FestivalIntProc* m_festProc; + // Synthesized wave file name. + QString m_waveFile; + // Progress dialog. + KProgressDialog* m_progressDlg; + // List of voice codes supported by Festival. + QStringList m_supportedVoiceCodes; + // List of displayed codec names. + QStringList m_codecList; + // Whether Festival supports SSML or not. + FestivalIntProc::SupportsSSML m_supportsSSML; +}; +#endif // _FESTIVALINTCONF_H_ diff --git a/kttsd/plugins/festivalint/festivalintconfwidget.ui b/kttsd/plugins/festivalint/festivalintconfwidget.ui new file mode 100644 index 0000000..f8b33fb --- /dev/null +++ b/kttsd/plugins/festivalint/festivalintconfwidget.ui @@ -0,0 +1,598 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>FestivalIntConfWidget</class> +<author>Gary Cramblitt <garycramblitt@comcast.net></author> +<widget class="QWidget"> + <property name="name"> + <cstring>FestivalIntConfWidget</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>559</width> + <height>318</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>7</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="caption"> + <string>Festival Config UI</string> + </property> + <property name="whatsThis" stdset="0"> + <string>This is the dialog for configuring the Festival speech synthesizer in interactive mode.</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QGroupBox" row="0" column="0"> + <property name="name"> + <cstring>festivalConfigurationBox</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>GroupBoxPanel</enum> + </property> + <property name="frameShadow"> + <enum>Sunken</enum> + </property> + <property name="title"> + <string>Festival &Interactive Configuration</string> + </property> + <property name="whatsThis" stdset="0"> + <string>This is the dialog for configuring the Festival speech synthesizer in interactive mode.</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>11</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QLayoutWidget" row="0" column="0"> + <property name="name"> + <cstring>voicesPathBox</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>festivalPathLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&Festival executable:</string> + </property> + <property name="alignment"> + <set>AlignVCenter|AlignRight</set> + </property> + <property name="buddy" stdset="0"> + <cstring>festivalPath</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>If Festival is in your PATH environment variable, just enter "festival", otherwise specify the full path to the Festival executable program.</string> + </property> + </widget> + <widget class="KURLRequester"> + <property name="name"> + <cstring>festivalPath</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="whatsThis" stdset="0"> + <string>If Festival is in your PATH environment variable, just enter "festival", otherwise specify the full path to the Festival executable program.</string> + </property> + </widget> + </hbox> + </widget> + <widget class="QLayoutWidget" row="1" column="0"> + <property name="name"> + <cstring>selectVoiceBox</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>selectVoiceLabel</cstring> + </property> + <property name="text"> + <string>&Select voice:</string> + </property> + <property name="alignment"> + <set>AlignVCenter|AlignRight</set> + </property> + <property name="buddy" stdset="0"> + <cstring>selectVoiceCombo</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Select a voice to speak text with. MultiSyn voices are high quality but are slow to load. If no voices are shown, check the Festival executable path. You must install at least one Festival voice. If you have installed a voice and still none are shown, check your Festival configuration. (See the README that comes with Festival.)</string> + </property> + </widget> + <widget class="KComboBox"> + <property name="name"> + <cstring>selectVoiceCombo</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="whatsThis" stdset="0"> + <string>Select a voice to speak text with. MultiSyn voices are high quality but are slow to load. If no voices are shown, check the Festival executable path. You must install at least one Festival voice. If you have installed a voice and still none are shown, check your Festival configuration. (See the README that comes with Festival.)</string> + </property> + </widget> + <widget class="KPushButton"> + <property name="name"> + <cstring>rescan</cstring> + </property> + <property name="text"> + <string>&Rescan</string> + <comment>Rescan for voices</comment> + </property> + </widget> + </hbox> + </widget> + <widget class="QLayoutWidget" row="2" column="0"> + <property name="name"> + <cstring>layout11</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout8</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>volumeLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&Volume:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>volumeBox</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the volume (loudness) of speech. Slide the slider to the left to lower the volume; to the right to increase volume. Anything less than 75 percent is considered "soft", and anything greater than 125 percent is considered "loud".</string> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>timeLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Sp&eed:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>timeBox</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the speed of speech. Slide the slider to the left to slow speech down; to the right to increase talking speed. Anything less than 75 percent is considered "slow", and anything greater than 125 percent is considered "fast". You cannot change the speed of MultiSyn voices.</string> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>frequencyLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&Pitch:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>frequencyBox</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the tone (frequency) of speech. Slide the slider to the left to lower the voice tone; to the right to increase tone. Anything less than 75 percent is considered "low", and anything greater than 125 percent is considered "high". You cannot change the pitch of MultiSyn voices.</string> + </property> + </widget> + </vbox> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout9</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="KIntSpinBox"> + <property name="name"> + <cstring>volumeBox</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string> %</string> + </property> + <property name="maxValue"> + <number>200</number> + </property> + <property name="minValue"> + <number>50</number> + </property> + <property name="value"> + <number>100</number> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the volume (loudness) of speech. Slide the slider to the left to lower the volume; to the right to increase volume. Anything less than 75 percent is considered "soft", and anything greater than 125 percent is considered "loud".</string> + </property> + </widget> + <widget class="KIntSpinBox"> + <property name="name"> + <cstring>timeBox</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string> %</string> + </property> + <property name="maxValue"> + <number>200</number> + </property> + <property name="minValue"> + <number>50</number> + </property> + <property name="value"> + <number>100</number> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the speed of speech. Slide the slider to the left to slow speech down; to the right to increase talking speed. Anything less than 75 percent is considered "slow", and anything greater than 125 percent is considered "fast". You cannot change the speed of MultiSyn voices.</string> + </property> + </widget> + <widget class="KIntSpinBox"> + <property name="name"> + <cstring>frequencyBox</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string> %</string> + </property> + <property name="maxValue"> + <number>200</number> + </property> + <property name="minValue"> + <number>50</number> + </property> + <property name="value"> + <number>100</number> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the tone (frequency) of speech. Slide the slider to the left to lower the voice tone; to the right to increase tone. Anything less than 75 percent is considered "low", and anything greater than 125 percent is considered "high". You cannot change the pitch of MultiSyn voices.</string> + </property> + </widget> + </vbox> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout10</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QSlider"> + <property name="name"> + <cstring>volumeSlider</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="focusPolicy"> + <enum>NoFocus</enum> + </property> + <property name="minValue"> + <number>0</number> + </property> + <property name="maxValue"> + <number>1000</number> + </property> + <property name="lineStep"> + <number>10</number> + </property> + <property name="pageStep"> + <number>100</number> + </property> + <property name="value"> + <number>500</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the volume (loudness) of speech. Slide the slider to the left to lower the volume; to the right to increase volume. Anything less than 75 percent is considered "soft", and anything greater than 125 percent is considered "loud".</string> + </property> + </widget> + <widget class="QSlider"> + <property name="name"> + <cstring>timeSlider</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="focusPolicy"> + <enum>NoFocus</enum> + </property> + <property name="maxValue"> + <number>1000</number> + </property> + <property name="lineStep"> + <number>10</number> + </property> + <property name="pageStep"> + <number>100</number> + </property> + <property name="value"> + <number>500</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the speed of speech. Slide the slider to the left to slow speech down; to the right to increase talking speed. Anything less than 75 percent is considered "slow", and anything greater than 125 percent is considered "fast". You cannot change the speed of MultiSyn voices.</string> + </property> + </widget> + <widget class="QSlider"> + <property name="name"> + <cstring>frequencySlider</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="focusPolicy"> + <enum>NoFocus</enum> + </property> + <property name="maxValue"> + <number>1000</number> + </property> + <property name="lineStep"> + <number>10</number> + </property> + <property name="pageStep"> + <number>100</number> + </property> + <property name="value"> + <number>500</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="whatsThis" stdset="0"> + <string>Sets the tone (frequency) of speech. Slide the slider to the left to lower the voice tone; to the right to increase tone. Anything less than 75 percent is considered "low", and anything greater than 125 percent is considered "high". You cannot change the pitch of MultiSyn voices.</string> + </property> + </widget> + </vbox> + </widget> + </hbox> + </widget> + <widget class="QLayoutWidget" row="4" column="0"> + <property name="name"> + <cstring>layout6</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QCheckBox"> + <property name="name"> + <cstring>preloadCheckBox</cstring> + </property> + <property name="text"> + <string>&Load this voice when starting KTTSD</string> + </property> + <property name="whatsThis" stdset="0"> + <string>If checked, Festival will be started and this voice will be loaded when the Text-to-Speech Deamon (KTTSD) is started. Check when a voice requires a long time to load in Festival (for example, multisyn voices), otherwise, leave unchecked.</string> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>spacer3</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>101</width> + <height>20</height> + </size> + </property> + </spacer> + <widget class="QPushButton"> + <property name="name"> + <cstring>testButton</cstring> + </property> + <property name="text"> + <string>&Test</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Click to test the configuration. Festival will be started and a test sentence will be spoken.</string> + </property> + </widget> + </hbox> + </widget> + <widget class="QLayoutWidget" row="3" column="0"> + <property name="name"> + <cstring>layout5</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>characterCodingLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Character e&ncoding:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>characterCodingBox</cstring> + </property> + </widget> + <widget class="KComboBox"> + <property name="name"> + <cstring>characterCodingBox</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="whatsThis" stdset="0"> + <string>This combo box specifies which character encoding is used for passing the text.</string> + </property> + </widget> + </hbox> + </widget> + </grid> + </widget> + </grid> +</widget> +<customwidgets> +</customwidgets> +<tabstops> + <tabstop>festivalPath</tabstop> + <tabstop>selectVoiceCombo</tabstop> + <tabstop>rescan</tabstop> + <tabstop>volumeBox</tabstop> + <tabstop>timeBox</tabstop> + <tabstop>frequencyBox</tabstop> + <tabstop>preloadCheckBox</tabstop> + <tabstop>testButton</tabstop> +</tabstops> +<includes> + <include location="global" impldecl="in declaration">kurlrequester.h</include> + <include location="global" impldecl="in implementation">kurlrequester.h</include> +</includes> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>kurlrequester.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>kcombobox.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>knuminput.h</includehint> + <includehint>knuminput.h</includehint> + <includehint>knuminput.h</includehint> + <includehint>kcombobox.h</includehint> +</includehints> +</UI> diff --git a/kttsd/plugins/festivalint/festivalintplugin.cpp b/kttsd/plugins/festivalint/festivalintplugin.cpp new file mode 100644 index 0000000..d793b40 --- /dev/null +++ b/kttsd/plugins/festivalint/festivalintplugin.cpp @@ -0,0 +1,31 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Generating the factories so festival (interactive) can be used as plug in. + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#include <kgenericfactory.h> + +#include "festivalintconf.h" +#include "festivalintproc.h" + +typedef K_TYPELIST_2( FestivalIntProc, FestivalIntConf ) Festival; +K_EXPORT_COMPONENT_FACTORY( libkttsd_festivalintplugin, KGenericFactory<Festival>("kttsd_festivalint") ) + diff --git a/kttsd/plugins/festivalint/festivalintproc.cpp b/kttsd/plugins/festivalint/festivalintproc.cpp new file mode 100644 index 0000000..77822ea --- /dev/null +++ b/kttsd/plugins/festivalint/festivalintproc.cpp @@ -0,0 +1,662 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Main speaking functions for the Festival (Interactive) Plug in + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +// C++ includes. +#include <math.h> + +// Qt includes. +#include <qstring.h> +#include <qstringlist.h> +#include <qthread.h> +#include <qtextcodec.h> + +// KDE includes. +#include <kdebug.h> +#include <kconfig.h> +#include <kstandarddirs.h> + +// KTTS includes. +#include "utils.h" + +// FestivalInt includes. +#include "festivalintproc.h" +#include "festivalintproc.moc" + +/** Constructor */ +FestivalIntProc::FestivalIntProc( QObject* parent, const char* name, const QStringList& ) : + PlugInProc( parent, name ){ + // kdDebug() << "FestivalIntProc::FestivalIntProc: Running" << endl; + m_ready = true; + m_writingStdin = false; + m_waitingQueryVoices = false; + m_waitingStop = false; + m_festProc = 0; + m_state = psIdle; + m_supportsSSML = ssUnknown; + m_languageCode = "en"; + m_codec = QTextCodec::codecForName("ISO8859-1"); +} + +/** Destructor */ +FestivalIntProc::~FestivalIntProc(){ + // kdDebug() << "FestivalIntProc::~FestivalIntProc: Running" << endl; + if (m_festProc) + { + if (m_festProc->isRunning()) + { + if (m_ready) + { + m_state = psIdle; + // kdDebug() << "FestivalIntProc::~FestivalIntProc: telling Festival to quit." << endl; + m_ready = false; + m_waitingStop = true; + m_festProc->writeStdin("(quit)", true); + } + else + { + // kdDebug() << "FestivalIntProc::~FestivalIntProc: killing Festival." << endl; + m_waitingStop = true; + m_festProc->kill(); + } + } + delete m_festProc; + } +} + +/** Initialize the speech */ +bool FestivalIntProc::init(KConfig *config, const QString &configGroup) +{ + // kdDebug() << "FestivalIntProc::init: Initializing plug in: Festival" << endl; + + config->setGroup(configGroup); + m_voiceCode = config->readEntry("Voice"); + m_festivalExePath = config->readEntry("FestivalExecutablePath", "festival"); + // kdDebug() << "---- The code for the selected voice " << config->readEntry("Voice") << " is " << voiceCode << endl; + m_time = config->readNumEntry("time", 100); + m_pitch = config->readNumEntry("pitch", 100); + m_volume = config->readNumEntry("volume", 100); + // If voice should be pre-loaded, start Festival and load the voice. + m_preload = config->readBoolEntry("Preload", false); + m_languageCode = config->readEntry("LanguageCode", "en"); + m_supportsSSML = static_cast<SupportsSSML>(config->readNumEntry("SupportsSSML", ssUnknown)); + QString codecName = config->readEntry("Codec", "Latin1"); + m_codec = codecNameToCodec(codecName); + if (m_preload) startEngine(m_festivalExePath, m_voiceCode, m_languageCode, m_codec); + 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 FestivalIntProc::sayText(const QString &text) +{ + synth(m_festivalExePath, text, QString::null, m_voiceCode, m_time, m_pitch, m_volume, + m_languageCode, m_codec); +} + +/** +* 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 FestivalIntProc::synthText(const QString& text, const QString& suggestedFilename) +{ + synth(m_festivalExePath, text, suggestedFilename, m_voiceCode, m_time, m_pitch, m_volume, + m_languageCode, m_codec); +} + +/** +* Sends command to Festival to query for a list of supported voice codes. +* Fires queryVoicesFinished when completed. +* @return False if busy doing something else and therefore cannot +* do the query. +*/ +bool FestivalIntProc::queryVoices(const QString &festivalExePath) +{ + // kdDebug() << "FestivalIntProc::queryVoices: Running" << endl; + if (m_state != psIdle && m_waitingQueryVoices && m_waitingStop) return false; + // Start Festival if not already running. + startEngine(festivalExePath, QString::null, m_languageCode, m_codec); + // Set state, waiting for voice codes list from Festival. + m_waitingQueryVoices = true; + // Voice rab_diphone is needed in order to support SSML. + m_supportsSSML = ssUnknown; + // Send command to query the voice codes. + sendToFestival("(print (mapcar (lambda (pair) (car pair)) voice-locations))"); + return true; +} + +/** +* Start Festival engine. +* @param festivalExePath Path to the Festival executable, or just "festival". +* @param voiceCode Voice code in which to speak text. +* @param languageCode Language code, for example, "en". +*/ +void FestivalIntProc::startEngine(const QString &festivalExePath, const QString &voiceCode, + const QString &languageCode, QTextCodec* codec) +{ + // Initialize Festival only if it's not initialized. + if (m_festProc) + { + // Stop Festival if a different EXE is requested or different language code. + // If festProc exists but is not running, it is because it was stopped. + if ((festivalExePath != m_festivalExePath) || !m_festProc->isRunning() || + (m_languageCode != languageCode) || (codec->name() != m_codec->name())) + { + delete m_festProc; + m_festProc = 0; + } + } + if(!m_festProc) + { + // kdDebug()<< "FestivalIntProc::startEngine: Creating Festival object" << endl; + m_festProc = new KProcess; + *m_festProc << festivalExePath; + *m_festProc << "--interactive"; + m_festProc->setEnvironment("LANG", languageCode + "." + codec->mimeName()); + m_festProc->setEnvironment("LC_CTYPE", languageCode + "." + codec->mimeName()); + // kdDebug() << "FestivalIntProc::startEngine: setting LANG = LC_CTYPE = " << languageCode << "." << codec->mimeName() << endl; + connect(m_festProc, SIGNAL(processExited(KProcess*)), + this, SLOT(slotProcessExited(KProcess*))); + connect(m_festProc, SIGNAL(receivedStdout(KProcess*, char*, int)), + this, SLOT(slotReceivedStdout(KProcess*, char*, int))); + connect(m_festProc, SIGNAL(receivedStderr(KProcess*, char*, int)), + this, SLOT(slotReceivedStderr(KProcess*, char*, int))); + connect(m_festProc, SIGNAL(wroteStdin(KProcess*)), + this, SLOT(slotWroteStdin(KProcess*))); + } + if (!m_festProc->isRunning()) + { + // kdDebug() << "FestivalIntProc::startEngine: Starting Festival process" << endl; + m_runningVoiceCode = QString::null; + m_runningTime = 100; + m_runningPitch = 100; + m_ready = false; + m_outputQueue.clear(); + if (m_festProc->start(KProcess::NotifyOnExit, KProcess::All)) + { + // kdDebug()<< "FestivalIntProc:startEngine: Festival initialized" << endl; + m_festivalExePath = festivalExePath; + m_languageCode = languageCode; + m_codec = codec; + // Load the SABLE to Wave module. + sendToFestival("(load \"" + + KGlobal::dirs()->resourceDirs("data").last() + "kttsd/festivalint/sabletowave.scm\")"); + } + else + { + kdDebug() << "FestivalIntProc::startEngine: Error starting Festival process. Is festival in the PATH?" << endl; + m_ready = true; + m_state = psIdle; + return; + } + } + // If we just started Festival, or voiceCode has changed, send code to Festival. + if (m_runningVoiceCode != voiceCode && !voiceCode.isEmpty()) { + sendToFestival("(voice_" + voiceCode + ")"); + m_runningVoiceCode = voiceCode; + } +} + +/** +* Say or Synthesize text. +* @param festivalExePath Path to the Festival executable, or just "festival". +* @param text The text to be synthesized. +* @param suggestedFilename If not Null, synthesize only to this filename, otherwise +* synthesize and audibilize the text. +* @param voiceCode Voice code in which to speak text. +* @param time Speed percentage. 50 to 200. 200% = 2x normal. +* @param pitch Pitch persentage. 50 to 200. +* @param volume Volume percentage. 50 to 200. +* @param languageCode Language code, for example, "en". +*/ +void FestivalIntProc::synth( + const QString &festivalExePath, + const QString &text, + const QString &synthFilename, + const QString &voiceCode, + int time, + int pitch, + int volume, + const QString &languageCode, + QTextCodec* codec) +{ + // kdDebug() << "FestivalIntProc::synth: festivalExePath = " << festivalExePath + // << " voiceCode = " << voiceCode << endl; + + // Initialize Festival only if it's not initialized + startEngine(festivalExePath, voiceCode, languageCode, codec); + // If we just started Festival, or rate changed, tell festival. + if (m_runningTime != time) { + QString timeMsg; + if (voiceCode.contains("_hts") > 0) + { + // Map 50% to 200% onto 0 to 1000. + // slider = alpha * (log(percent)-log(50)) + // with alpha = 1000/(log(200)-log(50)) + double alpha = 1000 / (log(200) - log(50)); + int slider = (int)floor (0.5 + alpha * (log(time)-log(50))); + // Center at 0. + slider = slider - 500; + // Map -500 to 500 onto 0.15 to -0.15. + float stretchValue = -float(slider) * 0.15 / 500.0; + timeMsg = QString("(set! hts_duration_stretch %1)").arg( + stretchValue, 0, 'f', 3); + } + else + timeMsg = QString("(Parameter.set 'Duration_Stretch %1)").arg( + 1.0/(float(time)/100.0), 0, 'f', 2); + sendToFestival(timeMsg); + m_runningTime = time; + } + // If we just started Festival, or pitch changed, tell festival. + if (m_runningPitch != pitch) { + // Pitch values range from 50 to 200 %, with 100% as the midpoint, + // while frequency values range from 41 to 500 with 105 as the "midpoint". + int pitchValue; + if (pitch <= 100) + { + pitchValue = (((pitch - 50) * 64) / 50) + 41; + } + else + { + pitchValue = (((pitch - 100) * 395) / 100) + 105; + } + QString pitchMsg = QString( + "(set! int_lr_params '((target_f0_mean %1) (target_f0_std 14)" + "(model_f0_mean 170) (model_f0_std 34)))").arg(pitchValue, 0, 10); + sendToFestival(pitchMsg); + m_runningPitch = pitch; + } + + QString saidText = text; + + // Split really long sentences into shorter sentences, by looking for commas and converting + // to periods. + int len = saidText.length(); + while (len > c_tooLong) + { + len = saidText.findRev(", ", len - (c_tooLong * 2 / 3), true); + if (len != -1) + { + QString c = saidText.mid(len+2, 1); + if (c != c.upper()) + { + saidText.replace(len, 2, ". "); + saidText.replace(len+2, 1, c.upper()); + kdDebug() << "FestivalIntProc::synth: Splitting long sentence at " << len << endl; + // kdDebug() << saidText << endl; + } + } + } + + // Encode quotation characters. + saidText.replace("\\\"", "#!#!"); + saidText.replace("\"", "\\\""); + saidText.replace("#!#!", "\\\""); + // Remove certain comment characters. + saidText.replace("--", ""); + + // Ok, let's rock. + if (synthFilename.isNull()) + { + m_state = psSaying; + m_synthFilename = QString::null; + // kdDebug() << "FestivalIntProc::synth: Saying text: '" << saidText << "' using Festival plug in with voice " + // << voiceCode << endl; + saidText = "(SayText \"" + saidText + "\")"; + sendToFestival(saidText); + } else { + m_state = psSynthing; + m_synthFilename = synthFilename; + // Volume must be given for each utterance. + // Volume values range from 50 to 200%, with 100% = normal. + // Map onto rescale range of .5 to 2. + float volumeValue = float(volume) / 100; + // Expand to range .25 to 4. + // float volumeValue = exp(log(volumeValue) * 2); + // kdDebug() << "FestivalIntProc::synth: Synthing text: '" << saidText << "' using Festival plug in with voice " + // << voiceCode << endl; + if (isSable(saidText)) + { + // Synth the text and adjust volume. + saidText = + "(ktts_sabletowave \"" + saidText + "\" \"" + + synthFilename + "\" " + + QString::number(volumeValue) + ")"; + } + else + { + saidText = + // Suppress pause at the beginning of each utterance. + "(define (insert_initial_pause utt) " + "(item.set_feat (utt.relation.first utt 'Segment) 'end 0.0))" + // Synth the text and adjust volume. + "(set! utt1 (Utterance Text \"" + saidText + + "\"))(utt.synth utt1)" + + "(utt.wave.rescale utt1 " + QString::number(volumeValue) + " t)" + + "(utt.save.wave utt1 \"" + synthFilename + "\")"; + } + sendToFestival(saidText); + } +} + +/** +* If ready for more output, sends the given text to Festival process, otherwise, +* puts it in the queue. +* @param text Text to send or queue. +*/ +void FestivalIntProc::sendToFestival(const QString& text) +{ + if (text.isNull()) return; + m_outputQueue.append(text); + sendIfReady(); +} + +/** +* If Festival is ready for more input and there is more output to send, send it. +* To be ready for more input, the Stdin buffer must be empty and the "festival>" +* prompt must have been received (m_ready = true). +* @return False when Festival is ready for more input +* but there is nothing to be sent, or if Festival +* has exited. +*/ +bool FestivalIntProc::sendIfReady() +{ + if (!m_ready) return true; + if (m_writingStdin) return true; + if (m_outputQueue.isEmpty()) return false; + if (!m_festProc->isRunning()) return false; + QString text = m_outputQueue[0]; + text += "\n"; + QCString encodedText; + if (m_codec) + encodedText = m_codec->fromUnicode(text); + else + encodedText = text.latin1(); // Should not happen, but just in case. + m_outputQueue.pop_front(); + m_ready = false; + // kdDebug() << "FestivalIntProc::sendIfReady: sending to Festival: " << text << endl; + m_writingStdin = true; + m_festProc->writeStdin(encodedText, encodedText.length()); + return true; +} + +/** +* Determine if the text has SABLE tags. If so, we will have to use a different +* synthesis method. +*/ +bool FestivalIntProc::isSable(const QString &text) +{ + return KttsUtils::hasRootElement( text, "SABLE" ); +} + +/** +* 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 FestivalIntProc::getFilename() { return m_synthFilename; } + +/** + * Stop text + */ +void FestivalIntProc::stopText(){ + // kdDebug() << "FestivalIntProc::stopText: Running" << endl; + if (m_festProc) + { + if (m_festProc->isRunning()) + { + if (m_ready) + m_state = psIdle; + else + { + // If using a preloaded voice, killing Festival is a bad idea because of + // huge startup times. So if synthing (not saying), let Festival continue + // synthing. When it completes, we will emit the stopped signal. + if (m_preload && (m_state == psSynthing)) + { + m_waitingStop = true; + // kdDebug() << "FestivalIntProc::stopText: Optimizing stopText() for preloaded voice." << endl; + } + else + { + // kdDebug() << "FestivalIntProc::stopText: killing Festival." << endl; + m_waitingStop = true; + m_festProc->kill(); + } + } + } else m_state = psIdle; + } else m_state = psIdle; +} + +void FestivalIntProc::slotProcessExited(KProcess*) +{ + // kdDebug() << "FestivalIntProc:slotProcessExited: Festival process has exited." << endl; + m_ready = true; + pluginState prevState = m_state; + if (m_waitingStop || m_waitingQueryVoices) + { + if (m_waitingStop) + { + m_waitingStop = false; + m_state = psIdle; + // kdDebug() << "FestivalIntProc::slotProcessExited: emitting stopped signal" << endl; + emit stopped(); + } + if (m_waitingQueryVoices) + { + // kdDebug() << "FestivalIntProc::slotProcessExited: canceling queryVoices operation" << endl; + m_waitingQueryVoices = false; + m_state = psIdle; + } + } else { + if (m_state != psIdle) m_state = psFinished; + if (prevState == psSaying) + { + // kdDebug() << "FestivalIntProc::slotProcessExited: emitting sayFinished signal" << endl; + emit sayFinished(); + } else + if (prevState == psSynthing) + { + // kdDebug() << "FestivalIntProc::slotProcessExited: emitting synthFinished signal" << endl; + emit synthFinished(); + } + } + delete m_festProc; + m_festProc = 0; + m_outputQueue.clear(); +} + +void FestivalIntProc::slotReceivedStdout(KProcess*, char* buffer, int buflen) +{ + QString buf = QString::fromLatin1(buffer, buflen); + // kdDebug() << "FestivalIntProc::slotReceivedStdout: Received from Festival: " << buf << endl; + bool promptSeen = (buf.contains("festival>") > 0); + bool emitQueryVoicesFinished = false; + QStringList voiceCodesList; + if (m_waitingQueryVoices && m_outputQueue.isEmpty()) + { + // Look for opening ( and closing ). + buf.simplifyWhiteSpace(); + if (buf.left(3) == "nil") { + emitQueryVoicesFinished = true; + m_waitingQueryVoices = false; + } else { + if (buf.left(1) == "(") + { + int rightParen = buf.find(')'); + if (rightParen > 0) + { + m_waitingQueryVoices = false; + // Extract contents between parens. + buf = buf.mid(1, rightParen - 1); + // Space separated list. + voiceCodesList = QStringList::split(" ", buf, false); + emitQueryVoicesFinished = true; + } + } + } + } + if (promptSeen) + { + // kdDebug() << "FestivalIntProc::slotReceivedStdout: Prompt seen" << endl; + m_ready = true; + if (!sendIfReady()) + { + // kdDebug() << "FestivalIntProc::slotReceivedStdout: All output sent. " << endl; + pluginState prevState = m_state; + if (m_state != psIdle) m_state = psFinished; + if (prevState == psSaying) + { + // kdDebug() << "FestivalIntProc::slotReceivedStdout: emitting sayFinished signal" << endl; + emit sayFinished(); + } else + if (prevState == psSynthing) + { + if (m_waitingStop) + { + m_waitingStop = false; + m_state = psIdle; + // kdDebug() << "FestivalIntProc::slotReceivedStdout: emitting optimized stopped signal" << endl; + emit stopped(); + } + else + { + // kdDebug() << "FestivalIntProc::slotReceivedStdout: emitting synthFinished signal" << endl; + emit synthFinished(); + } + } + } + } + if (emitQueryVoicesFinished) + { + // kdDebug() << "FestivalIntProc::slotReceivedStdout: emitting queryVoicesFinished" << endl; + m_supportsSSML = (voiceCodesList.contains("rab_diphone")) ? ssYes : ssNo; + emit queryVoicesFinished(voiceCodesList); + } +} + +void FestivalIntProc::slotReceivedStderr(KProcess*, char* buffer, int buflen) +{ + QString buf = QString::fromLatin1(buffer, buflen); + kdDebug() << "FestivalIntProc::slotReceivedStderr: Received error from Festival: " << buf << endl; +} + +void FestivalIntProc::slotWroteStdin(KProcess* /*proc*/) +{ + // kdDebug() << "FestivalIntProc::slotWroteStdin: Running" << endl; + m_writingStdin = false; + if (!sendIfReady()) + { + // kdDebug() << "FestivalIntProc::slotWroteStdin: all output sent" << endl; + pluginState prevState = m_state; + if (m_state != psIdle) m_state = psFinished; + if (prevState == psSaying) + { + // kdDebug() << "FestivalIntProc::slotWroteStdin: emitting sayFinished signal" << endl; + emit sayFinished(); + } else + if (prevState == psSynthing) + { + // kdDebug() << "FestivalIntProc::slotWroteStdin: emitting synthFinished signal" << endl; + emit synthFinished(); + } + } +} + + +bool FestivalIntProc::isReady() { return m_ready; } + +/** +* Return the current state of the plugin. +* This function only makes sense in asynchronous mode. +* @return The pluginState of the plugin. +* +* @see pluginState +*/ +pluginState FestivalIntProc::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 FestivalIntProc::ackFinished() +{ + if (m_state == psFinished) + { + m_state = psIdle; + m_synthFilename = 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 FestivalIntProc::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 FestivalIntProc::supportsSynth() { return true; } + +/** +* Returns the name of an XSLT stylesheet that will convert a valid SSML file +* into a format that can be processed by the synth. For example, +* The Festival plugin returns a stylesheet that will convert SSML into +* SABLE. Any tags the synth cannot handle should be stripped (leaving +* their text contents though). The default stylesheet strips all +* tags and converts the file to plain text. +* @return Name of the XSLT file. +*/ +QString FestivalIntProc::getSsmlXsltFilename() +{ + if (m_supportsSSML == ssYes) + return KGlobal::dirs()->resourceDirs("data").last() + "kttsd/festivalint/xslt/SSMLtoSable.xsl"; + else + return PlugInProc::getSsmlXsltFilename(); +} + diff --git a/kttsd/plugins/festivalint/festivalintproc.h b/kttsd/plugins/festivalint/festivalintproc.h new file mode 100644 index 0000000..a69642c --- /dev/null +++ b/kttsd/plugins/festivalint/festivalintproc.h @@ -0,0 +1,361 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Main speaking functions for the Festival (Interactive) Plug in + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#ifndef _FESTIVALINTPROC_H_ +#define _FESTIVALINTPROC_H_ + +#include <qstringlist.h> +#include <qmutex.h> + +#include <kprocess.h> + +#include <pluginproc.h> + +class QTextCodec; + +class FestivalIntProc : public PlugInProc{ + Q_OBJECT + + public: + enum SupportsSSML { + ssUnknown, + ssYes, + ssNo + }; + + /** + * Constructor + */ + FestivalIntProc( QObject* parent = 0, const char* name = 0, const QStringList &args = QStringList()); + + /** + * Destructor + */ + virtual ~FestivalIntProc(); + + /** + * Initializate the speech engine. + * @param config Settings object. + * @param configGroup Settings group. + */ + virtual bool init(KConfig *config, const QString &configGroup); + + /** + * Returns true when festival is ready to speak a sentence. + */ + bool isReady(); + + /** + * Say a text string. + * @param text The text to speak. + */ + virtual void sayText(const QString &text); + + /** + * 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 + * and emit @ref synthFinished signal when synthesis is completed. + * It must also implement the @ref getState method, which must return + * psFinished, when synthesis is completed. + */ + virtual void synthText(const QString &text, const QString &suggestedFilename); + + /** + * Get the generated audio filename from call to @ref synthText. + * @return Name of the audio file the plugin generated. + * Null if no such file. + * + * The plugin must not re-use or delete the filename. The file may not + * be locked when this method is called. The file will be deleted when + * KTTSD is finished using it. + */ + virtual QString getFilename(); + + /** + * 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. + */ + virtual void stopText(); + + /** + * Return the current state of the plugin. + * This function only makes sense in asynchronous mode. + * @return The pluginState of the plugin. + * + * @see pluginState + */ + virtual pluginState getState(); + + /** + * 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. + */ + virtual void ackFinished(); + + /** + * 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. + */ + virtual bool supportsAsync(); + + /** + * 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. + * + * If the plugin returns True, it must also implement the following methods: + * - @ref synthText + * - @ref getFilename + * - @ref ackFinished + * + * If the plugin returns True, it need not implement @ref sayText . + */ + virtual bool supportsSynth(); + + /** + * Say or Synthesize text with the given voice code. + * @param festivalExePath Path to the Festival executable, or just "festival". + * @param text The text to be synthesized. + * @param suggestedFilename If not Null, synthesize only to this filename, otherwise + * synthesize and audibilize the text. + * @param voiceCode Voice code. + * @param time Speed percentage. 50 to 200. 200% = 2x normal. + * @param pitch Pitch persentage. 50 to 200. + * @param volume Volume percentage. 50 to 200. + * @param languageCode Language code, for example, "en". + */ + void synth(const QString &festivalExePath, const QString &text, + const QString &synthFilename, const QString& voiceCode, + int time, int pitch, int volume, const QString &languageCode, + QTextCodec* codec); + + /** + * Sends commands to Festival to query for a list of supported voice codes. + * Fires queryVoicesFinished when completed. + * @return False if busy doing something else and therefore cannot + * do the query. + */ + bool queryVoices(const QString &festivalExePath); + + /** + * Returns the name of an XSLT stylesheet that will convert a valid SSML file + * into a format that can be processed by the synth. For example, + * The Festival plugin returns a stylesheet that will convert SSML into + * SABLE. Any tags the synth cannot handle should be stripped (leaving + * their text contents though). The default stylesheet strips all + * tags and converts the file to plain text. + * @return Name of the XSLT file. + */ + QString getSsmlXsltFilename(); + + /** + * Whether Festival supports SSML or not. + * 0 = Unknown + * 1 = Yes + * 2 = No + */ + SupportsSSML supportsSSML() { return m_supportsSSML; } + + signals: + /** + * This signal fires upon completion of a queryVoices operation. + * The list of voice codes do not have "voice_" prefix. + */ + void queryVoicesFinished(const QStringList &voiceCodes); + + private slots: + void slotProcessExited(KProcess* proc); + void slotReceivedStdout(KProcess* proc, char* buffer, int buflen); + void slotReceivedStderr(KProcess* proc, char* buffer, int buflen); + void slotWroteStdin(KProcess* proc); + + private: + /** + * Start Festival engine. + * @param festivalExePath Path to the Festival executable, or just "festival". + * @param voiceCode Voice code in which to speak text. + * @param languageCode Language code, for example, "en". + */ + void startEngine(const QString &festivalExePath, const QString &voiceCode, + const QString &languageCode, QTextCodec* codec); + + /** + * If ready for more output, sends the given text to Festival process, otherwise, + * puts it in the queue. + * @param text Text to send or queue. + */ + void sendToFestival(const QString& text); + + /** + * If Festival is ready for more input and there is more output to send, send it. + * To be ready for more input, the Stdin buffer must be empty and the "festival>" + * prompt must have been received (m_ready = true). + * @return False when Festival is ready for more input + * but there is nothing to be sent, or if Festival + * has exited. + */ + bool sendIfReady(); + + /** + * Determine if the text has SABLE tags. If so, we will have to use a different + * synthesis method. + */ + bool isSable(const QString &text); + + /** + * We attempt to shorten sentences longer than this by replacing commas with periods. + */ + static const int c_tooLong = 600; + + /** + * Path to the Festival executable. + */ + QString m_festivalExePath; + + /** + * Selected voice (from config). + */ + QString m_voiceCode; + + /** + * True if the voice is preloaded. Also used as a flag to supress killing + * Festival, since startup time will be excessive. + */ + bool m_preload; + + /** + * Selected speed (from config). + */ + int m_time; + + /** + * Selected pitch (frequency) (from config). + */ + int m_pitch; + + /** + * Selected volume (from config). + */ + int m_volume; + + /** + * Running voice. + */ + QString m_runningVoiceCode; + + /** + * Running time (speed). + */ + int m_runningTime; + + /** + * Running pitch (frequency). + */ + int m_runningPitch; + + /** + * Festival process + */ + KProcess* m_festProc; + + /** + * Synthesis filename. + */ + QString m_synthFilename; + + /** + * True when festival is ready for another input. + */ + volatile bool m_ready; + + /** + * Plugin state. + */ + pluginState m_state; + + /** + * True when stopText has been called. Used to force transition to psIdle when + * Festival exits. + */ + bool m_waitingStop; + + /** + * True when queryVoices has been called. + */ + bool m_waitingQueryVoices; + + /** + * A queue of outputs to be sent to the Festival process. + * Since Festival requires us to wait until the "festival>" prompt before + * sending the next command, this queue allows us to queue up multiple + * commands and send each one when the ReceivedStdOut signal fires. + */ + QStringList m_outputQueue; + + bool m_writingStdin; + + /** + * Language code. + */ + QString m_languageCode; + + /** + * Codec. + */ + QTextCodec* m_codec; + + /** + * Flag if SSML is supported. Festival cannot support SABLE (and therefore SSML) + * unless rab_diphone (British male) is installed. Gawd, I hope Festival folks fix this! + */ + SupportsSSML m_supportsSSML; +}; + +#endif // _FESTIVALINTPROC_H_ diff --git a/kttsd/plugins/festivalint/kttsd_festivalintplugin.desktop b/kttsd/plugins/festivalint/kttsd_festivalintplugin.desktop new file mode 100644 index 0000000..e8776da --- /dev/null +++ b/kttsd/plugins/festivalint/kttsd_festivalintplugin.desktop @@ -0,0 +1,72 @@ +[Desktop Entry] +Name=Festival Interactive +Name[da]=Festival Interaktiv +Name[el]=Διαδραστικό Festival +Name[eu]=Festival interaktiboa +Name[fa]=Festival تعاملی +Name[fi]=Interaktiivinen Festival +Name[ga]=Festival Idirghníomhach +Name[gl]=Festival Interactivo +Name[is]=Festival gagnvirkni +Name[it]=Festival Interattivo +Name[ja]=Festival インタラクティブ +Name[ka]=Festival ინტერაქტიული +Name[km]=អន្តរមុខងារ +Name[mk]=Интерактивен festival +Name[ms]=Interaktif Perayaan +Name[ne]=अन्तरक्रियात्मक फेस्टिभल +Name[pa]=ਫੈਸੀਟੇਵਲ ਇੰਟਰੇਕਟਵ +Name[pl]=Festival interaktywny +Name[pt_BR]=Festival Interativo +Name[sv]=Festival interaktiv +Name[ta]=பெஸ்டிவல் உள்செயல் +Name[tg]=Фестивали Интерактив +Name[tr]=Etkileşimli Festival +Name[vi]=Tương tác Festival +Comment=Festival speech synthetizer +Comment[bg]=Синтезатор на глас Festival +Comment[ca]=Sintetitzador de veu Festival +Comment[cs]=Hlasový syntetizér Festival +Comment[da]=Festival tale-synthetizer +Comment[de]=Festival Sprachsynthesizer +Comment[el]=Συνθέτης ομιλίας Festival +Comment[es]=Sintetizador de texto a voz Festival +Comment[et]=Kõnesüntesaator Festival +Comment[eu]=Festival hizketa-sintetizadorea +Comment[fa]=ترکیبدهندۀ گفتار Festival +Comment[fi]=Festival puhesyntetisaattori +Comment[fr]=Synthèse vocale Festival +Comment[ga]=Sintéiseoir cainte Festival +Comment[gl]=Sistetizador de voces Festival +Comment[hu]=Festival beszédszintetizátor +Comment[is]=Festival talgerfill +Comment[it]=Sintetizzatore vocale Festival +Comment[ja]=Festival スピーチシンセサイザ +Comment[ka]=Festival ხმის სინთეზატორი +Comment[km]= មុខងារកម្មវិធីសង្គ្រោះការនិយាយ +Comment[mk]=Синтетизатор за говор festival +Comment[ms]=Pensintesis tutur perayaan +Comment[nb]=Festival talesyntetisering +Comment[nds]=Blicksnuut Festival +Comment[ne]=संवाद सिन्थेसाइजर फेस्टिभल +Comment[nl]=Festival spraaksynthetizer +Comment[pa]=ਫੈਸੀਟੇਵਲ ਬੋਲੀ ਸੰਸਲੇਸ਼ਕ +Comment[pl]=Syntezator mowy Festival +Comment[pt]=O sintetizador de fala Festival +Comment[pt_BR]=Sistema de Sintetizador de Fala Festival +Comment[ru]=Синтезатор речи Festival +Comment[sk]=Syntetizátor reči Festival +Comment[sl]=Sintetizator govora Festival +Comment[sr]=Синтетизатор говора Festival +Comment[sr@Latn]=Sintetizator govora Festival +Comment[sv]=Festival talsyntes +Comment[ta]=பெஸ்டிவல் பேச்சு கூட்டிணைப்பான் +Comment[tg]=Таҳлилгари овози Festival +Comment[tr]=Festival konuşma bireştirici +Comment[uk]=Синтезатор мовлення Festival +Comment[vi]=Trình tổng hợp tiếng nói Festival +Comment[zh_TW]=Festival 語音合成器 +Type=Service +ServiceTypes=KTTSD/SynthPlugin +X-KDE-Library=libkttsd_festivalintplugin +X-KDE-Languages=en,en_US,en_GB,en_CA,es,es_mx,cy,de,fi,cs,pl,ru,it,sw,zu,fr_CA,vi diff --git a/kttsd/plugins/festivalint/sabletowave.scm b/kttsd/plugins/festivalint/sabletowave.scm new file mode 100644 index 0000000..71293b4 --- /dev/null +++ b/kttsd/plugins/festivalint/sabletowave.scm @@ -0,0 +1,92 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-*-mode:scheme-*- +;; ;; +;; Centre for Speech Technology Research ;; +;; University of Edinburgh, UK ;; +;; Copyright (c) 1996,1997 ;; +;; All Rights Reserved. ;; +;; ;; +;; Permission is hereby granted, free of charge, to use and distribute ;; +;; this software and its documentation without restriction, including ;; +;; without limitation the rights to use, copy, modify, merge, publish, ;; +;; distribute, sublicense, and/or sell copies of this work, and to ;; +;; permit persons to whom this work is furnished to do so, subject to ;; +;; the following conditions: ;; +;; 1. The code must retain the above copyright notice, this list of ;; +;; conditions and the following disclaimer. ;; +;; 2. Any modifications must be clearly marked as such. ;; +;; 3. Original authors' names are not deleted. ;; +;; 4. The authors' names are not used to endorse or promote products ;; +;; derived from this software without specific prior written ;; +;; permission. ;; +;; ;; +;; THE UNIVERSITY OF EDINBURGH AND THE CONTRIBUTORS TO THIS WORK ;; +;; DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ;; +;; ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT ;; +;; SHALL THE UNIVERSITY OF EDINBURGH NOR THE CONTRIBUTORS BE LIABLE ;; +;; FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES ;; +;; WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN ;; +;; AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ;; +;; ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF ;; +;; THIS SOFTWARE. ;; +;; ;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Author: Alan W Black +;;; Date: November 1997 +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Adapted from text2wave by Alan Black. Original copyright listed above. +;; +;; Copyright 2004 by Gary Cramblitt <garycramblitt@comcast.net> +;; +;; This scheme module is used by the Festival Interactive plugin, +;; which is part of KTTSD. To use, +;; (load sabletowave.scm) +;; after starting Festival interactively, then to synth text containing +;; SABLE tags to a single wave file. +;; (ktts_sabletowave "sable text" "filename" volume) +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;; List of generated intermediate wave files. +(defvar ktts_wavefiles nil) + +(define (ktts_save_record_wave utt) +"Saves the waveform and records its so it can be joined into a +a single waveform at the end." + (let ((fn (make_tmp_filename))) + (utt.save.wave utt fn) + (set! ktts_wavefiles (cons fn ktts_wavefiles)) + utt)) + +(define (ktts_combine_waves outfile volume) + "Join all the waves together into the desired output file +and delete the intermediate ones." + (let ((wholeutt (utt.synth (Utterance Text "")))) + (mapcar + (lambda (d) + (utt.import.wave wholeutt d t) + (delete-file d)) + (reverse ktts_wavefiles)) +;; (if ktts_frequency +;; (utt.wave.resample wholeutt (parse-number ktts_frequency))) + (if (not (equal? volume "1.0")) + (begin + (utt.wave.rescale wholeutt (parse-number volume)))) + (utt.save.wave wholeutt outfile 'riff) + )) + +;;; +;;; Redefine what happens to utterances during text to speech. +;;; Synthesize each utterance and save to a temporary wave file. +;;; +(set! tts_hooks (list utt.synth ktts_save_record_wave)) + +(define (ktts_sabletowave text filename volume) + (set! ktts_wavefiles nil) + ;; Do the synthesis, which creates multiple wave files. + (tts_text text 'sable) + ;; Now put the waveforms together and adjust volume. + (ktts_combine_waves filename volume) +) diff --git a/kttsd/plugins/festivalint/voices b/kttsd/plugins/festivalint/voices new file mode 100644 index 0000000..f6c0d58 --- /dev/null +++ b/kttsd/plugins/festivalint/voices @@ -0,0 +1,738 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<voices> + +<voice> + <code>kal_diphone</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>American Male</name> +</voice> + +<voice> + <code>ked_diphone</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>American Male</name> +</voice> + +<voice> + <code>us1_mbrola</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>American Female, MBROLA</name> +</voice> + +<voice> + <code>us2_mbrola</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>American Male, MBROLA</name> +</voice> + +<voice> + <code>us3_mbrola</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>American Male, MBROLA</name> +</voice> + +<voice> + <code>don_diphone</code> + <language>en_GB</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>British Male</name> +</voice> + +<voice> + <code>rab_diphone</code> + <language>en_GB</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>British Male</name> +</voice> + +<voice> + <code>el_diphone</code> + <language>es</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Castilian Spanish Male</name> +</voice> + +<voice> + <code>cmu_us_bdl_arctic_hts</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>American Male, HTS</name> +</voice> + +<voice> + <code>cmu_us_slt_arctic_hts</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>American Female, HTS</name> +</voice> + +<voice> + <code>cmu_us_jmk_arctic_hts</code> + <language>en_CA</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Canadian English Male, HTS</name> +</voice> + +<voice> + <code>cmu_us_awb_arctic_hts</code> + <language>en_GB</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Scottish Male, HTS</name> +</voice> + +<voice> + <code>cmu_us_kal_com_hts</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>American Male, HTS</name> +</voice> + +<voice> + <code>cstr_us_ked_timit_hts</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>American Male, HTS</name> +</voice> + +<voice> + <code>cstr_us_jmk_arctic_multisyn</code> + <language>en_CA</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>true</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>false</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Canadian English Male, MultiSyn</name> +</voice> + +<voice> + <code>cstr_us_awb_arctic_multisyn</code> + <language>en_GB</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>true</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>false</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Scottish Male, MultiSyn</name> +</voice> + +<voice> + <code>german_de1_os</code> + <language>de</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>German Female, Festival</name> +</voice> + +<voice> + <code>german_de2_os</code> + <language>de</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>German Male, Festival</name> +</voice> + +<voice> + <code>german_de3_os</code> + <language>de</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>German Female, Festival</name> +</voice> + +<voice> + <code>abc_diphone</code> + <language>es</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>Mexican Spanish Male, OGC</name> +</voice> + +<voice> + <code>hvs_diphone</code> + <language>es</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>Mexican Spanish Female, OGC</name> +</voice> + +<voice> + <code>mwm_diphone</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>American Male, OGC</name> +</voice> + +<voice> + <code>aec_diphone</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>American Male, OGC</name> +</voice> + +<voice> + <code>jph_diphone</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>American Male, OGC</name> +</voice> + +<voice> + <code>tll_diphone</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>American Female, OGC</name> +</voice> + +<voice> + <code>ogirab_diphone</code> + <language>en_UK</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>British Male, OGC</name> +</voice> + +<voice> + <code>mwm2jph_diphone</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>American Male, OGC</name> +</voice> + +<voice> + <code>mwm2tll_diphone</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>American Female, OGC</name> +</voice> + +<voice> + <code>hy_fi_mv_diphone</code> + <language>fi</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Finnish Male</name> +</voice> + +<voice> + <code>czech_mbrola_cz2</code> + <language>cs_CZ</language> + <codec>ISO 8859-2</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Czech Male, MBROLA</name> +</voice> + +<voice> + <code>cstr_pl_em_diphone</code> + <language>pl</language> + <codec>ISO 8859-2</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>true</pitch-adjustable> + <name>Polish Male</name> +</voice> + +<voice> + <code>msu_ru_nsh_diphone</code> + <language>ru</language> + <codec>KOI8-R</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Russian Male</name> +</voice> + +<voice> + <code>pc_diphone</code> + <language>it</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Italian Male</name> +</voice> + +<voice> + <code>lp_diphone</code> + <language>it</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Italian Female</name> +</voice> + +<voice> + <code>lp_mbrola</code> + <language>it</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Italian Female</name> +</voice> + +<voice> + <code>pc_mbrola</code> + <language>it</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Italian Male</name> +</voice> + +<voice> + <code>pc_ogi_diphone</code> + <language>it</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Italian Male</name> +</voice> + +<voice> + <code>lp_ogi_diphone</code> + <language>it</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Italian Female</name> +</voice> + +<voice> + <code>UON_swahili_kw_multisyn</code> + <language>sw</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>true</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>false</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Kiswahili Male</name> +</voice> + +<voice> + <code>uyo_ibibio_eno_multisyn</code> + <!-- There is no iso 639 (two-letter) code for ibibio. We use Zulu here. + The iso 639-2 (three-letter) code is nic (Niger-Kordofanian) --> + <language>zu</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>true</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>false</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Ibibio Female</name> +</voice> + +<voice> + <code>csir_isizulu_buhle_multisyn</code> + <language>zu</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>true</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>false</rate-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Zulu Male</name> +</voice> + +<voice> + <code>cepstral_david</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>American Male</name> +</voice> + +<voice> + <code>cepstral_diane</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>American Female</name> +</voice> + +<voice> + <code>cepstral_william</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>American Male</name> +</voice> + +<voice> + <code>cepstral_amy</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>American Female</name> +</voice> + +<voice> + <code>cepstral_frank</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>American Male</name> +</voice> + +<voice> + <code>cepstral_emily</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>American Female</name> +</voice> + +<voice> + <code>cepstral_duncan</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>American Male</name> +</voice> + +<voice> + <code>cepstral_linda</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>American Female</name> +</voice> + +<voice> + <code>cepstral_robin</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>American Female</name> +</voice> + +<voice> + <code>cepstral_walter</code> + <language>en_US</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>American Male</name> +</voice> + +<voice> + <code>cepstral_lawrence</code> + <language>en_UK</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>British Male</name> +</voice> + +<voice> + <code>cepstral_millie</code> + <language>en_UK</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>British Female</name> +</voice> + +<voice> + <code>cepstral_jean-pierre</code> + <language>fr_CA</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>French Canadian Male</name> +</voice> + +<voice> + <code>cepstral_isabelle</code> + <language>fr_CA</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>French Canadian Female</name> +</voice> + +<voice> + <code>cepstral_matthias</code> + <language>de</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>German Male</name> +</voice> + +<voice> + <code>cepstral_katrin</code> + <language>de</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>German Female</name> +</voice> + +<voice> + <code>cepstral_miguel</code> + <language>es</language> + <codec>ISO 8859-1</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>Americas Spanish Male</name> +</voice> + +<voice> + <code>cepstral_marta</code> + <language>es</language> + <codec>ISO 8859-1</codec> + <gender>female</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <rate-adjustable>true</rate-adjustable> + <pitch-adjustable>false/</pitch-adjustable> + <name>Americas Spanish Female</name> +</voice> + +<voice> + <code>wow_vi_ptn_diphone</code> + <language>vi</language> + <codec>UTF-8</codec> + <gender>male</gender> + <preload>false</preload> + <volume-adjustable>true</volume-adjustable> + <speed-adjustable>true></speed-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Vietnamese Male</name> +</voice> + +<voice> + <code>wow_vi_liz_diphone</code> + <language>vi</language> + <codec>UTF-8</codec> + <gender>female</gender> + <volume-adjustable>true</volume-adjustable> + <speed-adjustable>true</speed-adjustable> + <pitch-adjustable>false</pitch-adjustable> + <name>Vietnamese Female</name> +</voice> + +</voices> diff --git a/kttsd/plugins/flite/Makefile.am b/kttsd/plugins/flite/Makefile.am new file mode 100644 index 0000000..c451be3 --- /dev/null +++ b/kttsd/plugins/flite/Makefile.am @@ -0,0 +1,20 @@ +INCLUDES = \ + -I$(top_srcdir)/kttsd/libkttsd -I$(top_builddir)/kttsd/libkttsd \ + $(all_includes) + +METASOURCES = AUTO + +kde_module_LTLIBRARIES = libkttsd_fliteplugin.la + +libkttsd_fliteplugin_la_SOURCES = \ + fliteconfwidget.ui \ + fliteconf.cpp \ + fliteproc.cpp \ + fliteplugin.cpp +libkttsd_fliteplugin_la_LDFLAGS = $(KDE_PLUGIN) $(all_libraries) +libkttsd_fliteplugin_la_LIBADD = $(top_builddir)/kttsd/libkttsd/libkttsd.la + +services_DATA = kttsd_fliteplugin.desktop +servicesdir = $(kde_servicesdir) + +noinst_HEADERS = fliteproc.h fliteconf.h fliteconfwidget.h diff --git a/kttsd/plugins/flite/README b/kttsd/plugins/flite/README new file mode 100644 index 0000000..1bff55b --- /dev/null +++ b/kttsd/plugins/flite/README @@ -0,0 +1,3 @@ +This is the directory containing the Festival Lite plug in. +This plug in is developed and maintained by Gary Cramblitt. +<garycramblitt@comcast.net> diff --git a/kttsd/plugins/flite/configure.in.bot b/kttsd/plugins/flite/configure.in.bot new file mode 100644 index 0000000..54bd84e --- /dev/null +++ b/kttsd/plugins/flite/configure.in.bot @@ -0,0 +1,15 @@ +if test "x$flite_bindir" = "xno"; then + if test "$compile_flite_plugin" = "yes"; then + echo "" + echo "=======================================================" + echo "The Festival Lite (flite) program does not appear to be" + echo "installed on this system. The Flite plugin will" + echo "be built, but you need to install Flite before you" + echo "can use it. You can get it at" + echo " http://www.speech.cs.cmu.edu/flite/index.html" + echo "Debian users: apt-get install flite" + echo "======================================================" + all_tests=bad + fi +fi + diff --git a/kttsd/plugins/flite/configure.in.in b/kttsd/plugins/flite/configure.in.in new file mode 100644 index 0000000..f9b222f --- /dev/null +++ b/kttsd/plugins/flite/configure.in.in @@ -0,0 +1,22 @@ +dnl ========================================= +dnl checks for Festival Lite (Flite) Plug In +dnl ========================================= + +AC_ARG_ENABLE(kttsd-flite, + AC_HELP_STRING([--enable-kttsd-flite], + [build KTTSD Festival Lite (flite) [default=yes]]), + flite_plugin=$enableval, + flite_plugin=yes) + +compile_flite_plugin="yes" + +if test "x$flite_plugin" = "xno"; then + compile_flite_plugin="no" +fi + +dnl Check for flite executable. +dnl Note that flite plugin is always built, unless +dnl user overrides on configure command line. +AC_PATH_PROG(flite_bindir, "flite", "no") + +AM_CONDITIONAL(include_kttsd_flite, test "x$compile_flite_plugin" != "xno") diff --git a/kttsd/plugins/flite/fliteconf.cpp b/kttsd/plugins/flite/fliteconf.cpp new file mode 100644 index 0000000..cbc4b2d --- /dev/null +++ b/kttsd/plugins/flite/fliteconf.cpp @@ -0,0 +1,199 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Configuration widget and functions for Festival (Interactive) plug in + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +// Qt includes. +#include <qlayout.h> +#include <qfile.h> +#include <qapplication.h> + +// KDE includes. +#include <klocale.h> +#include <kdialog.h> +#include <ktempfile.h> +#include <kstandarddirs.h> +#include <kprogress.h> + +// KTTS includes. +#include <testplayer.h> + +// Flite Plugin includes. +#include "fliteproc.h" +#include "fliteconf.h" +#include "fliteconf.moc" + +/** Constructor */ +FliteConf::FliteConf( QWidget* parent, const char* name, const QStringList& /*args*/) : + PlugInConf(parent, name) +{ + // kdDebug() << "FliteConf::FliteConf: Running" << endl; + m_fliteProc = 0; + m_progressDlg = 0; + + QVBoxLayout *layout = new QVBoxLayout(this, KDialog::marginHint(), + KDialog::spacingHint(), "FliteConfigWidgetLayout"); + layout->setAlignment (Qt::AlignTop); + m_widget = new FliteConfWidget(this, "FliteConfigWidget"); + layout->addWidget(m_widget); + + defaults(); + + connect(m_widget->flitePath, SIGNAL(textChanged(const QString&)), + this, SLOT(configChanged())); + connect(m_widget->fliteTest, SIGNAL(clicked()), this, SLOT(slotFliteTest_clicked())); +} + +/** Destructor */ +FliteConf::~FliteConf(){ + // kdDebug() << "Running: FliteConf::~FliteConf()" << endl; + if (!m_waveFile.isNull()) QFile::remove(m_waveFile); + delete m_fliteProc; + delete m_progressDlg; +} + +void FliteConf::load(KConfig *config, const QString &configGroup){ + // kdDebug() << "FliteConf::load: Loading configuration for language " << langGroup << " with plug in " << "Festival Lite (flite)" << endl; + + config->setGroup(configGroup); + QString fliteExe = config->readEntry("FliteExePath", QString::null); + if (fliteExe.isEmpty()) + { + config->setGroup("Flite"); + fliteExe = config->readEntry("FliteExePath", "flite"); + } + m_widget->flitePath->setURL(fliteExe); +} + +void FliteConf::save(KConfig *config, const QString &configGroup){ + // kdDebug() << "FliteConf::save: Saving configuration for language " << langGroup << " with plug in " << "Festival Lite (flite)" << endl; + + config->setGroup("Flite"); + config->writeEntry("FliteExePath", + realFilePath(m_widget->flitePath->url())); + config->setGroup(configGroup); + config->writeEntry("FliteExePath", + realFilePath(m_widget->flitePath->url())); +} + +void FliteConf::defaults(){ + // kdDebug() << "FliteConf::defaults: Running" << endl; + m_widget->flitePath->setURL("flite"); +} + +void FliteConf::setDesiredLanguage(const QString &lang) +{ + m_languageCode = lang; +} + +QString FliteConf::getTalkerCode() +{ + QString fliteExe = realFilePath(m_widget->flitePath->url()); + if (!fliteExe.isEmpty()) + { + if (!getLocation(fliteExe).isEmpty()) + { + return QString( + "<voice lang=\"%1\" name=\"%2\" gender=\"%3\" />" + "<prosody volume=\"%4\" rate=\"%5\" />" + "<kttsd synthesizer=\"%6\" />") + .arg(m_languageCode) + .arg("fixed") + .arg("neutral") + .arg("medium") + .arg("medium") + .arg("Festival Lite (flite)"); + } + } + return QString::null; +} + +void FliteConf::slotFliteTest_clicked() +{ + // kdDebug() << "FliteConf::slotFliteTest_clicked(): Running" << endl; + // If currently synthesizing, stop it. + if (m_fliteProc) + m_fliteProc->stopText(); + else + { + m_fliteProc = new FliteProc(); + connect (m_fliteProc, SIGNAL(stopped()), this, SLOT(slotSynthStopped())); + } + // Create a temp file name for the wave file. + KTempFile tempFile (locateLocal("tmp", "fliteplugin-"), ".wav"); + QString tmpWaveFile = tempFile.file()->name(); + tempFile.close(); + + // Get test message in the language of the voice. + QString testMsg = testMessage(m_languageCode); + + // Tell user to wait. + m_progressDlg = new KProgressDialog(m_widget, "kttsmgr_flite_testdlg", + i18n("Testing"), + i18n("Testing."), + true); + m_progressDlg->progressBar()->hide(); + m_progressDlg->setAllowCancel(true); + + // Play an English test. Flite only supports English. + connect (m_fliteProc, SIGNAL(synthFinished()), this, SLOT(slotSynthFinished())); + m_fliteProc->synth( + testMsg, + tmpWaveFile, + realFilePath(m_widget->flitePath->url())); + + // Display progress dialog modally. Processing continues when plugin signals synthFinished, + // or if user clicks Cancel button. + m_progressDlg->exec(); + disconnect (m_fliteProc, SIGNAL(synthFinished()), this, SLOT(slotSynthFinished())); + if (m_progressDlg->wasCancelled()) m_fliteProc->stopText(); + delete m_progressDlg; + m_progressDlg = 0; +} + +void FliteConf::slotSynthFinished() +{ + // If user canceled, progress dialog is gone, so exit. + if (!m_progressDlg) + { + m_fliteProc->ackFinished(); + return; + } + // Hide the Cancel button so user can't cancel in the middle of playback. + m_progressDlg->showCancelButton(false); + // Get new wavefile name. + m_waveFile = m_fliteProc->getFilename(); + // Tell synth we're done. + m_fliteProc->ackFinished(); + // Play the wave file (possibly adjusting its Speed). + // Player object deletes the wave file when done. + if (m_player) m_player->play(m_waveFile); + QFile::remove(m_waveFile); + m_waveFile = QString::null; + if (m_progressDlg) m_progressDlg->close(); +} + +void FliteConf::slotSynthStopped() +{ + // Clean up after canceling test. + QString filename = m_fliteProc->getFilename(); + if (!filename.isNull()) QFile::remove(filename); +} diff --git a/kttsd/plugins/flite/fliteconf.h b/kttsd/plugins/flite/fliteconf.h new file mode 100644 index 0000000..aa26c7b --- /dev/null +++ b/kttsd/plugins/flite/fliteconf.h @@ -0,0 +1,124 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Configuration widget and functions for Festival (Interactive) plug in + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#ifndef _FLITECONF_H_ +#define _FLITECONF_H_ + +// Qt includes. +#include <qstring.h> + +// KDE includes. +#include <kconfig.h> +#include <kdebug.h> + +// KTTS includes. +#include <pluginconf.h> + +// Flite plugin includes. +#include "fliteconfwidget.h" + +class FliteProc; +class KProgressDialog; + +class FliteConf : public PlugInConf { + Q_OBJECT + + public: + /** Constructor */ + FliteConf( QWidget* parent = 0, const char* name = 0, const QStringList &args = QStringList()); + + /** Destructor */ + ~FliteConf(); + + /** This method is invoked whenever the module should read its + * configuration (most of the times from a config file) and update the + * user interface. This happens when the user clicks the "Reset" button in + * the control center, to undo all of his changes and restore the currently + * valid settings. NOTE that this is not called after the modules is loaded, + * so you probably want to call this method in the constructor. + */ + void load(KConfig *config, const QString &configGroup); + + /** This function gets called when the user wants to save the settings in + * the user interface, updating the config files or wherever the + * configuration is stored. The method is called when the user clicks "Apply" + * or "Ok". + */ + void save(KConfig *config, const QString &configGroup); + + /** This function is called to set the settings in the module to sensible + * default values. It gets called when hitting the "Default" button. The + * default values should probably be the same as the ones the application + * uses when started without a config file. + */ + void defaults(); + + /** + * This function informs the plugin of the desired language to be spoken + * by the plugin. The plugin should attempt to adapt itself to the + * specified language code, choosing sensible defaults if necessary. + * If the passed-in code is QString::null, no specific language has + * been chosen. + * @param lang The desired language code or Null if none. + * + * If the plugin is unable to support the desired language, that is OK. + * Language codes are given by ISO 639-1 and are in lowercase. + * The code may also include an ISO 3166 country code in uppercase + * separated from the language code by underscore (_). For + * example, en_GB. If your plugin supports the given language, but + * not the given country, treat it as though the country + * code were not specified, i.e., adapt to the given language. + */ + void setDesiredLanguage(const QString &lang); + + /** + * Return fully-specified talker code for the configured plugin. This code + * uniquely identifies the configured instance of the plugin and distinquishes + * one instance from another. If the plugin has not been fully configured, + * i.e., cannot yet synthesize, return QString::null. + * @return Fully-specified talker code. + */ + QString getTalkerCode(); + + private slots: + void configChanged(){ + // kdDebug() << "FliteConf::configChanged: Running" << endl; + emit changed(true); + }; + void slotFliteTest_clicked(); + void slotSynthFinished(); + void slotSynthStopped(); + + private: + // Language code. + QString m_languageCode; + // Configuration Widget. + FliteConfWidget* m_widget; + // Flite synthesizer. + FliteProc* m_fliteProc; + // Synthesized wave file name. + QString m_waveFile; + // Progress dialog. + KProgressDialog* m_progressDlg; +}; +#endif // _FLITECONF_H_ diff --git a/kttsd/plugins/flite/fliteconfwidget.ui b/kttsd/plugins/flite/fliteconfwidget.ui new file mode 100644 index 0000000..839970e --- /dev/null +++ b/kttsd/plugins/flite/fliteconfwidget.ui @@ -0,0 +1,186 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>FliteConfWidget</class> +<author>Gary Cramblitt</author> +<widget class="QWidget"> + <property name="name"> + <cstring>FliteConfWidget</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>559</width> + <height>233</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>7</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="caption"> + <string>Flite Config UI</string> + </property> + <property name="whatsThis" stdset="0"> + <string>This is the configuration dialog for the Festival Lite (Flite) speech synthesis engine.</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <spacer row="1" column="0"> + <property name="name"> + <cstring>spacer1</cstring> + </property> + <property name="orientation"> + <enum>Vertical</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>20</width> + <height>100</height> + </size> + </property> + </spacer> + <widget class="QGroupBox" row="0" column="0"> + <property name="name"> + <cstring>fliteConfigurationBox</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>GroupBoxPanel</enum> + </property> + <property name="frameShadow"> + <enum>Sunken</enum> + </property> + <property name="title"> + <string>Festival &Lite (flite) Configuration</string> + </property> + <property name="whatsThis" stdset="0"> + <string>This is the configuration dialog for the Festival Lite (Flite) speech synthesis engine.</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>11</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QLayoutWidget" row="0" column="0" rowspan="1" colspan="2"> + <property name="name"> + <cstring>flitePathBox</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>flitePathLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&Flite executable path:</string> + </property> + <property name="alignment"> + <set>AlignVCenter|AlignRight</set> + </property> + <property name="buddy" stdset="0"> + <cstring>flitePath</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>If Flite is in your PATH environment variable, simply enter "flite", otherwise specify the complete path to the Flite executable program.</string> + </property> + </widget> + <widget class="KURLRequester"> + <property name="name"> + <cstring>flitePath</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="url" stdset="0"> + <string>flite</string> + </property> + <property name="whatsThis" stdset="0"> + <string>If Flite is in your PATH environment variable, simply enter "flite", otherwise specify the complete path to the Flite executable program.</string> + </property> + </widget> + </hbox> + </widget> + <widget class="QPushButton" row="1" column="1"> + <property name="name"> + <cstring>fliteTest</cstring> + </property> + <property name="text"> + <string>&Test</string> + </property> + </widget> + <spacer row="1" column="0"> + <property name="name"> + <cstring>spacer2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>221</width> + <height>20</height> + </size> + </property> + </spacer> + </grid> + </widget> + </grid> +</widget> +<customwidgets> +</customwidgets> +<includes> + <include location="global" impldecl="in declaration">kurlrequester.h</include> + <include location="global" impldecl="in implementation">kurlrequester.h</include> +</includes> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>kurlrequester.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> +</includehints> +</UI> diff --git a/kttsd/plugins/flite/fliteplugin.cpp b/kttsd/plugins/flite/fliteplugin.cpp new file mode 100644 index 0000000..602b5bb --- /dev/null +++ b/kttsd/plugins/flite/fliteplugin.cpp @@ -0,0 +1,31 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Generating the factories so festival lite (flite) can be used as plug in. + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#include <kgenericfactory.h> + +#include "fliteconf.h" +#include "fliteproc.h" + +typedef K_TYPELIST_2( FliteProc, FliteConf ) Flite; +K_EXPORT_COMPONENT_FACTORY( libkttsd_fliteplugin, KGenericFactory<Flite>("kttsd_flite") ) + diff --git a/kttsd/plugins/flite/fliteproc.cpp b/kttsd/plugins/flite/fliteproc.cpp new file mode 100644 index 0000000..54b15e3 --- /dev/null +++ b/kttsd/plugins/flite/fliteproc.cpp @@ -0,0 +1,281 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Main speaking functions for the Festival Lite (Flite) Plug in + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +// Qt includes. +#include <qstring.h> +#include <qstringlist.h> + +// KDE includes. +#include <kdebug.h> +#include <kconfig.h> +#include <kstandarddirs.h> +#include <kprocess.h> + +// Flite Plugin includes. +#include "fliteproc.h" +#include "fliteproc.moc" + +/** Constructor */ +FliteProc::FliteProc( QObject* parent, const char* name, const QStringList& ) : + PlugInProc( parent, name ){ + kdDebug() << "FliteProc::FliteProc: Running" << endl; + m_state = psIdle; + m_waitingStop = false; + m_fliteProc = 0; +} + +/** Destructor */ +FliteProc::~FliteProc(){ + kdDebug() << "FliteProc::~FliteProc:: Running" << endl; + if (m_fliteProc) + { + stopText(); + delete m_fliteProc; + } +} + +/** Initialize the speech */ +bool FliteProc::init(KConfig* config, const QString& configGroup){ + // kdDebug() << "Running: FliteProc::init(const QString &lang)" << endl; + // kdDebug() << "Initializing plug in: Flite" << endl; + // Retrieve path to flite executable. + config->setGroup(configGroup); + m_fliteExePath = config->readEntry("FliteExePath", "flite"); + kdDebug() << "FliteProc::init: path to flite: " << m_fliteExePath << 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 FliteProc::sayText(const QString &text) +{ + synth(text, QString::null, m_fliteExePath); +} + +/** +* 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 FliteProc::synthText(const QString& text, const QString& suggestedFilename) +{ + synth(text, suggestedFilename, m_fliteExePath); +} + +/** +* Say or Synthesize text. +* @param text The text to be synthesized. +* @param suggestedFilename If not Null, synthesize only to this filename, otherwise +* synthesize and audibilize the text. +*/ +void FliteProc::synth( + const QString &text, + const QString &synthFilename, + const QString& fliteExePath) +{ + // kdDebug() << "Running: FliteProc::synth(const QString &text)" << endl; + + if (m_fliteProc) + { + if (m_fliteProc->isRunning()) m_fliteProc->kill(); + delete m_fliteProc; + m_fliteProc = 0; + } + // kdDebug()<< "FliteProc::synth: Creating Flite object" << endl; + m_fliteProc = new KProcess; + connect(m_fliteProc, SIGNAL(processExited(KProcess*)), + this, SLOT(slotProcessExited(KProcess*))); + connect(m_fliteProc, SIGNAL(receivedStdout(KProcess*, char*, int)), + this, SLOT(slotReceivedStdout(KProcess*, char*, int))); + connect(m_fliteProc, SIGNAL(receivedStderr(KProcess*, char*, int)), + this, SLOT(slotReceivedStderr(KProcess*, char*, int))); + connect(m_fliteProc, SIGNAL(wroteStdin(KProcess*)), + this, SLOT(slotWroteStdin(KProcess* ))); + if (synthFilename.isNull()) + m_state = psSaying; + else + m_state = psSynthing; + + + // Encode quotation characters. + QString saidText = text; +/* + saidText.replace("\\\"", "#!#!"); + saidText.replace("\"", "\\\""); + saidText.replace("#!#!", "\\\""); + // Remove certain comment characters. + saidText.replace("--", ""); + saidText = "\"" + saidText + "\""; +*/ + saidText += "\n"; + + *m_fliteProc << fliteExePath; +// *m_fliteProc << "-t" << saidText; + if (!synthFilename.isNull()) *m_fliteProc << "-o" << synthFilename; + + // Ok, let's rock. + m_synthFilename = synthFilename; + kdDebug() << "FliteProc::synth: Synthing text: '" << saidText << "' using Flite plug in" << endl; + if (!m_fliteProc->start(KProcess::NotifyOnExit, KProcess::All)) + { + kdDebug() << "FliteProc::synth: Error starting Flite process. Is flite in the PATH?" << endl; + m_state = psIdle; + return; + } + kdDebug()<< "FliteProc:synth: Flite initialized" << endl; + m_fliteProc->writeStdin(saidText.latin1(), saidText.length()); +} + +/** +* 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 FliteProc::getFilename() +{ + kdDebug() << "FliteProc::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 FliteProc::stopText(){ + kdDebug() << "FliteProc::stopText:: Running" << endl; + if (m_fliteProc) + { + if (m_fliteProc->isRunning()) + { + kdDebug() << "FliteProc::stopText: killing Flite." << endl; + m_waitingStop = true; + m_fliteProc->kill(); + } else m_state = psIdle; + }else m_state = psIdle; + kdDebug() << "FliteProc::stopText: Flite stopped." << endl; +} + +void FliteProc::slotProcessExited(KProcess*) +{ + kdDebug() << "FliteProc:slotProcessExited: Flite 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 FliteProc::slotReceivedStdout(KProcess*, char* buffer, int buflen) +{ + QString buf = QString::fromLatin1(buffer, buflen); + kdDebug() << "FliteProc::slotReceivedStdout: Received output from Flite: " << buf << endl; +} + +void FliteProc::slotReceivedStderr(KProcess*, char* buffer, int buflen) +{ + QString buf = QString::fromLatin1(buffer, buflen); + kdDebug() << "FliteProc::slotReceivedStderr: Received error from Flite: " << buf << endl; +} + +void FliteProc::slotWroteStdin(KProcess*) +{ + kdDebug() << "FliteProc::slotWroteStdin: closing Stdin" << endl; + m_fliteProc->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 FliteProc::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 FliteProc::ackFinished() +{ + if (m_state == psFinished) + { + m_state = psIdle; + m_synthFilename = 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 FliteProc::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 FliteProc::supportsSynth() { return true; } + diff --git a/kttsd/plugins/flite/fliteproc.h b/kttsd/plugins/flite/fliteproc.h new file mode 100644 index 0000000..7737b3a --- /dev/null +++ b/kttsd/plugins/flite/fliteproc.h @@ -0,0 +1,187 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Main speaking functions for the Festival Lite (Flite) Plug in + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: 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; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#ifndef _FLITEPROC_H_ +#define _FLITEPROC_H_ + +// Qt includes. +#include <qstringlist.h> +#include <qmutex.h> + +// KTTS includes. +#include <pluginproc.h> + +class KProcess; + +class FliteProc : public PlugInProc{ + Q_OBJECT + + public: + /** + * Constructor + */ + FliteProc( QObject* parent = 0, const char* name = 0, const QStringList &args = QStringList()); + + /** + * Destructor + */ + virtual ~FliteProc(); + + /** + * Initializate the speech engine. + * @param config Settings object. + * @param configGroup Settings Group. + */ + virtual bool init(KConfig *config, const QString &configGroup); + + /** + * Say a text string. + * @param text The text to speak. + */ + virtual void sayText(const QString &text); + + /** + * 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. + */ + virtual void synthText(const QString& text, const QString& suggestedFilename); + + /** + * 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. + */ + virtual QString getFilename(); + + /** + * 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. + */ + virtual void stopText(); + + /** + * Return the current state of the plugin. + * This function only makes sense in asynchronous mode. + * @return The pluginState of the plugin. + * + * @see pluginState + */ + virtual pluginState getState(); + + /** + * 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. + */ + virtual void ackFinished(); + + /** + * 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. + */ + virtual bool supportsAsync(); + + /** + * 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. + */ + virtual bool supportsSynth(); + + /** + * Say or Synthesize text. + * @param text The text to be synthesized. + * @param synthFilename If not Null, synthesize only to this filename, otherwise + * synthesize and audibilize the text. + * @param fliteExePath Path to the flite executable. + */ + void synth( + const QString &text, + const QString &synthFilename, + const QString &fliteExePath); + + private slots: + void slotProcessExited(KProcess* proc); + void slotReceivedStdout(KProcess* proc, char* buffer, int buflen); + void slotReceivedStderr(KProcess* proc, char* buffer, int buflen); + void slotWroteStdin(KProcess* proc); + + private: + + /** + * Path to flite executable (from config). + */ + QString m_fliteExePath; + + /** + * Flite process + */ + KProcess* m_fliteProc; + + /** + * Synthesis filename. + */ + QString m_synthFilename; + + /** + * Plugin state. + */ + pluginState m_state; + + /** + * True when stopText has been called. Used to force transition to psIdle when + * Flite exits. + */ + bool m_waitingStop; + +}; + +#endif // _FLITEPROC_H_ diff --git a/kttsd/plugins/flite/kttsd_fliteplugin.desktop b/kttsd/plugins/flite/kttsd_fliteplugin.desktop new file mode 100644 index 0000000..2105253 --- /dev/null +++ b/kttsd/plugins/flite/kttsd_fliteplugin.desktop @@ -0,0 +1,59 @@ +[Desktop Entry] +Name=Festival Lite (flite) +Name[el]=Festival ελαφρύ (flite) +Name[fi]=Festival Lite (kevytversio) +Name[ka]=Festival მსუბუქი (flite) +Name[km]=មុខងារ Lite (flite) +Name[nds]=Festival Lite (FLite) +Name[ne]=फेस्टिभल लाइट (एफ लाईट) +Name[pt_BR]=Festival Lite +Name[ta]=பெஸ்டிவல் லைட் (flite) +Name[tr]=Festival Lite(flite) +Name[zh_TW]=Festival Lite (flite)4 +Comment=Festival Lite (flite) speech synthesizer +Comment[bg]=Олекотен синтезатор на глас Festival Lite +Comment[ca]=Sintetitzador de veu Festival Lite (flite) +Comment[cs]=Hlasový syntetizér Festival Lite (flite) +Comment[da]=Festival Lite (flite) tale-synthesizer +Comment[de]=Festival Lite (flite) Sprachsynthesizer +Comment[el]=Συνθέτης ομιλίας Festival ελαφρύ (flite) +Comment[es]=Sintetizador de texto a voz Festival (flite) +Comment[et]=Kõnesüntesaator Festival lite (flite) +Comment[eu]=Festival Lite (flite) hizketa-sintetizadorea +Comment[fa]=ترکیبدهندۀ گفتار Festival Lite (flite) +Comment[fi]=Festival Lite (kevytversio) puhesyntetisaattori +Comment[fr]=Synthèse vocale Festival Lite (flite) +Comment[ga]=Sintéiseoir cainte Festival Lite (flite) +Comment[gl]=Sintetizador de voces Festival Lite (flite) +Comment[hu]=Festival Lite (flite) beszédszintetizátor +Comment[is]=Festival Lite (flite) talgerfill +Comment[it]=Sintetizzatore vocale Festival Lite (flite) +Comment[ja]=Festival Lite (flite) スピーチシンセサイザ +Comment[ka]=Festival მარტივი (flite) ხმის სინთეზატორი +Comment[km]=មុខងារកម្មវិធីសង្គ្រោះការនិយាយ Lite (flite) +Comment[mk]=Festival Lite (flite) синтетизатор на говор +Comment[ms]=Pensintesis tutur Festival Lite (flite) +Comment[nb]=Festival Lite (flite) talesyntetisering +Comment[nds]=Blicksnuut Festival lite (FLite) +Comment[ne]=फेस्टिभल लाइट (एफ लाइट) संवाद सिन्थेसाइजर +Comment[nl]=Festival Lite (flite) spraaksynthesizer +Comment[pa]=Festival Lite (flite) ਬੋਲੀ ਸੰਸਲੇਸ਼ਕ +Comment[pl]=Syntezator mowy Festival Lite (flite) +Comment[pt]=O sintetizador de fala Festival Lite (flite) +Comment[pt_BR]=Sintetizador de fala Festival Lite (leve) +Comment[ru]=Синтезатор речи Festival Lite (flite) +Comment[sk]=Syntetizátor reči Festival Lite (flite) +Comment[sl]=Sintetizator govora Festival Lite (flite) +Comment[sr]=Синтетизатор говора Festival Lite (flite) +Comment[sr@Latn]=Sintetizator govora Festival Lite (flite) +Comment[sv]=Festival Lite (flite) talsyntes +Comment[ta]=பெஸ்டிவல் லைட் (flite) பேச்சு கூட்டிணைப்பான் +Comment[tg]=Таҳлилгари овози Festival Lite (flite) +Comment[tr]=Festival Lite(flite) konuşma bireştirici +Comment[uk]=Синтезатор мовлення Festival Lite (flite) +Comment[vi]=Trình tổng hợp tiếng nói Festival Lite (flite) +Comment[zh_TW]=Festival Lite (flite) 語音合成器 +Type=Service +ServiceTypes=KTTSD/SynthPlugin +X-KDE-Library=libkttsd_fliteplugin +X-KDE-Languages=en,en_US,en_GB diff --git a/kttsd/plugins/freetts/Makefile.am b/kttsd/plugins/freetts/Makefile.am new file mode 100644 index 0000000..be9312b --- /dev/null +++ b/kttsd/plugins/freetts/Makefile.am @@ -0,0 +1,18 @@ +INCLUDES = \ + -I$(top_srcdir)/kttsd/libkttsd -I$(top_builddir)/kttsd/libkttsd \ + $(all_includes) + +METASOURCES = AUTO + +kde_module_LTLIBRARIES = libkttsd_freettsplugin.la + +libkttsd_freettsplugin_la_SOURCES = \ + freettsconfigwidget.ui \ + freettsconf.cpp \ + freettsproc.cpp \ + freettsplugin.cpp +libkttsd_freettsplugin_la_LDFLAGS = $(KDE_PLUGIN) $(all_libraries) +libkttsd_freettsplugin_la_LIBADD = $(top_builddir)/kttsd/libkttsd/libkttsd.la + +services_DATA = kttsd_freettsplugin.desktop +servicesdir = $(kde_servicesdir) diff --git a/kttsd/plugins/freetts/README b/kttsd/plugins/freetts/README new file mode 100644 index 0000000..54d9dc2 --- /dev/null +++ b/kttsd/plugins/freetts/README @@ -0,0 +1,6 @@ +This is the directory containing the FreeTTS plug in. +If you intend to edit the GUI (freettsconfig.ui) you will have to first +copy kttsd/libkttsd/pluginconf.h to a standard include directory +like $KDEDIR/include/ since the interface has been tweaked to derive +from PlugInConf instead of QWidget and Qt Designer refuses to open it +if the header is not properly place. diff --git a/kttsd/plugins/freetts/configure.in.bot b/kttsd/plugins/freetts/configure.in.bot new file mode 100644 index 0000000..b3985b9 --- /dev/null +++ b/kttsd/plugins/freetts/configure.in.bot @@ -0,0 +1,15 @@ +if test "x$freetts_bindir" = "xno"; then + if test "$compile_freetts_plugin" = "yes"; then + echo "" + echo "======================================================" + echo "The freetts.jar Java archive has not been found on" + echo "this system. The FreeTTS plugin will be built, " + echo "but you must install FreeTTS it before you can use it," + echo "or if it already installed you must set the path to" + echo "it in the KDE Text-to-Speech Manager." + echo "FreeTTS is available from freetts.sourceforge.net" + echo "=====================================================" + all_tests=bad + fi +fi + diff --git a/kttsd/plugins/freetts/configure.in.in b/kttsd/plugins/freetts/configure.in.in new file mode 100644 index 0000000..78555fc --- /dev/null +++ b/kttsd/plugins/freetts/configure.in.in @@ -0,0 +1,22 @@ +dnl ================ +dnl checks for FreeTTS +dnl ================ + +AC_ARG_ENABLE(kttsd-freetts, + AC_HELP_STRING([--enable-kttsd-freetts], + [build KTTSD FreeTTS Plugin [default=yes]]), + freetts_plugin=$enableval, + freetts_plugin=yes) + +compile_freetts_plugin="yes" + +if test "x$freetts_plugin" = "xno"; then + compile_freetts_plugin="no" +fi + +dnl Check for festival executable. +dnl Note that Festival Interactive plugin is always built +dnl whether binary is found or not, unless user overrides with -disable-freetts. +AC_PATH_PROG(freetts_bindir, "freetts.jar", "no") + +AM_CONDITIONAL(include_kttsd_freetts, test "x$compile_freetts_plugin" = "xyes") diff --git a/kttsd/plugins/freetts/freettsconf.cpp b/kttsd/plugins/freetts/freettsconf.cpp new file mode 100644 index 0000000..701af5e --- /dev/null +++ b/kttsd/plugins/freetts/freettsconf.cpp @@ -0,0 +1,230 @@ +/**************************************************************************** + Configuration widget and functions for FreeTTS (interactive) plug in + ------------------- + Copyright : (C) 2004 Paul Giannaros + ------------------- + Original author: Paul Giannaros <ceruleanblaze@gmail.com> + Current Maintainer: Paul Giannaros <ceruleanblaze@gmail.com> + ******************************************************************************/ + +/*************************************************************************** + * * + * 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 <qlayout.h> +#include <qlabel.h> +#include <qstring.h> +#include <qstringlist.h> +#include <qfile.h> +#include <qapplication.h> + +// KDE includes. +#include <kdialog.h> +#include <ktempfile.h> +#include <kstandarddirs.h> +#include <kmessagebox.h> +#include <klocale.h> +#include <kprogress.h> + +// KTTS includes. +#include <pluginconf.h> +#include <testplayer.h> + +// FreeTTS includes. +#include "freettsconf.h" +#include "freettsconfigwidget.h" + +/** Constructor */ +FreeTTSConf::FreeTTSConf( QWidget* parent, const char* name, const QStringList&/*args*/) : + PlugInConf( parent, name ) { + + // kdDebug() << "FreeTTSConf::FreeTTSConf: Running" << endl; + m_freettsProc = 0; + m_progressDlg = 0; + + QVBoxLayout *layout = new QVBoxLayout(this, KDialog::marginHint(), + KDialog::spacingHint(), "FreeTTSConfigWidgetLayout"); + layout->setAlignment (Qt::AlignTop); + m_widget = new FreeTTSConfWidget(this, "FreeTTSConfigWidget"); + layout->addWidget(m_widget); + + defaults(); + + connect(m_widget->freettsPath, SIGNAL(textChanged(const QString&)), + this, SLOT(configChanged())); + connect(m_widget->freettsTest, SIGNAL(clicked()), this, SLOT(slotFreeTTSTest_clicked())); +} + +/** Destructor */ +FreeTTSConf::~FreeTTSConf() { + // kdDebug() << "Running: FreeTTSConf::~FreeTTSConf()" << endl; + if (!m_waveFile.isNull()) QFile::remove(m_waveFile); + delete m_freettsProc; + delete m_progressDlg; +} + +void FreeTTSConf::load(KConfig *config, const QString &configGroup) { + // kdDebug() << "FreeTTSConf::load: Running" << endl; + + config->setGroup(configGroup); + QString freeTTSJar = config->readEntry("FreeTTSJarPath", QString::null); + if (freeTTSJar.isEmpty()) + { + config->setGroup("FreeTTS"); + freeTTSJar = config->readEntry("FreeTTSJarPath", QString::null); + } + if (freeTTSJar.isEmpty()) + freeTTSJar = getLocation("freetts.jar"); + m_widget->freettsPath->setURL(freeTTSJar); + /// If freettsPath is still empty, then we couldn't find the file in the path. +} + +void FreeTTSConf::save(KConfig *config, const QString &configGroup){ + // kdDebug() << "FreeTTSConf::save: Running" << endl; + + config->setGroup("FreeTTS"); + config->writeEntry("FreeTTSJarPath", + realFilePath(m_widget->freettsPath->url())); + + config->setGroup(configGroup); + if(m_widget->freettsPath->url().isEmpty()) + KMessageBox::sorry(0, i18n("Unable to locate freetts.jar in your path.\nPlease specify the path to freetts.jar in the Properties tab before using KDE Text-to-Speech"), i18n("KDE Text-to-Speech")); + config->writeEntry("FreeTTSJarPath", + realFilePath(m_widget->freettsPath->url())); +} + +void FreeTTSConf::defaults(){ + // kdDebug() << "Running: FreeTTSConf::defaults()" << endl; + m_widget->freettsPath->setURL(""); +} + +void FreeTTSConf::setDesiredLanguage(const QString &lang) +{ + m_languageCode = lang; +} + +QString FreeTTSConf::getTalkerCode() +{ + QString freeTTSJar = realFilePath(m_widget->freettsPath->url()); + if (!freeTTSJar.isEmpty()) + { + if (!getLocation(freeTTSJar).isEmpty()) + { + return QString( + "<voice lang=\"%1\" name=\"%2\" gender=\"%3\" />" + "<prosody volume=\"%4\" rate=\"%5\" />" + "<kttsd synthesizer=\"%6\" />") + .arg(m_languageCode) + .arg("fixed") + .arg("neutral") + .arg("medium") + .arg("medium") + .arg("FreeTTS"); + } + } + return QString::null; +} + +// QString FreeTTSConf::getLocation(const QString &name) { +// /// Iterate over the path and see if 'name' exists in it. Return the +// /// full path to it if it does. Else return an empty QString. +// kdDebug() << "FreeTTSConf::getLocation: Searching for " << name << " in the path... " << endl; +// kdDebug() << m_path << endl; +// for(QStringList::iterator it = m_path.begin(); it != m_path.end(); ++it) { +// QString fullName = *it; +// fullName += "/"; +// fullName += name; +// /// The user either has the directory of the file in the path... +// if(QFile::exists(fullName)) { +// return fullName; +// kdDebug() << fullName << endl; +// } +// /// ....Or the file itself +// else if(QFileInfo(*it).baseName().append(QString(".").append(QFileInfo(*it).extension())) == name) { +// return fullName; +// kdDebug() << fullName << endl; +// } +// } +// return ""; +// } + + +void FreeTTSConf::slotFreeTTSTest_clicked() +{ + // kdDebug() << "FreeTTSConf::slotFreeTTSTest_clicked(): Running" << endl; + // If currently synthesizing, stop it. + if (m_freettsProc) + m_freettsProc->stopText(); + else + { + m_freettsProc = new FreeTTSProc(); + connect (m_freettsProc, SIGNAL(stopped()), this, SLOT(slotSynthStopped())); + } + // Create a temp file name for the wave file. + KTempFile tempFile (locateLocal("tmp", "freettsplugin-"), ".wav"); + QString tmpWaveFile = tempFile.file()->name(); + tempFile.close(); + + // Get test message in the language of the voice. + QString testMsg = testMessage(m_languageCode); + + // Tell user to wait. + m_progressDlg = new KProgressDialog(m_widget, "kttsmgr_freetts_testdlg", + i18n("Testing"), + i18n("Testing."), + true); + m_progressDlg->progressBar()->hide(); + m_progressDlg->setAllowCancel(true); + + // I think FreeTTS only officialy supports English, but if anyone knows of someone + // whos built up a different language lexicon and has it working with FreeTTS gimme an email at ceruleanblaze@gmail.com + connect (m_freettsProc, SIGNAL(synthFinished()), this, SLOT(slotSynthFinished())); + m_freettsProc->synth( + testMsg, + tmpWaveFile, + realFilePath(m_widget->freettsPath->url())); + + // Display progress dialog modally. Processing continues when plugin signals synthFinished, + // or if user clicks Cancel button. + m_progressDlg->exec(); + disconnect (m_freettsProc, SIGNAL(synthFinished()), this, SLOT(slotSynthFinished())); + if (m_progressDlg->wasCancelled()) m_freettsProc->stopText(); + delete m_progressDlg; + m_progressDlg = 0; +} + +void FreeTTSConf::slotSynthFinished() +{ + // If user canceled, progress dialog is gone, so exit. + if (!m_progressDlg) + { + m_freettsProc->ackFinished(); + return; + } + // Hide the Cancel button so user can't cancel in the middle of playback. + m_progressDlg->showCancelButton(false); + // Get new wavefile name. + m_waveFile = m_freettsProc->getFilename(); + // Tell synth we're done. + m_freettsProc->ackFinished(); + // Play the wave file (possibly adjusting its Speed). + // Player object deletes the wave file when done. + if (m_player) m_player->play(m_waveFile); + QFile::remove(m_waveFile); + m_waveFile = QString::null; + if (m_progressDlg) m_progressDlg->close(); +} + +void FreeTTSConf::slotSynthStopped() +{ + // Clean up after canceling test. + QString filename = m_freettsProc->getFilename(); + if (!filename.isNull()) QFile::remove(filename); +} + +#include "freettsconf.moc" diff --git a/kttsd/plugins/freetts/freettsconf.h b/kttsd/plugins/freetts/freettsconf.h new file mode 100644 index 0000000..9ab3ae8 --- /dev/null +++ b/kttsd/plugins/freetts/freettsconf.h @@ -0,0 +1,122 @@ +/**************************************************************************** + Configuration widget and functions for FreeTTS (interactive) plug in + ------------------- + Copyright : (C) 2004 Paul Giannaros + ------------------- + Original author: Paul Giannaros <ceruleanblaze@gmail.com> + Current Maintainer: Paul Giannaros <ceruleanblaze@gmail.com> + ******************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef _FREETTSCONF_H_ +#define _FREETTSCONF_H_ + +#include <qstringlist.h> + +#include <kdebug.h> +#include <kconfig.h> + +#include <pluginconf.h> + +#include "freettsconfigwidget.h" +#include "freettsproc.h" + +class QStringList; +class KProgressDialog; + +class FreeTTSConf : public PlugInConf { + Q_OBJECT + + public: + /** Constructor */ + FreeTTSConf( QWidget* parent = 0, const char* name = 0, const QStringList &args = QStringList()); + + /** Destructor */ + ~FreeTTSConf(); + + /** This method is invoked whenever the module should read its + configuration (most of the times from a config file) and update the + user interface. This happens when the user clicks the "Reset" button in + the control center, to undo all of his changes and restore the currently + valid settings. NOTE that this is not called after the modules is loaded, + so you probably want to call this method in the constructor.*/ + void load(KConfig *config, const QString &configGroup); + + /** This function gets called when the user wants to save the settings in + the user interface, updating the config files or wherever the + configuration is stored. The method is called when the user clicks "Apply" + or "Ok". */ + void save(KConfig *config, const QString &configGroup); + + /** This function is called to set the settings in the module to sensible + default values. It gets called when hitting the "Default" button. The + default values should probably be the same as the ones the application + uses when started without a config file. */ + void defaults(); + + /** + * This function informs the plugin of the desired language to be spoken + * by the plugin. The plugin should attempt to adapt itself to the + * specified language code, choosing sensible defaults if necessary. + * If the passed-in code is QString::null, no specific language has + * been chosen. + * @param lang The desired language code or Null if none. + * + * If the plugin is unable to support the desired language, that is OK. + * Language codes are given by ISO 639-1 and are in lowercase. + * The code may also include an ISO 3166 country code in uppercase + * separated from the language code by underscore (_). For + * example, en_GB. If your plugin supports the given language, but + * not the given country, treat it as though the country + * code were not specified, i.e., adapt to the given language. + */ + void setDesiredLanguage(const QString &lang); + + /** + * Return fully-specified talker code for the configured plugin. This code + * uniquely identifies the configured instance of the plugin and distinquishes + * one instance from another. If the plugin has not been fully configured, + * i.e., cannot yet synthesize, return QString::null. + * @return Fully-specified talker code. + */ + QString getTalkerCode(); + + /** + * Function searches the $PATH for a file. If it exists, it returns the full path + * to that file. If not, then it returns an empty QString. + * @param name The name of the file to search for. + * @returns The full path to the file or an empty QString. + */ + + private slots: + void configChanged(bool t = true) { + emit changed(t); + }; + void slotFreeTTSTest_clicked(); + void slotSynthFinished(); + void slotSynthStopped(); + + private: + /// Language code. + QString m_languageCode; + + /// Configuration Widget. + FreeTTSConfWidget *m_widget; + + /// FreeTTS synthesizer. + FreeTTSProc *m_freettsProc; + + /// Synthesized wave file name. + QString m_waveFile; + + /// Progress dialog. + KProgressDialog* m_progressDlg; +}; +#endif diff --git a/kttsd/plugins/freetts/freettsconfigwidget.ui b/kttsd/plugins/freetts/freettsconfigwidget.ui new file mode 100644 index 0000000..1d95751 --- /dev/null +++ b/kttsd/plugins/freetts/freettsconfigwidget.ui @@ -0,0 +1,200 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>FreeTTSConfWidget</class> +<author>Gary Cramblitt <garycramblitt@comcast.net></author> +<widget class="QWidget"> + <property name="name"> + <cstring>FreeTTSConfWidget</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>576</width> + <height>134</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>7</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="caption"> + <string>FreeTTS Config UI</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <spacer row="1" column="0"> + <property name="name"> + <cstring>spacer1</cstring> + </property> + <property name="orientation"> + <enum>Vertical</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>20</width> + <height>16</height> + </size> + </property> + </spacer> + <widget class="QGroupBox" row="0" column="0"> + <property name="name"> + <cstring>freettsConfigurationBox</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>GroupBoxPanel</enum> + </property> + <property name="frameShadow"> + <enum>Sunken</enum> + </property> + <property name="title"> + <string>FreeTTS Interactive Configuration</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>11</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QLayoutWidget" row="0" column="0"> + <property name="name"> + <cstring>voicesPathBox</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>freettsPathLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&FreeTTS jar file:</string> + </property> + <property name="textFormat"> + <enum>AutoText</enum> + </property> + <property name="alignment"> + <set>AlignVCenter|AlignRight</set> + </property> + <property name="buddy" stdset="0"> + <cstring>freettsPath</cstring> + </property> + </widget> + <widget class="KURLRequester"> + <property name="name"> + <cstring>freettsPath</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </hbox> + </widget> + <widget class="QLayoutWidget" row="1" column="0"> + <property name="name"> + <cstring>selectVoiceBox</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + </hbox> + </widget> + <widget class="QLayoutWidget" row="3" column="0"> + <property name="name"> + <cstring>layout10</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <spacer> + <property name="name"> + <cstring>spacer2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>410</width> + <height>20</height> + </size> + </property> + </spacer> + <widget class="QPushButton"> + <property name="name"> + <cstring>freettsTest</cstring> + </property> + <property name="text"> + <string>Test</string> + </property> + </widget> + </hbox> + </widget> + </grid> + </widget> + </grid> +</widget> +<customwidgets> +</customwidgets> +<includes> + <include location="global" impldecl="in declaration">kurlrequester.h</include> + <include location="global" impldecl="in implementation">kurlrequester.h</include> +</includes> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>kurlrequester.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> +</includehints> +</UI> diff --git a/kttsd/plugins/freetts/freettsplugin.cpp b/kttsd/plugins/freetts/freettsplugin.cpp new file mode 100644 index 0000000..c8a75df --- /dev/null +++ b/kttsd/plugins/freetts/freettsplugin.cpp @@ -0,0 +1,26 @@ +/**************************************************************************** + Factory generation for the FreeTTS plugin so it can actually be used + as such. + ------------------- + Copyright : (C) 2004 Paul Giannaros + ------------------- + Original author: Paul Giannaros <ceruleanblaze@gmail.com> + Current Maintainer: Paul Giannaros <ceruleanblaze@gmail.com> + ******************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <kgenericfactory.h> + +#include "freettsconf.h" +#include "freettsproc.h" + +typedef K_TYPELIST_2( FreeTTSProc, FreeTTSConf ) FreeTTS; +K_EXPORT_COMPONENT_FACTORY( libkttsd_freettsplugin, KGenericFactory<FreeTTS>("kttsd_freetts")) + diff --git a/kttsd/plugins/freetts/freettsproc.cpp b/kttsd/plugins/freetts/freettsproc.cpp new file mode 100644 index 0000000..829fee9 --- /dev/null +++ b/kttsd/plugins/freetts/freettsproc.cpp @@ -0,0 +1,275 @@ +/**************************************************************************** + Main speaking functions for the FreeTTS Plug in + ------------------- + Copyright : (C) 2004 Paul Giannaros + ------------------- + Original author: Paul Giannaros <ceruleanblaze@gmail.com> + Current Maintainer: Paul Giannaros <ceruleanblaze@gmail.com> + ******************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <qstring.h> +#include <qstringlist.h> +#include <qfileinfo.h> + +#include <kdebug.h> +#include <kconfig.h> +#include <kstandarddirs.h> +#include <kprocess.h> + +#include "freettsproc.h" + +/** Constructor */ +FreeTTSProc::FreeTTSProc( QObject* parent, const char* name, const QStringList& /*args*/) : + PlugInProc( parent, name ) { + kdDebug() << "Running: FreeTTSProc::FreeTTSProc" << endl; + m_state = psIdle; + m_waitingStop = false; + m_freettsProc = 0; +} + +/** Desctructor */ +FreeTTSProc::~FreeTTSProc() { + kdDebug() << "Running: FreeTTSProc::~FreeTTSProc" << endl; + if (m_freettsProc) { + stopText(); + delete m_freettsProc; + } +} + +/** Initializate the speech */ +bool FreeTTSProc::init(KConfig *config, const QString &configGroup) { + kdDebug() << "Running: FreeTTSProc::init()" << endl; + kdDebug() << "Initializing plug in: FreeTTS" << endl; + config->setGroup(configGroup); + m_freettsJarPath = config->readEntry("FreeTTSJarPath", "freetts.jar"); + kdDebug() << "FreeTTSProc::init: path to freetts.jar: " << m_freettsJarPath << 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 FreeTTSProc::sayText(const QString &text) { + synth(text, QString::null, m_freettsJarPath); +} + +/** + * 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 FreeTTSProc::synthText(const QString& text, const QString& suggestedFilename) { + kdDebug() << "Running: FreeTTSProc::synthText" << endl; + synth(text, suggestedFilename, m_freettsJarPath); +} + +// A little helper function because KDE 3.2 kdDebug() does not support QValueList<QCString>. +QStringList argsToQStringList(const QValueList<QCString> list) +{ + QStringList newList; + QValueList<QCString>::ConstIterator it = list.begin(); + for ( ; it != list.end(); ++it ) newList.append(*it); + return newList; +} + +void FreeTTSProc::synth( + const QString &text, + const QString &synthFilename, + const QString& freettsJarPath) { + + kdDebug() << "Running: FreeTTSProc::synth" << endl; + + if (m_freettsProc) { + if (m_freettsProc->isRunning()) m_freettsProc->kill(); + delete m_freettsProc; + m_freettsProc = 0; + + } + + m_freettsProc = new KProcess; + connect(m_freettsProc, SIGNAL(processExited(KProcess*)), + this, SLOT(slotProcessExited(KProcess*))); + connect(m_freettsProc, SIGNAL(receivedStdout(KProcess*, char*, int)), + this, SLOT(slotReceivedStdout(KProcess*, char*, int))); + connect(m_freettsProc, SIGNAL(receivedStderr(KProcess*, char*, int)), + this, SLOT(slotReceivedStderr(KProcess*, char*, int))); + connect(m_freettsProc, SIGNAL(wroteStdin(KProcess*)), + this, SLOT(slotWroteStdin(KProcess* ))); + if (synthFilename.isNull()) + m_state = psSaying; + else + m_state = psSynthing; + + + QString saidText = text; + saidText += "\n"; + + /// As freetts.jar doesn't seem to like being called from an absolute path, + /// we need to strip off the path to freetts.jar and pass it to + /// KProcess::setWorkingDirectory() + /// We could just strip off 11 characters from the end of the path to freetts.jar, but thats + /// not exactly very portable... + QString filename = QFileInfo(freettsJarPath).baseName().append(QString(".").append(QFileInfo(freettsJarPath).extension())); + QString freettsJarDir = freettsJarPath.left((freettsJarPath.length() - filename.length()) - 1); + + m_freettsProc->setWorkingDirectory(freettsJarDir); + kdDebug() << "FreeTTSProc::synthText: moved to directory '" << freettsJarDir << "'" << endl; + kdDebug() << "FreeTTSProc::synthText: Running file: '" << filename << "'" << endl; + *m_freettsProc << "java" << "-jar" << filename; + + /// Dump audio into synthFilename + + if (!synthFilename.isNull()) *m_freettsProc << "-dumpAudio" << synthFilename; + + m_synthFilename = synthFilename; + + kdDebug() << "FreeTTSProc::synth: Synthing text: '" << saidText << "' using FreeTTS plug in" << endl; + if (!m_freettsProc->start(KProcess::NotifyOnExit, KProcess::All)) { + kdDebug() << "FreeTTSProc::synth: Error starting FreeTTS process. Is freetts.jar in the PATH?" << endl; + m_state = psIdle; + kdDebug() << "KProcess args: " << argsToQStringList(m_freettsProc->args()) << endl; + return; + } + kdDebug()<< "FreeTTSProc:synth: FreeTTS initialized" << endl; + m_freettsProc->writeStdin(saidText.latin1(), saidText.length()); +} + +/** + * Returning the filename of the synth'd text + * @returns The filename of the last synth'd text + */ +QString FreeTTSProc::getFilename() { + kdDebug() << "FreeTTSProc::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 FreeTTSProc::stopText() { + kdDebug() << "FreeTTSProc::stopText:: Running" << endl; + if (m_freettsProc) { + if (m_freettsProc->isRunning()) { + kdDebug() << "FreeTTSProc::stopText: killing FreeTTS." << endl; + m_waitingStop = true; + m_freettsProc->kill(); + } + else m_state = psIdle; + } + else m_state = psIdle; + kdDebug() << "FreeTTSProc::stopText: FreeTTS stopped." << endl; +} + +void FreeTTSProc::slotProcessExited(KProcess*) { + kdDebug() << "FreeTTSProc:slotProcessExited: FreeTTS 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 FreeTTSProc::slotReceivedStdout(KProcess*, char* buffer, int buflen) { + QString buf = QString::fromLatin1(buffer, buflen); + kdDebug() << "FreeTTSProc::slotReceivedStdout: Received output from FreeTTS: " << buf << endl; +} + +void FreeTTSProc::slotReceivedStderr(KProcess*, char* buffer, int buflen) { + QString buf = QString::fromLatin1(buffer, buflen); + kdDebug() << "FreeTTSProc::slotReceivedStderr: Received error from FreeTTS: " << buf << endl; +} + +void FreeTTSProc::slotWroteStdin(KProcess*) { + kdDebug() << "FreeTTSProc::slotWroteStdin: closing Stdin" << endl; + m_freettsProc->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 FreeTTSProc::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 FreeTTSProc::ackFinished() { + if (m_state == psFinished) { + m_state = psIdle; + m_synthFilename = 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 FreeTTSProc::supportsAsync() { +// return true; + 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 FreeTTSProc::supportsSynth() { +// return true; + return true; +} + + +#include "freettsproc.moc" diff --git a/kttsd/plugins/freetts/freettsproc.h b/kttsd/plugins/freetts/freettsproc.h new file mode 100644 index 0000000..b2bb631 --- /dev/null +++ b/kttsd/plugins/freetts/freettsproc.h @@ -0,0 +1,180 @@ +/**************************************************************************** + Main speaking functions for the FreeTTS Plug in + ------------------- + Copyright : (C) 2004 Paul Giannaros + ------------------- + Original author: Paul Giannaros <ceruleanblaze@gmail.com> + Current Maintainer: Paul Giannaros <ceruleanblaze@gmail.com> + ******************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef _FREETTSPROC_H_ +#define _FREETTSPROC_H_ + + +#include <qstringlist.h> +#include <qmutex.h> + +#include <pluginproc.h> + +class KProcess; + +class FreeTTSProc : public PlugInProc{ + Q_OBJECT + +public: + /** + * Constructor + */ + FreeTTSProc( QObject* parent = 0, const char* name = 0, const QStringList &args = QStringList()); + + /** + * Destructor + */ + virtual ~FreeTTSProc(); + + /** + * Initializate the speech engine. + * @param config Settings object. + * @param configGroup Settings group. + */ + virtual bool init(KConfig *config, const QString &configGroup); + + /** + * Say a text string. + * @param text The text to speak. + */ + virtual void sayText(const QString &text); + + /** + * 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. + */ + virtual void synthText(const QString& text, const QString& suggestedFilename); + + /** + * 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. + */ + virtual QString getFilename(); + + /** + * 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. + */ + virtual void stopText(); + + /** + * Return the current state of the plugin. + * This function only makes sense in asynchronous mode. + * @return The pluginState of the plugin. + * + * @see pluginState + */ + virtual pluginState getState(); + + /** + * 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. + */ + virtual void ackFinished(); + + /** + * 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. + */ + virtual bool supportsAsync(); + + /** + * 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. + */ + virtual bool supportsSynth(); + + /** + * Say or Synthesize text. + * @param text The text to be synthesized. + * @param synthFilename If not Null, synthesize only to this filename, otherwise + * synthesize and audibilize the text. + * @param freettsJarPath Path to the freetts jar file. + */ + void synth( + const QString &text, + const QString &synthFilename, + const QString &freettsJarPath); + +private slots: + void slotProcessExited(KProcess* proc); + void slotReceivedStdout(KProcess* proc, char* buffer, int buflen); + void slotReceivedStderr(KProcess* proc, char* buffer, int buflen); + void slotWroteStdin(KProcess* proc); + +private: + + /** + * Path to FreeTTS jar file (from config). + */ + QString m_freettsJarPath; + + /** + * FreeTTS process + */ + KProcess* m_freettsProc; + + /** + * Synthesis filename. + */ + QString m_synthFilename; + + /** + * Plugin state. + */ + pluginState m_state; + + /** + * True when stopText has been called. Used to force transition to psIdle when + * FreeTTS exits. + */ + bool m_waitingStop; + +}; + +#endif // _FREETTSPROC_H_ diff --git a/kttsd/plugins/freetts/kttsd_freettsplugin.desktop b/kttsd/plugins/freetts/kttsd_freettsplugin.desktop new file mode 100644 index 0000000..cdfab63 --- /dev/null +++ b/kttsd/plugins/freetts/kttsd_freettsplugin.desktop @@ -0,0 +1,51 @@ +[Desktop Entry] +Name=FreeTTS +Name[tr]=Freetts +Name[vi]=Văn bản sang Tiếng nói Tự do +Comment=FreeTTS speech synthesizer +Comment[bg]=Синтезатор на глас FreeTTS +Comment[ca]=Sintetitzador de veu FreeTTS +Comment[cs]=Hlasový syntetizér FreeTTS +Comment[da]=FreeTTS tale-synthesizer +Comment[de]=FreeTTS Sprachsynthesizer +Comment[el]=Συνθέτης ομιλίας FreeTTS +Comment[es]=Sintetizador de texto a voz FreeTTS +Comment[et]=Kõnesüntesaator FreeTTS +Comment[eu]=FreeTTS hizketa-sintetizadorea +Comment[fa]=ترکیبدهندۀ گفتار FreeTTS +Comment[fi]=FreeTTS puhesyntetisaattori +Comment[fr]=Synthèse vocale FreeTTS +Comment[ga]=Sintéiseoir cainte FreeTTS +Comment[gl]=Sintetizador de voces FreeTTS +Comment[hu]=FreeTTS beszédszintetizátor +Comment[is]=FreeTTS talgerfill +Comment[it]=Sintetizzatore vocale FreeTTS +Comment[ja]=FreeTTS スピーチシンセサイザ +Comment[ka]=FreeTTS ხმის სინთეზატორი +Comment[km]=កម្មវិធីសង្គ្រោះការនិយាយ FreeTTS +Comment[mk]=FreeTTS синтетизатор на говор +Comment[ms]=Pensintesis tutur FreeTTS +Comment[nb]=FreeTTS talesyntetisering +Comment[nds]=Blicksnuut FreeTTS +Comment[ne]=FreeTTS संवाद सिन्थेसाइजर +Comment[nl]=FreeTTS spraaksynthesizer +Comment[pa]=FreeTTS ਬੋਲੀ ਸੰਸਲੇਸ਼ਕ +Comment[pl]=Syntezator mowy FreeTTS +Comment[pt]=O sintetizador de fala FreeTTS +Comment[pt_BR]=Sintetizador de fala FreeTTS +Comment[ru]=Синтезатор речи FreeTTS +Comment[sk]=Syntetizátor reči FreeTTS +Comment[sl]=Sintetizator govora FreeTTS +Comment[sr]=Синтетизатор говора FreeTTS +Comment[sr@Latn]=Sintetizator govora FreeTTS +Comment[sv]=FreeTTS talsyntes +Comment[ta]=FreeTTS பேச்சு கூட்டிணைப்பான் +Comment[tg]=Таҳлилгари овози FreeTTS +Comment[tr]=FreeTTS konuşma bireştirici +Comment[uk]=Синтезатор мовлення FreeTTS +Comment[vi]=Trình tổng hợp tiếng nói Tự do +Comment[zh_TW]=FreeTTS 語音合成器 +Type=Service +ServiceTypes=KTTSD/SynthPlugin +X-KDE-Library=libkttsd_freettsplugin +X-KDE-Languages=en,en_US,en_GB diff --git a/kttsd/plugins/hadifix/Makefile.am b/kttsd/plugins/hadifix/Makefile.am new file mode 100644 index 0000000..35ca445 --- /dev/null +++ b/kttsd/plugins/hadifix/Makefile.am @@ -0,0 +1,23 @@ +INCLUDES = \ + -I$(top_srcdir)/kttsd/libkttsd -I$(top_builddir)/kttsd/libkttsd \ + $(all_includes) + +METASOURCES = AUTO + +kde_module_LTLIBRARIES = libkttsd_hadifixplugin.la + +libkttsd_hadifixplugin_la_SOURCES = \ + hadifixconf.cpp \ + hadifixproc.cpp \ + hadifixplugin.cpp \ + hadifixconfigui.ui \ + voicefileui.ui +libkttsd_hadifixplugin_la_LDFLAGS = $(KDE_PLUGIN) $(all_libraries) +libkttsd_hadifixplugin_la_LIBADD = $(LIB_KDECORE) $(LIB_KFILE) $(top_builddir)/kttsd/libkttsd/libkttsd.la + +services_DATA = kttsd_hadifixplugin.desktop +servicesdir = $(kde_servicesdir) + +# Data files. +festivalintxsltdatadir = $(kde_datadir)/kttsd/hadifix/xslt/ +festivalintxsltdata_DATA = SSMLtoTxt2pho.xsl diff --git a/kttsd/plugins/hadifix/README b/kttsd/plugins/hadifix/README new file mode 100644 index 0000000..f41b967 --- /dev/null +++ b/kttsd/plugins/hadifix/README @@ -0,0 +1,3 @@ +This is the directory containing the Hadifix plug in. +This plug in is developed and maintained by Gunnar Schmi Dt. +Rework by Gary Cramblitt <garycramblitt@comcast.net> diff --git a/kttsd/plugins/hadifix/SSMLtoTxt2pho.xsl b/kttsd/plugins/hadifix/SSMLtoTxt2pho.xsl new file mode 100644 index 0000000..5a81c8f --- /dev/null +++ b/kttsd/plugins/hadifix/SSMLtoTxt2pho.xsl @@ -0,0 +1,164 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> +<xsl:output method="text" encoding="ISO-8859-1" indent="no"/> + +<!-- XSLT stylesheet to convert SSML into a format that can be processed by the + Hadifix txt2pho processor. + + (c) 2004 by Gary Cramblitt + + Original author: Gary Cramblitt <garycramblitt@comcast.net> + 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; either version 2 of the License, or * + (at your option) any later version. * + + The txt2pho processor permits special markup to be embedded in text to control + speech attributes such as speed (duration), pitch, etc. + The markup must be inside curly braces and separated from other text by a space. + Spaces within the markup are not permitted. + See the txt2pho README file for details. + + Something the README does not say is that {Pitch} markup applies only to one + sentence and reverts back to normal when the sentence is completed. + It means that we must repeatedly ouput {Pitch} markup + for each sentence in the text. For example, the SSML + + <prosody pitch="high">Sentence one. Sentence two.</prosody> Sentence three. + + must be converted to + + {Pitch:300} Sentence one. {Pitch:300} Sentence two. Sentence three. + + {Duration} markup, on the other hand, is addative and stays in effect until + changed. For example, the SSML + + <prosody rate="fast">Sentence one. Sentence two.</prosody> Sentence three. + + must be converted to + + {Duration:-0.5} Sentence one. Sentence two. {Duration:0.5} Sentence three. + + txt2pho will also stop processing when it sees a newline. Therefore, we must take + care to strip all newlines and avoid inserting any newlines. + --> + +<!-- Strip all elements and attributes from output. --> +<xsl:strip-space elements="*" /> + +<xsl:template match="speak"> + <xsl:apply-templates/> +</xsl:template> + +<!-- Handle markup that maintains state. --> +<xsl:template match="prosody"> + <!-- Rate (speed), Rates are addative and stay in effect until changed. --> + <!-- TODO: SSML permits nesting of prosody elements. As coded below, + <prosody rate="slow">One<prosody rate="slow">Two</prosody</prosody> + will pronounce "Two" doubly slow, which it should not do. --> + <xsl:choose> + <xsl:when test="@rate='fast'"> + <!-- Increase speed. --> + <xsl:value-of select="'{Duration:-0.5} '"/> + <!-- Continue processing. --> + <xsl:apply-templates/> + <!-- Decrease speed. --> + <xsl:value-of select="'{Duration:0.5} '"/> + </xsl:when> + <xsl:when test="@rate='slow'"> + <!-- Decrease speed. --> + <xsl:value-of select="'{Duration:0.5} '"/> + <!-- Continue processing. --> + <xsl:apply-templates/> + <!-- Increase speed. --> + <xsl:value-of select="'{Duration:-0.5} '"/> + </xsl:when> + <xsl:otherwise> + <xsl:apply-templates/> + </xsl:otherwise> + </xsl:choose> +</xsl:template> + +<!-- Outputs a sentence with txt2pho markup. + Called in the context of a text node. Obtain markup by + extracting ancestor node attributes. + @param sentence The sentence to mark up. + @return Sentence preceeded by txt2pho markup. + --> +<xsl:template name="output-sentence"> + <xsl:param name="sentence"/> + <!-- Pitch --> + <xsl:if test="ancestor::prosody/@pitch='high'"> + <xsl:value-of select="'{Pitch:300} '"/> + </xsl:if> + <xsl:if test="ancestor::prosody/@pitch='low'"> + <xsl:value-of select="'{Pitch:-40} '"/> + </xsl:if> + <!-- Output the sentence itself. --> + <xsl:value-of select="$sentence"/> +</xsl:template> + +<!-- Return the first sentence of argument. + Sentence delimiters are '.:;!?' + @param paragraph Paragraph from which to extract first sentence. + @return The first sentence, or if no such sentence, the paragraph. + --> +<xsl:template name="parse-sentence"> + <xsl:param name="paragraph"/> + <!-- Copy paragraph, replacing all delimeters with period. --> + <xsl:variable name="tmp"> + <xsl:value-of select="translate($paragraph,':;!?','....')"/> + </xsl:variable> + <!-- Look for first period and space and extract corresponding substring from original. --> + <xsl:choose> + <xsl:when test="contains($tmp, '. ')"> + <xsl:value-of select="substring($paragraph, 1, string-length(substring-before($tmp, '. '))+2)"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="$paragraph"/> + </xsl:otherwise> + </xsl:choose> +</xsl:template> + +<!-- Process a paragraph, outputting each sentence with txt2pho markup. + @param paragraph The paragraph to process. + @return The paragraph with each sentence preceeded by txt2pho markup. + --> +<xsl:template name="output-paragraph"> + <xsl:param name="paragraph" /> + <!-- Stop when no more sentences to output. --> + <xsl:choose> + <xsl:when test="normalize-space($paragraph)!=''"> + <!-- Split the paragraph into first sentence and rest of paragraph (if any). --> + <xsl:variable name="sentence"> + <xsl:call-template name="parse-sentence"> + <xsl:with-param name="paragraph" select="$paragraph"/> + </xsl:call-template> + </xsl:variable> + <!-- debug: <xsl:value-of select="concat('[sentence: ',$sentence,']')"/> --> + <xsl:variable name="rest"> + <xsl:value-of select="substring-after($paragraph,$sentence)" /> + </xsl:variable> + <!-- Output the sentence with markup. --> + <xsl:call-template name="output-sentence" > + <xsl:with-param name="sentence" select="$sentence" /> + </xsl:call-template> + <!-- Recursively process the rest of the paragraph. --> + <xsl:call-template name="output-paragraph"> + <xsl:with-param name="paragraph" select="$rest" /> + </xsl:call-template> + </xsl:when> + </xsl:choose> +</xsl:template> + +<!-- Process each text node. --> +<xsl:template match="text()"> + <!-- debug: <xsl:value-of select="concat('[paragraph: ',normalize-space(.),']')"/> --> + <xsl:call-template name="output-paragraph"> + <xsl:with-param name="paragraph" select="concat(normalize-space(.),' ')"/> + </xsl:call-template> +</xsl:template> + +</xsl:stylesheet> diff --git a/kttsd/plugins/hadifix/configure.in.bot b/kttsd/plugins/hadifix/configure.in.bot new file mode 100644 index 0000000..ffada4f --- /dev/null +++ b/kttsd/plugins/hadifix/configure.in.bot @@ -0,0 +1,37 @@ +if test "x$compile_hadifix_plugin" = "xyes"; then + if test "x$hadifix_inst" = "xno"; then + echo "" + echo "======================================================" + echo "Some of the tools needed to run Hadifix do not appear" + echo "to be installed on this system. (mbrola and txt2pho" + echo "were not found in the PATH or /etc/txt2pho was not" + echo "found). The Hadifix plugin will be built, but you need" + echo "to install the required tools before you can use it:" + echo "" + if test "x$hadifix_mbrola_bin" = "xno"; then + echo "- mbrola was not found. You can get it from" + echo " http://tcts.fpms.ac.be/synthesis/mbrola.html" + fi + if test "x$hadifix_txt2pho_bin" = "xno"; then + echo "- txt2pho was not found. You can get it from" + echo " http://www.ikp.uni-bonn.de/dt/forsch/phonetik/" + echo " hadifix/HADIFIXforMBROLA.html" + fi + echo "" + echo "Please read the KTTS Handbook for further information." + echo "=====================================================" + all_tests=bad + fi + if test "x$hadifix_inst" = "xyes"; then + if test "x$hadifix_txt2pho" = "xno"; then + echo "" + echo "======================================================" + echo "The configuration file /etc/txt2pho was not found on" + echo "this system. This is no problem as long as all users" + echo "have a valid ~/.txt2phorc in their home directories if" + echo "they want to use the Hadifix plugin." + echo "======================================================" + fi + fi +fi + diff --git a/kttsd/plugins/hadifix/configure.in.in b/kttsd/plugins/hadifix/configure.in.in new file mode 100644 index 0000000..3c997c2 --- /dev/null +++ b/kttsd/plugins/hadifix/configure.in.in @@ -0,0 +1,37 @@ +dnl ========================== +dnl checks for Hadifix plug in +dnl ========================== + +AC_ARG_ENABLE(kttsd-hadifix, + AC_HELP_STRING([--enable-kttsd-hadifix], + [build KTTSD Hadifix Plugin [default=yes]]), + hadifix_plugin=$enableval, + hadifix_plugin=yes) + +compile_hadifix_plugin="no" + +if test "x$hadifix_plugin" = "xyes"; then + compile_hadifix_plugin="yes" +fi + +if test "x$compile_hadifix_plugin" = "xyes"; then + dnl Check for Hadifix installation. + dnl Note that Hadifix plugin is always built, + dnl unless user overrides with -disable-kttsd-hadifix. + AC_PATH_PROG(hadifix_mbrola_bin, "mbrola", "no", [$PATH:]) + AC_PATH_PROG(hadifix_txt2pho_bin, "txt2pho", "no", [$PATH:]) + + hadifix_inst="yes" + if test "x$hadifix_mbrola_bin" = "xno"; then + hadifix_inst="no" + fi + if test "x$hadifix_txt2pho_bin" = "xno"; then + hadifix_inst="no" + fi + + if test "x$hadifix_inst" = "xyes"; then + AC_CHECK_FILE(/etc/txt2pho, [hadifix_txt2pho="yes"], [hadifix_txt2pho="no"]) + fi +fi + +AM_CONDITIONAL(include_kttsd_hadifix, test "x$compile_hadifix_plugin" = "xyes") diff --git a/kttsd/plugins/hadifix/hadifixconf.cpp b/kttsd/plugins/hadifix/hadifixconf.cpp new file mode 100644 index 0000000..a2c5547 --- /dev/null +++ b/kttsd/plugins/hadifix/hadifixconf.cpp @@ -0,0 +1,406 @@ +/*************************************************************************** + begin : Mon Okt 14 2002 + copyright : (C) 2002 by Gunnar Schmi Dt + email : gunnar@schmi-dt.de + current mainainer: : 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +// Qt includes. +#include <qlayout.h> +#include <qlabel.h> +#include <qgroupbox.h> +#include <qstring.h> +#include <qstringlist.h> +#include <qdir.h> +#include <qfileinfo.h> +#include <qfile.h> + +// KDE includes. +#include <ktempfile.h> +#include <kaboutdata.h> +#include <kaboutapplication.h> +#include <kdebug.h> +#include <klocale.h> +#include <kdialog.h> +#include <kcombobox.h> +#include <kstandarddirs.h> +#include <kurlrequester.h> +#include <kdialogbase.h> +#include <klineedit.h> +#include <knuminput.h> +#include <kprogress.h> + +// KTTS includes. +#include <pluginconf.h> +#include <testplayer.h> +#include <talkercode.h> + +// Hadifix includes. +#include "hadifixproc.h" +#include "voicefileui.h" +#include "hadifixconfigui.h" +#include "hadifixconf.h" +#include "hadifixconf.moc" + +class HadifixConfPrivate { + friend class HadifixConf; + private: + HadifixConfPrivate() { + hadifixProc = 0; + progressDlg = 0; + findInitialConfig(); + }; + + ~HadifixConfPrivate() { + if (hadifixProc) hadifixProc->stopText(); + delete hadifixProc; + if (!waveFile.isNull()) QFile::remove(waveFile); + delete progressDlg; + }; + + #include "initialconfig.h" + + void setConfiguration (QString hadifixExec, QString mbrolaExec, + QString voice, bool male, + int volume, int time, int pitch, + QString codecName) + { + configWidget->hadifixURL->setURL (hadifixExec); + configWidget->mbrolaURL->setURL (mbrolaExec); + configWidget->setVoice (voice, male); + + configWidget->volumeBox->setValue (volume); + configWidget->timeBox->setValue (time); + configWidget->frequencyBox->setValue (pitch); + int codec = PlugInProc::codecNameToListIndex(codecName, codecList); + configWidget->characterCodingBox->setCurrentItem(codec); + } + + void initializeVoices () { + QStringList::iterator it; + for (it = defaultVoices.begin(); it != defaultVoices.end(); ++it) { + HadifixProc::VoiceGender gender; + QString name = QFileInfo(*it).fileName(); + gender = HadifixProc::determineGender(defaultMbrolaExec, *it); + if (gender == HadifixProc::MaleGender) + configWidget->addVoice(*it, true, i18n("Male voice \"%1\"").arg(name)); + else if (gender == HadifixProc::FemaleGender) + configWidget->addVoice(*it, false, i18n("Female voice \"%1\"").arg(name)); + else { + if (name == "de1") + configWidget->addVoice(*it, false, i18n("Female voice \"%1\"").arg(name)); + else { + configWidget->addVoice(*it, true, i18n("Unknown voice \"%1\"").arg(name)); + configWidget->addVoice(*it, false, i18n("Unknown voice \"%1\"").arg(name)); + } + } + } + }; + + void initializeCharacterCodes() { + // Build codec list and fill combobox. + codecList = PlugInProc::buildCodecList(); + configWidget->characterCodingBox->clear(); + configWidget->characterCodingBox->insertStringList(codecList); + } + + void setDefaultEncodingFromVoice() { + QString voiceFile = configWidget->getVoiceFilename(); + QString voiceCode = QFileInfo(voiceFile).baseName(false); + voiceCode = voiceCode.left(2); + QString codecName = "Local"; + if (voiceCode == "de") codecName = "ISO 8859-1"; + if (voiceCode == "hu") codecName = "ISO 8859-2"; + configWidget->characterCodingBox->setCurrentItem(PlugInProc::codecNameToListIndex( + codecName, codecList)); +} + + void setDefaults () { + QStringList::iterator it = defaultVoices.begin(); + // Find a voice that matches language code, if any. + if (!languageCode.isEmpty()) + { + QString justLang = languageCode.left(2); + for (;it != defaultVoices.end();++it) + { + QString voiceCode = QFileInfo(*it).baseName(false).left(2); + if (voiceCode == justLang) break; + } + if (it == defaultVoices.end()) it = defaultVoices.begin(); + } + HadifixProc::VoiceGender gender; + gender = HadifixProc::determineGender(defaultMbrolaExec, *it); + + setConfiguration (defaultHadifixExec, defaultMbrolaExec, + *it, gender == HadifixProc::MaleGender, + 100, 100, 100, "Local"); + }; + + void load (KConfig *config, const QString &configGroup) { + config->setGroup(configGroup); + + QString voice = config->readEntry("voice", configWidget->getVoiceFilename()); + + HadifixProc::VoiceGender gender; + gender = HadifixProc::determineGender(defaultMbrolaExec, voice); + bool isMale = (gender == HadifixProc::MaleGender); + + QString defaultCodecName = "Local"; + // TODO: Need a better way to determine proper codec for each voice. + // This will do for now. + QString voiceCode = QFileInfo(voice).baseName(false); + if (voiceCode.left(2) == "de") defaultCodecName = "ISO 8859-1"; + if (voiceCode.left(2) == "hu") defaultCodecName = "ISO 8859-2"; + + setConfiguration ( + config->readEntry ("hadifixExec",defaultHadifixExec), + config->readEntry ("mbrolaExec", defaultMbrolaExec), + config->readEntry ("voice", voice), + config->readBoolEntry("gender", isMale), + config->readNumEntry ("volume", 100), + config->readNumEntry ("time", 100), + config->readNumEntry ("pitch", 100), + config->readEntry ("codec", defaultCodecName) + ); + }; + + void save (KConfig *config, const QString &configGroup) { + config->setGroup(configGroup); + config->writeEntry ("hadifixExec", PlugInConf::realFilePath(configWidget->hadifixURL->url())); + config->writeEntry ("mbrolaExec", PlugInConf::realFilePath(configWidget->mbrolaURL->url())); + config->writeEntry ("voice", configWidget->getVoiceFilename()); + config->writeEntry ("gender", configWidget->isMaleVoice()); + config->writeEntry ("volume", configWidget->volumeBox->value()); + config->writeEntry ("time", configWidget->timeBox->value()); + config->writeEntry ("pitch", configWidget->frequencyBox->value()); + config->writeEntry ("codec", PlugInProc::codecIndexToCodecName( + configWidget->characterCodingBox->currentItem(), codecList)); + } + + HadifixConfigUI *configWidget; + + QString languageCode; + QString defaultHadifixExec; + QString defaultMbrolaExec; + QStringList defaultVoices; + QStringList codecList; + + // Wave file playing on play object. + QString waveFile; + // Synthesizer. + HadifixProc* hadifixProc; + // Progress Dialog. + KProgressDialog* progressDlg; +}; + +/** Constructor */ +HadifixConf::HadifixConf( QWidget* parent, const char* name, const QStringList &) : + PlugInConf( parent, name ){ + // kdDebug() << "HadifixConf::HadifixConf: Running" << endl; + QVBoxLayout *layout = new QVBoxLayout (this, KDialog::marginHint(), KDialog::spacingHint(), "CommandConfigWidgetLayout"); + layout->setAlignment (Qt::AlignTop); + + d = new HadifixConfPrivate(); + d->configWidget = new HadifixConfigUI (this, "configWidget"); + + QString file = locate("data", "LICENSES/LGPL_V2"); + i18n("This plugin is distributed under the terms of the GPL v2 or later."); + + connect(d->configWidget->voiceButton, SIGNAL(clicked()), this, SLOT(voiceButton_clicked())); + connect(d->configWidget->testButton, SIGNAL(clicked()), this, SLOT(testButton_clicked())); + connect(d->configWidget, SIGNAL(changed(bool)), this, SLOT(configChanged (bool))); + connect(d->configWidget->characterCodingBox, SIGNAL(textChanged(const QString&)), + this, SLOT(configChanged())); + connect(d->configWidget->voiceCombo, SIGNAL(activated(int)), this, SLOT(voiceCombo_activated(int))); + d->initializeCharacterCodes(); + d->initializeVoices(); + d->setDefaults(); + layout->addWidget (d->configWidget); +} + +/** Destructor */ +HadifixConf::~HadifixConf(){ + // kdDebug() << "HadifixConf::~HadifixConf: Running" << endl; + delete d; +} + +void HadifixConf::load(KConfig *config, const QString &configGroup) { + // kdDebug() << "HadifixConf::load: Running" << endl; + d->setDefaults(); + d->load (config, configGroup); +} + +void HadifixConf::save(KConfig *config, const QString &configGroup) { + // kdDebug() << "HadifixConf::save: Running" << endl; + d->save (config, configGroup); +} + +void HadifixConf::defaults() { + // kdDebug() << "HadifixConf::defaults: Running" << endl; + d->setDefaults(); +} + +void HadifixConf::setDesiredLanguage(const QString &lang) +{ + d->languageCode = lang; +} + +QString HadifixConf::getTalkerCode() +{ + if (!d->configWidget->hadifixURL->url().isEmpty() && !d->configWidget->mbrolaURL->url().isEmpty()) + { + QString voiceFile = d->configWidget->getVoiceFilename(); + if (QFileInfo(voiceFile).exists()) + { + // mbrola voice file names usually start with two-letter language code, + // but this is by no means guaranteed. + QString voiceCode = QFileInfo(voiceFile).baseName(false); + QString voiceLangCode = voiceCode.left(2); + if (d->languageCode.left(2) != voiceLangCode) + { + // Verify that first two letters of voice filename are a valid language code. + // If they are, switch to that language. + if (!TalkerCode::languageCodeToLanguage(voiceLangCode).isEmpty()) + d->languageCode = voiceLangCode; + } + QString gender = "male"; + if (!d->configWidget->isMaleVoice()) gender = "female"; + QString volume = "medium"; + if (d->configWidget->volumeBox->value() < 75) volume = "soft"; + if (d->configWidget->volumeBox->value() > 125) volume = "loud"; + QString rate = "medium"; + if (d->configWidget->timeBox->value() < 75) rate = "slow"; + if (d->configWidget->timeBox->value() > 125) rate = "fast"; + return QString( + "<voice lang=\"%1\" name=\"%2\" gender=\"%3\" />" + "<prosody volume=\"%4\" rate=\"%5\" />" + "<kttsd synthesizer=\"%6\" />") + .arg(d->languageCode) + .arg(voiceCode) + .arg(gender) + .arg(volume) + .arg(rate) + .arg("Hadifix"); + } + } + return QString::null; +} + +void HadifixConf::voiceButton_clicked () { + KDialogBase *dialog = new KDialogBase (this, 0, true, + i18n("Voice File - Hadifix Plugin"), + KDialogBase::Ok|KDialogBase::Cancel, + KDialogBase::Ok, true); + VoiceFileWidget *widget = new VoiceFileWidget(dialog); + dialog->setMainWidget (widget); + + widget->femaleOption->setChecked(!d->configWidget->isMaleVoice()); + widget->maleOption->setChecked(d->configWidget->isMaleVoice()); + widget->voiceFileURL->setURL(d->configWidget->getVoiceFilename()); + widget->mbrola = d->defaultMbrolaExec; + + if (dialog->exec() == QDialog::Accepted) { + d->configWidget->setVoice (widget->voiceFileURL->url(), + widget->maleOption->isChecked()); + d->setDefaultEncodingFromVoice(); + emit changed(true); + } + + delete dialog; +} + +void HadifixConf::voiceCombo_activated(int /*index*/) +{ + d->setDefaultEncodingFromVoice(); +} + +void HadifixConf::testButton_clicked () { + // If currently synthesizing, stop it. + if (d->hadifixProc) + d->hadifixProc->stopText(); + else + { + d->hadifixProc = new HadifixProc(); + connect (d->hadifixProc, SIGNAL(stopped()), this, SLOT(slotSynthStopped())); + } + // Create a temp file name for the wave file. + KTempFile tempFile (locateLocal("tmp", "hadifixplugin-"), ".wav"); + QString tmpWaveFile = tempFile.file()->name(); + tempFile.close(); + + // Tell user to wait. + d->progressDlg = new KProgressDialog(d->configWidget, "ktts_hadifix_testdlg", + i18n("Testing"), + i18n("Testing."), + true); + d->progressDlg->progressBar()->hide(); + d->progressDlg->setAllowCancel(true); + + // Speak a German sentence as hadifix is a German tts + // TODO: Actually, Hadifix does support English (and other languages?) as well, + // If you install the right voice files. The hard part is finding and installing + // a working txt2pho for the desired language. There seem to be some primitive french, + // italian, and a few others, written in perl, but they have many issues. + // Go to the mbrola website and click on "TTS" to learn more. + + // QString testMsg = "K D E ist eine moderne grafische Arbeitsumgebung für UNIX-Computer."; + QString testMsg = testMessage(d->languageCode); + connect (d->hadifixProc, SIGNAL(synthFinished()), this, SLOT(slotSynthFinished())); + d->hadifixProc->synth (testMsg, + realFilePath(d->configWidget->hadifixURL->url()), + d->configWidget->isMaleVoice(), + realFilePath(d->configWidget->mbrolaURL->url()), + d->configWidget->getVoiceFilename(), + d->configWidget->volumeBox->value(), + d->configWidget->timeBox->value(), + d->configWidget->frequencyBox->value(), + PlugInProc::codecIndexToCodec(d->configWidget->characterCodingBox->currentItem(), d->codecList), + tmpWaveFile); + + // Display progress dialog modally. Processing continues when plugin signals synthFinished, + // or if user clicks Cancel button. + d->progressDlg->exec(); + disconnect (d->hadifixProc, SIGNAL(synthFinished()), this, SLOT(slotSynthFinished())); + if (d->progressDlg->wasCancelled()) d->hadifixProc->stopText(); + delete d->progressDlg; + d->progressDlg = 0; +} + +void HadifixConf::slotSynthFinished() +{ + // If user canceled, progress dialog is gone, so exit. + if (!d->progressDlg) + { + d->hadifixProc->ackFinished(); + return; + } + // Hide the Cancel button so user can't cancel in the middle of playback. + d->progressDlg->showCancelButton(false); + // Get new wavefile name. + d->waveFile = d->hadifixProc->getFilename(); + // Tell synth we're done. + d->hadifixProc->ackFinished(); + // Play the wave file (possibly adjusting its Speed). + // Player object deletes the wave file when done. + if (m_player) m_player->play(d->waveFile); + QFile::remove(d->waveFile); + d->waveFile = QString::null; + if (d->progressDlg) d->progressDlg->close(); +} + +void HadifixConf::slotSynthStopped() +{ + // Clean up after canceling test. + QString filename = d->hadifixProc->getFilename(); + // kdDebug() << "HadifixConf::slotSynthStopped: filename = " << filename << endl; + if (!filename.isNull()) QFile::remove(filename); +} diff --git a/kttsd/plugins/hadifix/hadifixconf.h b/kttsd/plugins/hadifix/hadifixconf.h new file mode 100644 index 0000000..a53ecb1 --- /dev/null +++ b/kttsd/plugins/hadifix/hadifixconf.h @@ -0,0 +1,83 @@ +#ifndef _HADIFIXCONF_H_ +#define _HADIFIXCONF_H_ + +#include <qstringlist.h> + +#include <kconfig.h> + +#include <pluginconf.h> + +class HadifixProc; +class HadifixConfPrivate; + +class HadifixConf : public PlugInConf { + Q_OBJECT + + public: + /** Constructor */ + HadifixConf( QWidget* parent = 0, const char* name = 0, const QStringList &args = QStringList()); + + /** Destructor */ + ~HadifixConf(); + + /** This method is invoked whenever the module should read its + configuration (most of the times from a config file) and update the + user interface. This happens when the user clicks the "Reset" button in + the control center, to undo all of his changes and restore the currently + valid settings. NOTE that this is not called after the modules is loaded, + so you probably want to call this method in the constructor.*/ + void load(KConfig *config, const QString &configGroup); + + /** This function gets called when the user wants to save the settings in + the user interface, updating the config files or wherever the + configuration is stored. The method is called when the user clicks "Apply" + or "Ok". */ + void save(KConfig *config, const QString &configGroup); + + /** This function is called to set the settings in the module to sensible + default values. It gets called when hitting the "Default" button. The + default values should probably be the same as the ones the application + uses when started without a config file. */ + void defaults(); + + /** + * This function informs the plugin of the desired language to be spoken + * by the plugin. The plugin should attempt to adapt itself to the + * specified language code, choosing sensible defaults if necessary. + * If the passed-in code is QString::null, no specific language has + * been chosen. + * @param lang The desired language code or Null if none. + * + * If the plugin is unable to support the desired language, that is OK. + * Language codes are given by ISO 639-1 and are in lowercase. + * The code may also include an ISO 3166 country code in uppercase + * separated from the language code by underscore (_). For + * example, en_GB. If your plugin supports the given language, but + * not the given country, treat it as though the country + * code were not specified, i.e., adapt to the given language. + */ + void setDesiredLanguage(const QString &lang); + + /** + * Return fully-specified talker code for the configured plugin. This code + * uniquely identifies the configured instance of the plugin and distinquishes + * one instance from another. If the plugin has not been fully configured, + * i.e., cannot yet synthesize, return QString::null. + * @return Fully-specified talker code. + */ + QString getTalkerCode(); + + public slots: + void configChanged(bool t = true){emit changed(t);}; + + private slots: + virtual void voiceButton_clicked(); + virtual void testButton_clicked(); + virtual void voiceCombo_activated(int index); + void slotSynthFinished(); + void slotSynthStopped(); + + private: + HadifixConfPrivate *d; +}; +#endif diff --git a/kttsd/plugins/hadifix/hadifixconfigui.ui b/kttsd/plugins/hadifix/hadifixconfigui.ui new file mode 100644 index 0000000..302c1de --- /dev/null +++ b/kttsd/plugins/hadifix/hadifixconfigui.ui @@ -0,0 +1,692 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>HadifixConfigUI</class> +<widget class="QWidget"> + <property name="name"> + <cstring>HadifixConfigUI</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>578</width> + <height>388</height> + </rect> + </property> + <property name="caption"> + <string>Hadifix Configuration</string> + </property> + <property name="whatsThis" stdset="0"> + <string>This is the configuration dialog for the Hadifix (txt2pho and Mbrola) speech synthesizer.</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QGroupBox" row="0" column="0"> + <property name="name"> + <cstring>GroupBox4</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Had&ifix Configuration</string> + </property> + <property name="whatsThis" stdset="0"> + <string>This is the configuration dialog for the Hadifix (txt2pho and Mbrola) speech synthesizer.</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>11</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QGroupBox" row="0" column="0" rowspan="1" colspan="2"> + <property name="name"> + <cstring>basicOptions</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>Box</enum> + </property> + <property name="frameShadow"> + <enum>Sunken</enum> + </property> + <property name="title"> + <string>&Basic Options</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel" row="0" column="0"> + <property name="name"> + <cstring>voiceLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&Voice file:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>voiceCombo</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Select a voice for speaking text. If no voices are listed, check your Mbrola configuration. You must install at least one voice.</string> + </property> + </widget> + <widget class="KComboBox" row="0" column="1" rowspan="1" colspan="2"> + <property name="name"> + <cstring>voiceCombo</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>1</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="whatsThis" stdset="0"> + <string>Select a voice for speaking text. If no voices are listed, check your Mbrola configuration. You must install at least one voice.</string> + </property> + </widget> + <widget class="KPushButton" row="0" column="3"> + <property name="name"> + <cstring>voiceButton</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>1</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&Select...</string> + </property> + </widget> + <widget class="QLabel" row="1" column="0"> + <property name="name"> + <cstring>volumeLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Volume &ratio:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>volumeBox</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Adjusts the volume of speech. Slide to left for softer speech; to the right for louder.</string> + </property> + </widget> + <widget class="KIntSpinBox" row="1" column="1"> + <property name="name"> + <cstring>volumeBox</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string> %</string> + </property> + <property name="maxValue"> + <number>200</number> + </property> + <property name="minValue"> + <number>50</number> + </property> + <property name="value"> + <number>100</number> + </property> + <property name="whatsThis" stdset="0"> + <string>Adjusts the volume of speech. Slide to left for softer speech; to the right for louder.</string> + </property> + </widget> + <widget class="QSlider" row="1" column="2" rowspan="1" colspan="2"> + <property name="name"> + <cstring>volumeSlider</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="focusPolicy"> + <enum>NoFocus</enum> + </property> + <property name="minValue"> + <number>0</number> + </property> + <property name="maxValue"> + <number>1000</number> + </property> + <property name="lineStep"> + <number>10</number> + </property> + <property name="pageStep"> + <number>100</number> + </property> + <property name="value"> + <number>500</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="tickmarks"> + <enum>NoMarks</enum> + </property> + <property name="whatsThis" stdset="0"> + <string>Adjusts the volume of speech. Slide to left for softer speech; to the right for louder.</string> + </property> + </widget> + <widget class="QLabel" row="2" column="0"> + <property name="name"> + <cstring>timeLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Speed:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>timeBox</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Adjusts the speed of speech. Slide to left for slower speech; to the right for faster.</string> + </property> + </widget> + <widget class="KIntSpinBox" row="2" column="1"> + <property name="name"> + <cstring>timeBox</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string> %</string> + </property> + <property name="maxValue"> + <number>200</number> + </property> + <property name="minValue"> + <number>50</number> + </property> + <property name="value"> + <number>100</number> + </property> + <property name="whatsThis" stdset="0"> + <string>Adjusts the speed of speech. Slide to left for slower speech; to the right for faster.</string> + </property> + </widget> + <widget class="QSlider" row="2" column="2" rowspan="1" colspan="2"> + <property name="name"> + <cstring>timeSlider</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="focusPolicy"> + <enum>NoFocus</enum> + </property> + <property name="maxValue"> + <number>1000</number> + </property> + <property name="lineStep"> + <number>10</number> + </property> + <property name="pageStep"> + <number>100</number> + </property> + <property name="value"> + <number>500</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="whatsThis" stdset="0"> + <string>Adjusts the speed of speech. Slide to left for slower speech; to the right for faster.</string> + </property> + </widget> + <widget class="QLabel" row="3" column="0"> + <property name="name"> + <cstring>frequencyLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&Pitch:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>frequencyBox</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Adjusts the pitch (tone) of speech. Slide to left for lower speech; to the right for higher.</string> + </property> + </widget> + <widget class="KIntSpinBox" row="3" column="1"> + <property name="name"> + <cstring>frequencyBox</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string> %</string> + </property> + <property name="maxValue"> + <number>200</number> + </property> + <property name="minValue"> + <number>50</number> + </property> + <property name="value"> + <number>100</number> + </property> + <property name="whatsThis" stdset="0"> + <string>Adjusts the pitch (tone) of speech. Slide to left for lower speech; to the right for higher.</string> + </property> + </widget> + <widget class="QSlider" row="3" column="2" rowspan="1" colspan="2"> + <property name="name"> + <cstring>frequencySlider</cstring> + </property> + <property name="focusPolicy"> + <enum>NoFocus</enum> + </property> + <property name="maxValue"> + <number>1000</number> + </property> + <property name="lineStep"> + <number>10</number> + </property> + <property name="pageStep"> + <number>100</number> + </property> + <property name="value"> + <number>500</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="whatsThis" stdset="0"> + <string>Adjusts the pitch (tone) of speech. Slide to left for lower speech; to the right for higher.</string> + </property> + </widget> + </grid> + </widget> + <widget class="QGroupBox" row="1" column="0" rowspan="1" colspan="2"> + <property name="name"> + <cstring>advancedOptions</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>&Advanced Options</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>11</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QLabel" row="0" column="0"> + <property name="name"> + <cstring>hadifixBinLabel</cstring> + </property> + <property name="text"> + <string>txt2pho e&xecutable:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>hadifixURL</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>If the txt2pho program is in your PATH environment variable, simply enter "txt2pho", otherwise specify the full path to the txt2pho executable program.</string> + </property> + </widget> + <widget class="KURLRequester" row="0" column="1"> + <property name="name"> + <cstring>hadifixURL</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="whatsThis" stdset="0"> + <string>If the txt2pho program is in your PATH environment variable, simply enter "txt2pho", otherwise specify the full path to the txt2pho executable program.</string> + </property> + </widget> + <widget class="QLabel" row="1" column="0"> + <property name="name"> + <cstring>mbrolaBinLabel</cstring> + </property> + <property name="text"> + <string>&Mbrola executable:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>mbrolaURL</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>If the Mbrola program is in your PATH environment variable, simply enter "mbrola", otherwise specify the full path to the Mbrola executable program.</string> + </property> + </widget> + <widget class="KURLRequester" row="1" column="1"> + <property name="name"> + <cstring>mbrolaURL</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="whatsThis" stdset="0"> + <string>If the Mbrola program is in your PATH environment variable, simply enter "mbrola", otherwise specify the full path to the Mbrola executable program.</string> + </property> + </widget> + <widget class="QLayoutWidget" row="2" column="0" rowspan="1" colspan="2"> + <property name="name"> + <cstring>layout5</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>characterCodingLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Character &encoding:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>characterCodingBox</cstring> + </property> + </widget> + <widget class="KComboBox"> + <property name="name"> + <cstring>characterCodingBox</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="whatsThis" stdset="0"> + <string>This combo box specifies which character encoding is used for passing the text. For most western languages, use ISO-8859-1. For Hungarian, use ISO-8859-2.</string> + </property> + </widget> + </hbox> + </widget> + </grid> + </widget> + <spacer row="2" column="0"> + <property name="name"> + <cstring>spacer1</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Preferred</enum> + </property> + <property name="sizeHint"> + <size> + <width>240</width> + <height>20</height> + </size> + </property> + </spacer> + <widget class="KPushButton" row="2" column="1"> + <property name="name"> + <cstring>testButton</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&Test</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Click to test the configuration. You should hear a spoken sentence.</string> + </property> + </widget> + </grid> + </widget> + </grid> +</widget> +<customwidgets> +</customwidgets> +<connections> + <connection> + <sender>volumeBox</sender> + <signal>valueChanged(int)</signal> + <receiver>HadifixConfigUI</receiver> + <slot>volumeBox_valueChanged(int)</slot> + </connection> + <connection> + <sender>volumeSlider</sender> + <signal>valueChanged(int)</signal> + <receiver>HadifixConfigUI</receiver> + <slot>volumeSlider_valueChanged(int)</slot> + </connection> + <connection> + <sender>timeBox</sender> + <signal>valueChanged(int)</signal> + <receiver>HadifixConfigUI</receiver> + <slot>timeBox_valueChanged(int)</slot> + </connection> + <connection> + <sender>timeSlider</sender> + <signal>valueChanged(int)</signal> + <receiver>HadifixConfigUI</receiver> + <slot>timeSlider_valueChanged(int)</slot> + </connection> + <connection> + <sender>frequencyBox</sender> + <signal>valueChanged(int)</signal> + <receiver>HadifixConfigUI</receiver> + <slot>frequencyBox_valueChanged(int)</slot> + </connection> + <connection> + <sender>frequencySlider</sender> + <signal>valueChanged(int)</signal> + <receiver>HadifixConfigUI</receiver> + <slot>frequencySlider_valueChanged(int)</slot> + </connection> + <connection> + <sender>voiceCombo</sender> + <signal>activated(const QString&)</signal> + <receiver>HadifixConfigUI</receiver> + <slot>changed(const QString&)</slot> + </connection> + <connection> + <sender>volumeBox</sender> + <signal>valueChanged(const QString&)</signal> + <receiver>HadifixConfigUI</receiver> + <slot>changed(const QString&)</slot> + </connection> + <connection> + <sender>timeBox</sender> + <signal>valueChanged(const QString&)</signal> + <receiver>HadifixConfigUI</receiver> + <slot>changed(const QString&)</slot> + </connection> + <connection> + <sender>frequencyBox</sender> + <signal>valueChanged(const QString&)</signal> + <receiver>HadifixConfigUI</receiver> + <slot>changed(const QString&)</slot> + </connection> + <connection> + <sender>hadifixURL</sender> + <signal>textChanged(const QString&)</signal> + <receiver>HadifixConfigUI</receiver> + <slot>changed(const QString&)</slot> + </connection> + <connection> + <sender>mbrolaURL</sender> + <signal>textChanged(const QString&)</signal> + <receiver>HadifixConfigUI</receiver> + <slot>changed(const QString&)</slot> + </connection> +</connections> +<includes> + <include location="global" impldecl="in declaration">qradiobutton.h</include> + <include location="global" impldecl="in declaration">qpixmap.h</include> + <include location="global" impldecl="in declaration">kurl.h</include> + <include location="global" impldecl="in declaration">qmap.h</include> + <include location="global" impldecl="in implementation">kglobal.h</include> + <include location="global" impldecl="in implementation">qstringlist.h</include> + <include location="global" impldecl="in implementation">math.h</include> + <include location="global" impldecl="in implementation">kiconloader.h</include> +</includes> +<forwards> + <forward>class QStringList;</forward> +</forwards> +<variables> + <variable>QMap<QString,int> maleVoices;</variable> + <variable>QMap<int,QString> defaultVoices;</variable> + <variable>QPixmap female;</variable> + <variable>QPixmap male;</variable> + <variable>QMap<QString,int> femaleVoices;</variable> +</variables> +<signals> + <signal>changed(bool)</signal> +</signals> +<slots> + <slot access="protected" specifier="non virtual">volumeBox_valueChanged( int percentValue )</slot> + <slot access="protected" specifier="non virtual">timeBox_valueChanged( int percentValue )</slot> + <slot access="protected" specifier="non virtual">frequencyBox_valueChanged( int percentValue )</slot> + <slot access="protected" specifier="non virtual">volumeSlider_valueChanged( int sliderValue )</slot> + <slot access="protected" specifier="non virtual">timeSlider_valueChanged( int sliderValue )</slot> + <slot access="protected" specifier="non virtual">frequencySlider_valueChanged( int sliderValue )</slot> + <slot access="protected">changed( const QString & )</slot> +</slots> +<functions> + <function access="protected" specifier="non virtual" returnType="int">percentToSlider( int percentValue )</function> + <function access="protected" specifier="non virtual" returnType="int">sliderToPercent( int sliderValue )</function> + <function access="private" specifier="non virtual">init()</function> + <function specifier="non virtual">addVoice( const QString &filename, bool isMale )</function> + <function specifier="non virtual">addVoice( const QString &filename, bool isMale, const QString &displayname )</function> + <function specifier="non virtual">setVoice( const QString &filename, bool isMale )</function> + <function specifier="non virtual" returnType="QString">getVoiceFilename()</function> + <function specifier="non virtual" returnType="bool">isMaleVoice()</function> +</functions> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>kcombobox.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>knuminput.h</includehint> + <includehint>knuminput.h</includehint> + <includehint>knuminput.h</includehint> + <includehint>kurlrequester.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>kurlrequester.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>kcombobox.h</includehint> + <includehint>kpushbutton.h</includehint> +</includehints> +</UI> diff --git a/kttsd/plugins/hadifix/hadifixconfigui.ui.h b/kttsd/plugins/hadifix/hadifixconfigui.ui.h new file mode 100644 index 0000000..e3a7a04 --- /dev/null +++ b/kttsd/plugins/hadifix/hadifixconfigui.ui.h @@ -0,0 +1,114 @@ +/**************************************************************************** +** ui.h extension file, included from the uic-generated form implementation. +** +** If you wish to add, delete or rename slots use Qt Designer which will +** update this file, preserving your code. Create an init() slot in place of +** a constructor, and a destroy() slot in place of a destructor. +*****************************************************************************/ + +// Basically the slider values are logarithmic (0,...,1000) whereas percent +// values are linear (50%,...,200%). +// +// slider = alpha * (log(percent)-log(50)) +// with alpha = 1000/(log(200)-log(50)) + +int HadifixConfigUI::percentToSlider (int percentValue) { + double alpha = 1000 / (log(200) - log(50)); + return (int)floor (0.5 + alpha * (log(percentValue)-log(50))); +} + +int HadifixConfigUI::sliderToPercent (int sliderValue) { + double alpha = 1000 / (log(200) - log(50)); + return (int)floor(0.5 + exp (sliderValue/alpha + log(50))); +} + +void HadifixConfigUI::volumeBox_valueChanged (int percentValue) { + volumeSlider->setValue (percentToSlider (percentValue)); +} + +void HadifixConfigUI::timeBox_valueChanged (int percentValue) { + timeSlider->setValue (percentToSlider (percentValue)); +} + +void HadifixConfigUI::frequencyBox_valueChanged (int percentValue) { + frequencySlider->setValue (percentToSlider (percentValue)); +} + +void HadifixConfigUI::volumeSlider_valueChanged (int sliderValue) { + volumeBox->setValue (sliderToPercent (sliderValue)); +} + +void HadifixConfigUI::timeSlider_valueChanged (int sliderValue) { + timeBox->setValue (sliderToPercent (sliderValue)); +} + +void HadifixConfigUI::frequencySlider_valueChanged (int sliderValue) { + frequencyBox->setValue (sliderToPercent (sliderValue)); +} + +void HadifixConfigUI::init () { + male = KGlobal::iconLoader()->loadIcon("male", KIcon::Small); + female = KGlobal::iconLoader()->loadIcon("female", KIcon::Small); +} + +void HadifixConfigUI::addVoice (const QString &filename, bool isMale) { + if (isMale) { + if (!maleVoices.contains(filename)) { + int id = voiceCombo->count(); + maleVoices.insert (filename, id); + voiceCombo->insertItem (male, filename, id); + } + } + else { + if (!femaleVoices.contains(filename)) { + int id = voiceCombo->count(); + femaleVoices.insert (filename, id); + voiceCombo->insertItem (female, filename, id); + } + } +} + +void HadifixConfigUI::addVoice (const QString &filename, bool isMale, const QString &displayname) { + addVoice (filename, isMale); + + if (isMale) { + defaultVoices [maleVoices [filename]] = filename; + voiceCombo->changeItem (male, displayname, maleVoices [filename]); + } + else{ + defaultVoices [femaleVoices [filename]] = filename; + voiceCombo->changeItem (female, displayname, femaleVoices [filename]); + } +} + +void HadifixConfigUI::setVoice (const QString &filename, bool isMale) { + addVoice (filename, isMale); + if (isMale) + voiceCombo->setCurrentItem (maleVoices[filename]); + else + voiceCombo->setCurrentItem (femaleVoices[filename]); +} + +QString HadifixConfigUI::getVoiceFilename() { + int curr = voiceCombo->currentItem(); + + QString filename = voiceCombo->text(curr); + if (defaultVoices.contains(curr)) + filename = defaultVoices[curr]; + + return filename; +} + +bool HadifixConfigUI::isMaleVoice() { + int curr = voiceCombo->currentItem(); + QString filename = getVoiceFilename(); + + if (maleVoices.contains(filename)) + return maleVoices[filename] == curr; + else + return false; +} + +void HadifixConfigUI::changed (const QString &) { + emit changed (true); +} diff --git a/kttsd/plugins/hadifix/hadifixplugin.cpp b/kttsd/plugins/hadifix/hadifixplugin.cpp new file mode 100644 index 0000000..b52052c --- /dev/null +++ b/kttsd/plugins/hadifix/hadifixplugin.cpp @@ -0,0 +1,23 @@ +/*************************************************************************** + begin : Mon Okt 14 2002 + copyright : (C) 2002 by Gunnar Schmi Dt + email : gunnar@schmi-dt.de + current mainainer: : 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include <kgenericfactory.h> + +#include "hadifixconf.h" +#include "hadifixproc.h" + +typedef K_TYPELIST_2( HadifixProc, HadifixConf ) Hadifix; +K_EXPORT_COMPONENT_FACTORY( libkttsd_hadifixplugin, KGenericFactory<Hadifix>("kttsd_hadifix") ) diff --git a/kttsd/plugins/hadifix/hadifixproc.cpp b/kttsd/plugins/hadifix/hadifixproc.cpp new file mode 100644 index 0000000..42730b3 --- /dev/null +++ b/kttsd/plugins/hadifix/hadifixproc.cpp @@ -0,0 +1,411 @@ +/*************************************************************************** + hadifixproc.cpp - description + ------------------- + begin : Mon Okt 14 2002 + copyright : (C) 2002 by Gunnar Schmi Dt + email : gunnar@schmi-dt.de + current mainainer: : 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include <qstring.h> +#include <qstringlist.h> +#include <qtextcodec.h> + +#include <kdebug.h> +#include <kconfig.h> +#include <kprocess.h> +#include <kstandarddirs.h> + +#include "hadifixproc.h" +#include "hadifixproc.moc" + +class HadifixProcPrivate { + friend class HadifixProc; + private: + HadifixProcPrivate () { + hadifixProc = 0; + waitingStop = false; + state = psIdle; + synthFilename = QString::null; + gender = false; + volume = 100; + time = 100; + pitch = 100; + codec = 0; + }; + + ~HadifixProcPrivate() { + delete hadifixProc; + }; + + void load(KConfig *config, const QString &configGroup) { + config->setGroup(configGroup); + hadifix = config->readEntry ("hadifixExec", QString::null); + mbrola = config->readEntry ("mbrolaExec", QString::null); + voice = config->readEntry ("voice", QString::null); + gender = config->readBoolEntry("gender", false); + volume = config->readNumEntry ("volume", 100); + time = config->readNumEntry ("time", 100); + pitch = config->readNumEntry ("pitch", 100); + codec = PlugInProc::codecNameToCodec(config->readEntry ("codec", "Local")); + }; + + QString hadifix; + QString mbrola; + QString voice; + bool gender; + int volume; + int time; + int pitch; + + bool waitingStop; + KShellProcess* hadifixProc; + volatile pluginState state; + QTextCodec* codec; + QString synthFilename; +}; + +/** Constructor */ +HadifixProc::HadifixProc( QObject* parent, const char* name, const QStringList &) : + PlugInProc( parent, name ){ + // kdDebug() << "HadifixProc::HadifixProc: Running" << endl; + d = 0; +} + +/** Destructor */ +HadifixProc::~HadifixProc(){ + // kdDebug() << "HadifixProc::~HadifixProc: Running" << endl; + + if (d != 0) { + delete d; + d = 0; + } +} + +/** Initializate the speech */ +bool HadifixProc::init(KConfig *config, const QString &configGroup){ + // kdDebug() << "HadifixProc::init: Initializing plug in: Hadifix" << endl; + + if (d == 0) + d = new HadifixProcPrivate(); + d->load(config, configGroup); + 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 +* and emit sayFinished signal when synthesis and audibilizing is finished. +* It must also implement the @ref getState method, which must return +* psFinished, when saying is completed. +*/ +void HadifixProc::sayText(const QString& /*text*/) +{ + kdDebug() << "HadifixProc::sayText: Warning, sayText not implemented." << endl; + return; +} + +/** +* 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 +* and emit @ref synthFinished signal when synthesis is completed. +* It must also implement the @ref getState method, which must return +* psFinished, when synthesis is completed. +*/ +void HadifixProc::synthText(const QString &text, const QString &suggestedFilename) +{ + if (d == 0) return; // Caller should have called init. + synth(text, d->hadifix, d->gender, d->mbrola, d->voice, d->volume, + d->time, d->pitch, d->codec, suggestedFilename); +} + +/** +* Synthesize text using a specified configuration. +* @param text The text to synthesize. +* @param hadifix Command to run hadifix (txt2pho). +* @param isMale True to use male voice. +* @param mbrola Command to run mbrola. +* @param voice Voice file for mbrola to use. +* @param volume Volume percent. 100 = normal +* @param time Speed percent. 100 = normal +* @param pitch Frequency. 100 = normal +* @param waveFilename Name of file to synthesize to. +*/ +void HadifixProc::synth(QString text, + QString hadifix, bool isMale, + QString mbrola, QString voice, + int volume, int time, int pitch, + QTextCodec *codec, + const QString waveFilename) +{ + // kdDebug() << "HadifixProc::synth: Saying text: '" << text << "' using Hadifix plug in" << endl; + if (d == 0) + { + d = new HadifixProcPrivate(); + } + if (hadifix.isNull() || hadifix.isEmpty()) + return; + if (mbrola.isNull() || mbrola.isEmpty()) + return; + if (voice.isNull() || voice.isEmpty()) + return; + + // If process exists, delete it so we can create a new one. + // kdDebug() << "HadifixProc::synth: creating process" << endl; + if (d->hadifixProc) delete d->hadifixProc; + + // Create process. + d->hadifixProc = new KShellProcess; + + // Set up txt2pho and mbrola commands. + // kdDebug() << "HadifixProc::synth: setting up commands" << endl; + QString hadifixCommand = d->hadifixProc->quote(hadifix); + if (isMale) + hadifixCommand += " -m"; + else + hadifixCommand += " -f"; + + QString mbrolaCommand = d->hadifixProc->quote(mbrola); + mbrolaCommand += " -e"; //Ignore fatal errors on unkown diphone + mbrolaCommand += QString(" -v %1").arg(volume/100.0); // volume ratio + mbrolaCommand += QString(" -f %1").arg(pitch/100.0); // freqency ratio + mbrolaCommand += QString(" -t %1").arg(1/(time/100.0)); // time ratio + mbrolaCommand += " " + d->hadifixProc->quote(voice); + mbrolaCommand += " - " + d->hadifixProc->quote(waveFilename); + + // kdDebug() << "HadifixProc::synth: Hadifix command: " << hadifixCommand << endl; + // kdDebug() << "HadifixProc::synth: Mbrola command: " << mbrolaCommand << endl; + + QString command = hadifixCommand + "|" + mbrolaCommand; + *(d->hadifixProc) << command; + + // Connect signals from process. + connect(d->hadifixProc, SIGNAL(processExited(KProcess *)), + this, SLOT(slotProcessExited(KProcess *))); + connect(d->hadifixProc, SIGNAL(wroteStdin(KProcess *)), + this, SLOT(slotWroteStdin(KProcess *))); + + // Store off name of wave file to be generated. + d->synthFilename = waveFilename; + // Set state, busy synthing. + d->state = psSynthing; + if (!d->hadifixProc->start(KProcess::NotifyOnExit, KProcess::Stdin)) + { + kdDebug() << "HadifixProc::synth: start process failed." << endl; + d->state = psIdle; + } else { + QCString encodedText; + if (codec) { + encodedText = codec->fromUnicode(text); + // kdDebug() << "HadifixProc::synth: encoding using " << codec->name() << endl; + } else + encodedText = text.latin1(); // Should not happen, but just in case. + // Send the text to be synthesized to process. + d->hadifixProc->writeStdin(encodedText, encodedText.length()); + } +} + +/** +* Get the generated audio filename from call to @ref synthText. +* @return Name of the audio file the plugin generated. +* Null if no such file. +* +* The plugin must not re-use or delete the filename. The file may not +* be locked when this method is called. The file will be deleted when +* KTTSD is finished using it. +*/ +QString HadifixProc::getFilename() { return d->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 HadifixProc::stopText(){ + // kdDebug() << "Running: HadifixProc::stopText()" << endl; + if (d->hadifixProc) + { + if (d->hadifixProc->isRunning()) + { + // kdDebug() << "HadifixProc::stopText: killing Hadifix shell." << endl; + d->waitingStop = true; + d->hadifixProc->kill(); + } else d->state = psIdle; + } else d->state = psIdle; + // d->state = psIdle; + // kdDebug() << "HadifixProc::stopText: Hadifix stopped." << endl; +} + +/** +* Return the current state of the plugin. +* This function only makes sense in asynchronous mode. +* @return The pluginState of the plugin. +* +* @see pluginState +*/ +pluginState HadifixProc::getState() { return d->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 HadifixProc::ackFinished() +{ + if (d->state == psFinished) + { + d->state = psIdle; + d->synthFilename = 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 HadifixProc::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. +* +* If the plugin returns True, it must also implement the following methods: +* - @ref synthText +* - @ref getFilename +* - @ref ackFinished +* +* If the plugin returns True, it need not implement @ref sayText . +*/ +bool HadifixProc::supportsSynth() { return true; } + + +void HadifixProc::slotProcessExited(KProcess*) +{ + // kdDebug() << "HadifixProc:hadifixProcExited: Hadifix process has exited." << endl; + pluginState prevState = d->state; + if (d->waitingStop) + { + d->waitingStop = false; + d->state = psIdle; + emit stopped(); + } else { + d->state = psFinished; + if (prevState == psSynthing) + emit synthFinished(); + } +} + +void HadifixProc::slotWroteStdin(KProcess*) +{ + // kdDebug() << "HadifixProc::slotWroteStdin: closing Stdin" << endl; + d->hadifixProc->closeStdin(); +} + + +/***************************************************************************/ + +/** +* Static function to determine whether the voice file is male or female. +* @param mbrola the mbrola executable +* @param voice the voice file +* @param output the output of mbrola will be written into this QString* +* @return HadifixSpeech::MaleGender if the voice is male, +* HadifixSpeech::FemaleGender if the voice is female, +* HadifixSpeech::NoGender if the gender cannot be determined, +* HadifixSpeech::NoVoice if there is an error in the voice file +*/ +HadifixProc::VoiceGender HadifixProc::determineGender(QString mbrola, QString voice, QString *output) +{ + QString command = mbrola + " -i " + voice + " - -"; + + // create a new process + HadifixProc speech; + KShellProcess proc; + proc << command; + connect(&proc, SIGNAL(receivedStdout(KProcess *, char *, int)), + &speech, SLOT(receivedStdout(KProcess *, char *, int))); + connect(&proc, SIGNAL(receivedStderr(KProcess *, char *, int)), + &speech, SLOT(receivedStderr(KProcess *, char *, int))); + + speech.stdOut = QString::null; + speech.stdErr = QString::null; + proc.start (KProcess::Block, KProcess::AllOutput); + + VoiceGender result; + if (!speech.stdErr.isNull() && !speech.stdErr.isEmpty()) { + if (output != 0) + *output = speech.stdErr; + result = NoVoice; + } + else { + if (output != 0) + *output = speech.stdOut; + if (speech.stdOut.contains("female", false)) + result = FemaleGender; + else if (speech.stdOut.contains("male", false)) + result = MaleGender; + else + result = NoGender; + } + + return result; +} + +void HadifixProc::receivedStdout (KProcess *, char *buffer, int buflen) { + stdOut += QString::fromLatin1(buffer, buflen); +} + +void HadifixProc::receivedStderr (KProcess *, char *buffer, int buflen) { + stdErr += QString::fromLatin1(buffer, buflen); +} + +/** + * Returns the name of an XSLT stylesheet that will convert a valid SSML file + * into a format that can be processed by the synth. For example, + * The Festival plugin returns a stylesheet that will convert SSML into + * SABLE. Any tags the synth cannot handle should be stripped (leaving + * their text contents though). The default stylesheet strips all + * tags and converts the file to plain text. + * @return Name of the XSLT file. + */ +QString HadifixProc::getSsmlXsltFilename() +{ + return KGlobal::dirs()->resourceDirs("data").last() + "kttsd/hadifix/xslt/SSMLtoTxt2pho.xsl"; +} diff --git a/kttsd/plugins/hadifix/hadifixproc.h b/kttsd/plugins/hadifix/hadifixproc.h new file mode 100644 index 0000000..225c7ed --- /dev/null +++ b/kttsd/plugins/hadifix/hadifixproc.h @@ -0,0 +1,203 @@ +/*************************************************************************** + hadifixproc.h - description + ------------------- + begin : Mon Okt 14 2002 + copyright : (C) 2002 by Gunnar Schmi Dt + email : gunnar@schmi-dt.de + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef _HADIFIXPROC_H_ +#define _HADIFIXPROC_H_ + +#include <qstringlist.h> + +#include <pluginproc.h> + +class KProcess; + +class HadifixProcPrivate; +class HadifixProc : public PlugInProc{ + Q_OBJECT + + public: + enum VoiceGender { + MaleGender = 2, + FemaleGender = 1, + NoGender = 0, + NoVoice = -1 + }; + + /** Constructor */ + HadifixProc( QObject* parent = 0, const char* name = 0, const QStringList &args = QStringList()); + + /** Destructor */ + ~HadifixProc(); + + /** Initializate the speech */ + virtual bool init (KConfig *config, const QString &configGroup); + + /** + * Say a text. Synthesize and audibilize it. + * @param text The text to be spoken. + * + * If the plugin supports asynchronous operation, it should return immediately + * and emit sayFinished signal when synthesis and audibilizing is finished. + * It must also implement the @ref getState method, which must return + * psFinished, when saying is completed. + */ + virtual void sayText(const QString &text); + + /** + * 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 + * and emit @ref synthFinished signal when synthesis is completed. + * It must also implement the @ref getState method, which must return + * psFinished, when synthesis is completed. + */ + virtual void synthText(const QString &text, const QString &suggestedFilename); + + /** + * Get the generated audio filename from call to @ref synthText. + * @return Name of the audio file the plugin generated. + * Null if no such file. + * + * The plugin must not re-use or delete the filename. The file may not + * be locked when this method is called. The file will be deleted when + * KTTSD is finished using it. + */ + virtual QString getFilename(); + + /** + * 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. + */ + virtual void stopText(); + + /** + * Return the current state of the plugin. + * This function only makes sense in asynchronous mode. + * @return The pluginState of the plugin. + * + * @see pluginState + */ + virtual pluginState getState(); + + /** + * 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. + */ + virtual void ackFinished(); + + /** + * 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. + */ + virtual bool supportsAsync(); + + /** + * 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. + * + * If the plugin returns True, it must also implement the following methods: + * - @ref synthText + * - @ref getFilename + * - @ref ackFinished + * + * If the plugin returns True, it need not implement @ref sayText . + */ + virtual bool supportsSynth(); + + /** + * Synthesize text using a specified configuration. + * @param text The text to synthesize. + * @param hadifix Command to run hadifix (txt2pho). + * @param isMale True to use male voice. + * @param mbrola Command to run mbrola. + * @param voice Voice file for mbrola to use. + * @param volume Volume percent. 100 = normal + * @param time Speed percent. 100 = normal + * @param pitch Frequency. 100 = normal + * @param waveFilename Name of file to synthesize to. + */ + void synth(QString text, + QString hadifix, bool isMale, + QString mbrola, QString voice, + int volume, int time, int pitch, + QTextCodec* codec, + const QString waveFilename); + + /** + * Static function to determine whether the voice file is male or female. + * @param mbrola the mbrola executable + * @param voice the voice file + * @param output the output of mbrola will be written into this QString* + * @return HadifixSpeech::MaleGender if the voice is male, + * HadifixSpeech::FemaleGender if the voice is female, + * HadifixSpeech::NoGender if the gender cannot be determined, + * HadifixSpeech::NoVoice if there is an error in the voice file + */ + static VoiceGender determineGender(QString mbrola, QString voice, QString *output = 0); + + /** + * Returns the name of an XSLT stylesheet that will convert a valid SSML file + * into a format that can be processed by the synth. For example, + * The Festival plugin returns a stylesheet that will convert SSML into + * SABLE. Any tags the synth cannot handle should be stripped (leaving + * their text contents though). The default stylesheet strips all + * tags and converts the file to plain text. + * @return Name of the XSLT file. + */ + virtual QString getSsmlXsltFilename(); + + private slots: + void slotProcessExited(KProcess*); + void slotWroteStdin(KProcess*); + + void receivedStdout (KProcess *, char *buffer, int buflen); + void receivedStderr (KProcess *, char *buffer, int buflen); + + private: + HadifixProcPrivate *d; + + QString stdOut; + QString stdErr; +}; + +#endif diff --git a/kttsd/plugins/hadifix/initialconfig.h b/kttsd/plugins/hadifix/initialconfig.h new file mode 100644 index 0000000..4fac632 --- /dev/null +++ b/kttsd/plugins/hadifix/initialconfig.h @@ -0,0 +1,164 @@ + +/** + * Tries to find hadifix and mbrola by looking onto the hard disk. This is + * neccessary because both hadifix and mbrola do not have standard + * installation directories. + */ +void findInitialConfig() { + QString hadifixDataPath = findHadifixDataPath(); + + defaultHadifixExec = findExecutable("txt2pho", hadifixDataPath+"/../"); + + QStringList list; list += "mbrola"; list += "mbrola-linux-i386"; + defaultMbrolaExec = findExecutable(list, hadifixDataPath+"/../../mbrola/"); + + defaultVoices = findVoices (defaultMbrolaExec, hadifixDataPath); +} + +/** Tries to find the hadifix data path by looking into a number of files. */ +QString findHadifixDataPath () { + QStringList files; + files += "/etc/txt2pho"; + files += QDir::homeDirPath()+"/.txt2phorc"; + + QStringList::iterator it; + for (it = files.begin(); it != files.end(); ++it) { + + QFile file(*it); + if ( file.open(IO_ReadOnly) ) { + QTextStream stream(&file); + + while (!stream.atEnd()) { + QString s = stream.readLine().stripWhiteSpace(); + // look for a line "DATAPATH=..." + + if (s.startsWith("DATAPATH")) { + s = s.mid(8, s.length()-8).stripWhiteSpace(); + if (s.startsWith("=")) { + s = s.mid(1, s.length()-1).stripWhiteSpace(); + if (s.startsWith("/")) + return s; + else { + QFileInfo info (QFileInfo(*it).dirPath() + "/" + s); + return info.absFilePath(); + } + } + } + } + file.close(); + } + } + return "/usr/local/txt2pho/"; +} + +/** Tries to find the an executable by looking onto the hard disk. */ +QString findExecutable (const QStringList &names, const QString &possiblePath) { + // a) Try to find it directly + QStringList::ConstIterator it; + QStringList::ConstIterator itEnd = names.constEnd(); + for (it = names.constBegin(); it != itEnd; ++it) { + QString executable = KStandardDirs::findExe (*it); + if (!executable.isNull() && !executable.isEmpty()) + return executable; + } + + // b) Try to find it in the path specified by the second parameter + for (it = names.constBegin(); it != itEnd; ++it) { + QFileInfo info (possiblePath+*it); + if (info.exists() && info.isExecutable() && info.isFile()) { + return info.absFilePath(); + } + } + + // Both tries failed, so the user has to locate the executable. + return QString::null; +} + +/** Tries to find the voice files by looking onto the hard disk. */ +QStringList findVoices(QString mbrolaExec, const QString &hadifixDataPath) { + + // First of all: + // dereference links to the mbrola executable (if mbrolaExec is a link). + for (int i = 0; i < 10; ++i) { + // If we have a chain of more than ten links something is surely wrong. + QFileInfo info (mbrolaExec); + if (info.exists() && info.isSymLink()) + mbrolaExec = info.readLink(); + } + + // Second: + // create a list of directories that possibly contain voice files + QStringList list; + + // 2a) search near the mbrola executable + QFileInfo info (mbrolaExec); + if (info.exists() && info.isFile() && info.isExecutable()) { + QString mbrolaPath = info.dirPath (true); + list += mbrolaPath; + } + + // 2b) search near the hadifix data path + info.setFile(hadifixDataPath + "../../mbrola"); + QString mbrolaPath = info.dirPath (true) + "/mbrola"; + if (!list.contains(mbrolaPath)) + list += mbrolaPath; + + // 2c) broaden the search by adding subdirs (with a depth of 2) + QStringList subDirs = findSubdirs (list); + QStringList subSubDirs = findSubdirs (subDirs); + list += subDirs; + list += subSubDirs; + + // Third: + // look into each of these directories and search for voice files. + QStringList result; + QStringList::iterator it; + for (it = list.begin(); it != list.end(); ++it) { + QDir baseDir (*it, QString::null, + QDir::Name|QDir::IgnoreCase, QDir::Files); + QStringList files = baseDir.entryList(); + + QStringList::iterator iter; + for (iter = files.begin(); iter != files.end(); ++iter) { + // Voice files start with "MBROLA", but are afterwards binary files + QString filename = *it + "/" + *iter; + QFile file (filename); + if (file.open(IO_ReadOnly)) { + QTextStream stream(&file); + if (!stream.atEnd()) { + QString s = stream.readLine(); + if (s.startsWith("MBROLA")) + if (HadifixProc::determineGender(mbrolaExec, filename) + != HadifixProc::NoVoice + ) + result += filename; + file.close(); + } + } + } + } + return result; +} + +/** Returns a list of subdirs (with absolute paths) */ +QStringList findSubdirs (const QStringList &baseDirs) { + QStringList result; + + QStringList::ConstIterator it; + QStringList::ConstIterator itEnd = baseDirs.constEnd(); + for (it = baseDirs.constBegin(); it != itEnd; ++it) { + // a) get a list of directory names + QDir baseDir (*it, QString::null, + QDir::Name|QDir::IgnoreCase, QDir::Dirs); + QStringList list = baseDir.entryList(); + + // b) produce absolute paths + QStringList::ConstIterator iter; + QStringList::ConstIterator iterEnd = list.constEnd(); + for (iter = list.constBegin(); iter != iterEnd; ++iter) { + if ((*iter != ".") && (*iter != "..")) + result += *it + "/" + *iter; + } + } + return result; +} diff --git a/kttsd/plugins/hadifix/kttsd_hadifixplugin.desktop b/kttsd/plugins/hadifix/kttsd_hadifixplugin.desktop new file mode 100644 index 0000000..2db3e1c --- /dev/null +++ b/kttsd/plugins/hadifix/kttsd_hadifixplugin.desktop @@ -0,0 +1,51 @@ +[Desktop Entry] +Name=Hadifix +Name[ne]=ह्याडिफिक्स +Comment=German hadifix text-to-speech system +Comment[bg]=Синтезатор на глас за немски език Hadifix +Comment[ca]=Sistema de text a veu alemany hadifix +Comment[cs]=Německý systém hlasové syntézy hadifix +Comment[da]=Tysk hadifix tekst-til-tale system +Comment[de]=Deutsches hadifix-Sprachausgabesystem +Comment[el]=Σύστημα κειμένου-σε-ομιλία γερμανικού hadifix +Comment[es]=Sintetizador de texto a voz en alemán hadifix +Comment[et]=Saksa teksti kõneks muutmise süsteem Hadifix +Comment[eu]=Alemanierazko hadifix testutik hizketarako sistema +Comment[fa]=سیستم متن به گفتار hadifix آلمانی +Comment[fi]=Saksalainen hadifix teksti puheeksi -systeemi +Comment[fr]=Système allemand de synthèse vocale hadifix +Comment[ga]=Córas téacs-go-caint Gearmáinise hadifix +Comment[gl]=Sistema alemao de texto-para-fala hadifix +Comment[hu]=Hadifix szövegfelolvasó (Mbrola-alapú) +Comment[is]=Þýska hadifix texti-í-tal kerfið +Comment[it]=Sistema di pronuncia tedesca Hadifix +Comment[ja]=ドイツ語 hadifix テキスト読み上げシステム +Comment[ka]=გერმანული hadifix ტექსტის გახმოვანების სისტემა +Comment[km]=ប្រព័ន្ធអត្ថបទដែលត្រូវនិយាយ hadifix អាល្លឺម៉ង់ +Comment[mk]=Германски hadifix систем за текст-во-говор +Comment[ms]=Sistem teks-ke-tutur hadifiks Jerman +Comment[mt]=Sistema test-għal-vuċi hadifix Ġermaniż +Comment[nb]=Tysk hadifix system for tekst-til-tale +Comment[nds]=Düütsch Blicksnuut Hadifix +Comment[ne]=जर्मनी ह्याडिफिक्स पाठ वाचक प्रणाली +Comment[nl]=Duits hadifix tekst-tot-spraak-systeem +Comment[pa]=ਜਰਮਨ hadifix ਪਾਠ ਤੋਂ ਬੋਲੀ ਸਿਸਟਮ +Comment[pl]=Niemiecki system syntezy mowy hadifix +Comment[pt]=Sistema alemão de texto-para-voz hadifix +Comment[pt_BR]=Sistema de conversão de texto para áudio almeão hadifix +Comment[ru]=Немецкая система синтеза речи Hadifix +Comment[sk]=Nemecký systém text-na-reč hadifix +Comment[sl]=Nemški sistem besedila v govor hadifix +Comment[sr]=Hadifix, немачки систем за текст-у-говор +Comment[sr@Latn]=Hadifix, nemački sistem za tekst-u-govor +Comment[sv]=Tyska Hadifix text-till-tal system +Comment[ta]=ஜெர்மன் ஹாடிஃபிக்ஸ் உரையில் இருந்து பேச்சு அமைப்பு +Comment[tg]=Системаи олмонии таҳлили овози hadifix +Comment[tr]=Alman hadifix metinden konuşmaya sistemi +Comment[uk]=Німецька система синтезу мовлення hadifix +Comment[vi]=Hệ thống tổng hợp tiếng nói Đức hadifix +Comment[zh_TW]=德語 hadifix 文字轉語音系統 +Type=Service +ServiceTypes=KTTSD/SynthPlugin +X-KDE-Library=libkttsd_hadifixplugin +X-KDE-Languages=de,hu diff --git a/kttsd/plugins/hadifix/voicefileui.ui b/kttsd/plugins/hadifix/voicefileui.ui new file mode 100644 index 0000000..6c038f7 --- /dev/null +++ b/kttsd/plugins/hadifix/voicefileui.ui @@ -0,0 +1,119 @@ +<!DOCTYPE UI><UI version="3.1" stdsetdef="1"> +<class>VoiceFileWidget</class> +<widget class="QWidget"> + <property name="name"> + <cstring>VoiceFileWidget</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>452</width> + <height>117</height> + </rect> + </property> + <property name="caption"> + <string>Selecting Voice File</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>11</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QLabel" row="0" column="0"> + <property name="name"> + <cstring>voiceFileLabel</cstring> + </property> + <property name="text"> + <string>Path of the voice file:</string> + </property> + </widget> + <widget class="KURLRequester" row="0" column="1"> + <property name="name"> + <cstring>voiceFileURL</cstring> + </property> + <property name="frameShape"> + <enum>NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>Plain</enum> + </property> + </widget> + <widget class="QButtonGroup" row="1" column="0" rowspan="1" colspan="2"> + <property name="name"> + <cstring>genderOption</cstring> + </property> + <property name="title"> + <string>Gender</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>11</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QRadioButton" row="0" column="0"> + <property name="name"> + <cstring>femaleOption</cstring> + </property> + <property name="text"> + <string>Female</string> + </property> + </widget> + <widget class="QRadioButton" row="0" column="1"> + <property name="name"> + <cstring>maleOption</cstring> + </property> + <property name="text"> + <string>Male</string> + </property> + </widget> + <widget class="KPushButton" row="0" column="2"> + <property name="name"> + <cstring>genderButton</cstring> + </property> + <property name="text"> + <string>Try to Determine From Voice File</string> + </property> + </widget> + </grid> + </widget> + </grid> +</widget> +<connections> + <connection> + <sender>genderButton</sender> + <signal>clicked()</signal> + <receiver>VoiceFileWidget</receiver> + <slot>genderButton_clicked()</slot> + </connection> +</connections> +<includes> + <include location="global" impldecl="in implementation">kurlrequesterdlg.h</include> + <include location="global" impldecl="in implementation">kmessagebox.h</include> + <include location="local" impldecl="in implementation">hadifixproc.h</include> + <include location="local" impldecl="in implementation">voicefileui.ui.h</include> +</includes> +<variables> + <variable access="public">QString mbrola;</variable> +</variables> +<slots> + <slot>genderButton_clicked()</slot> +</slots> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>kurlrequester.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>kpushbutton.h</includehint> +</includehints> +</UI> diff --git a/kttsd/plugins/hadifix/voicefileui.ui.h b/kttsd/plugins/hadifix/voicefileui.ui.h new file mode 100644 index 0000000..9d37375 --- /dev/null +++ b/kttsd/plugins/hadifix/voicefileui.ui.h @@ -0,0 +1,35 @@ +/**************************************************************************** +** ui.h extension file, included from the uic-generated form implementation. +** +** If you wish to add, delete or rename functions or slots use +** Qt Designer which will update this file, preserving your code. Create an +** init() function in place of a constructor, and a destroy() function in +** place of a destructor. +*****************************************************************************/ + + +void VoiceFileWidget::genderButton_clicked() +{ + HadifixProc::VoiceGender gender; + QString details; + gender = HadifixProc::determineGender(mbrola, voiceFileURL->url(), &details); + + if (gender == HadifixProc::MaleGender) { + maleOption->setChecked (true); + femaleOption->setChecked (false); + } + else if (gender == HadifixProc::FemaleGender) { + maleOption->setChecked (false); + femaleOption->setChecked (true); + } + else if (gender == HadifixProc::NoGender) { + KMessageBox::sorry (this, + i18n("The gender of the voice file %1 could not be detected.").arg(voiceFileURL->url()), + i18n("Trying to Determine the Gender - Hadifix Plug In")); + } + else { + KMessageBox::detailedSorry (this, + i18n("The file %1 does not seem to be a voice file.").arg(voiceFileURL->url()), + details, i18n("Trying to Determine the Gender - Hadifix Plug In")); + } +} |