1 /***************************************************** vim:set ts=4 sw=4 sts=4:
4 This class is in charge of converting SSML text into a format that can
5 be handled by individual synths.
8 (C) 2004 by Paul Giannaros <ceruleanblaze@gmail.com>
9 (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net>
11 Original author: Paul Giannaros <ceruleanblaze@gmail.com>
12 ******************************************************************************/
14 /***************************************************************************
16 * This program is free software; you can redistribute it and/or modify *
17 * it under the terms of the GNU General Public License as published by *
18 * the Free Software Foundation; version 2 of the License. *
20 ***************************************************************************/
24 #include <qstringlist.h>
27 #include <qtextstream.h>
30 #include <kdeversion.h>
31 #include <kstandarddirs.h>
33 #include <ktempfile.h>
36 // SSMLConvert includes.
37 #include "ssmlconvert.h"
38 #include "ssmlconvert.moc"
41 SSMLConvert::SSMLConvert() {
42 m_talkers
= QStringList();
47 /// Constructor. Set the talkers to be used as reference for entered text.
48 SSMLConvert::SSMLConvert(const QStringList
&talkers
) {
55 SSMLConvert::~SSMLConvert() {
57 if (!m_inFilename
.isEmpty()) QFile::remove(m_inFilename
);
58 if (!m_outFilename
.isEmpty()) QFile::remove(m_outFilename
);
61 /// Set the talkers to be used as reference for entered text.
62 void SSMLConvert::setTalkers(const QStringList
&talkers
) {
66 QString
SSMLConvert::extractTalker(const QString
&talkercode
) {
67 QString t
= talkercode
.section("synthesizer=", 1, 1);
68 t
= t
.section('"', 1, 1);
69 if(t
.contains("flite"))
72 return t
.left(t
.find(" ")).toLower();
76 * Return the most appropriate talker for the text to synth talker code.
77 * @param text the text that will be parsed.
78 * @returns the appropriate talker for the job as a talker code.
80 * The appropriate talker is the one that has the most features that are required in some
81 * SSML markup. In the future i'm hoping to make the importance of individual features
82 * configurable, but better to walk before you can run.
83 * Currently, the searching method in place is like a filter: Those that meet the criteria we're
84 * searchin for stay while others are sifted out. This should leave us with the right talker to use.
85 * It's not a very good method, but should be appropriate in most cases and should do just fine for now.
87 * As it stands, here is the list of things that are looked for, in order of most importance:
89 * Obviously the most important. If a language is specified, look for the talkers that support it.
90 * Default to en (or some form of en - en_US, en_GB, etc). Only one language at a time is allowed
91 * at the moment, and must be specified in the root speak element (<speak xml:lang="en-US">)
93 * If a gender is specified, look for talkers that comply. There is no default so if no gender is
94 * specified, no talkers will be removed. The only gender that will be searched for is the one
95 * specified in the root speak element. This should change in the future.
97 * Check if prosody modification is allowed by the talker. Currently this is hardcoded (it
98 * is stated which talkers do and do not in a variable somewhere).
100 * Bear in mind that the XSL stylesheet that will be applied to the SSML is the same regardless
101 * of the how the talker is chosen, meaning that you don't lose some features of the talker if this
102 * search doesn't encompass them.
104 * QDom is the item of choice for the matching. Just walk the tree..
106 QString
SSMLConvert::appropriateTalker(const QString
&text
) const {
108 ssml
.setContent(text
, false); // No namespace processing.
109 /// Matches are stored here. Obviously to begin with every talker matches.
110 QStringList matches
= m_talkers
;
112 /// Check that this is (well formed) SSML and all our searching will not be in vain.
113 QDomElement root
= ssml
.documentElement();
114 if(root
.tagName() != "speak") {
116 return QString::null
;
120 * For each rule that we are looking through, iterate over all currently
121 * matching talkers and remove all the talkers that don't match.
123 * Storage for talker code components.
125 QString talklang
, talkvoice
, talkgender
, talkvolume
, talkrate
, talkname
;
127 kdDebug() << "SSMLConvert::appropriateTalker: BEFORE LANGUAGE SEARCH: " << matches
.join(" ") << endl
;;
131 if(root
.hasAttribute("xml:lang")) {
132 QString lang
= root
.attribute("xml:lang");
133 kdDebug() << "SSMLConvert::appropriateTalker: xml:lang found (" << lang
<< ")" << endl
;
134 /// If it is set to en*, then match all english speakers. They all sound the same anyways.
135 if(lang
.contains("en-")) {
136 kdDebug() << "SSMLConvert::appropriateTalker: English" << endl
;
139 /// Find all hits and place them in matches. We don't search for the closing " because if
140 /// the talker emits lang="en-UK" or something we'll be ignoring it, which we don't what.
141 matches
= matches
.grep("lang=\"" + lang
);
144 kdDebug() << "SSMLConvert::appropriateTalker: no xml:lang found. Defaulting to en.." << endl
;
145 matches
= matches
.grep("lang=\"en");
148 kdDebug() << "SSMLConvert::appropriateTalker: AFTER LANGUAGE SEARCH: " << matches
.join(" ") << endl
;;
152 * If, for example, male is specified and only female is found,
153 * ignore the choice and just use female.
155 if(root
.hasAttribute("gender")) {
156 QString gender
= root
.attribute("gender");
157 kdDebug() << "SSMLConvert::appropriateTalker: gender found (" << gender
<< ")" << endl
;
158 /// If the gender found is not 'male' or 'female' then ignore it.
159 if(!(gender
== "male" || gender
== "female")) {
160 /// Make sure that we don't strip away all the talkers because of no matches.
161 if(matches
.grep("gender=\"" + gender
).count() >= 1)
162 matches
= matches
.grep("gender=\"" + gender
);
166 kdDebug() << "SSMLConvert::appropriateTalker: no gender found." << endl
;
171 * Search for talkers that allow modification of the synth output - louder, higher,
172 * slower, etc. There should be a direct way to query each synth to find out if this
173 * is supported (some function in PlugInConf), but for now, hardcode all the way :(
175 /// Known to support (feel free to add to the list and if search):
176 /// Festival Int (not flite), Hadifix
177 if(matches
.grep("synthesizer=\"Festival Interactive").count() >= 1 ||
178 matches
.grep("synthesizer=\"Hadifix").count() >= 1) {
180 kdDebug() << "SSMLConvert::appropriateTalker: Prosody allowed" << endl
;
181 QStringList tmpmatches
= matches
.grep("synthesizer=\"Festival Interactive");
182 matches
= matches
.grep("synthesizer=\"Hadifix");
183 matches
= tmpmatches
+ matches
;
186 kdDebug() << "SSMLConvert::appropriateTalker: No prosody-supporting talkers found" << endl
;
188 /// Return the first match that complies. Maybe a discrete way to
189 /// choose between all the matches could be offered in the future. Some form of preference.
194 * Applies the spreadsheet for a talker to the SSML and returns the talker-native output.
195 * @param text The markup to apply the spreadsheet to.
196 * @param xsltFilename The name of the stylesheet file that will be applied (i.e freetts, flite).
197 * @returns False if an error occurs.
199 * This converts a piece of SSML into a format the given talker can understand. It applies
200 * an XSLT spreadsheet to the SSML and returns the output.
202 * Emits transformFinished signal when completed. Caller then calls getOutput to retrieve
203 * the transformed text.
206 bool SSMLConvert::transform(const QString
&text
, const QString
&xsltFilename
) {
207 m_xsltFilename
= xsltFilename
;
208 /// Write @param text to a temporary file.
209 KTempFile
inFile(locateLocal("tmp", "kttsd-"), ".ssml");
210 m_inFilename
= inFile
.file()->name();
211 QTextStream
* wstream
= inFile
.textStream();
214 kdDebug() << "SSMLConvert::transform: Can't write to " << m_inFilename
<< endl
;;
217 // TODO: Is encoding an issue here?
218 // TODO: It would be nice if we detected whether the XML is properly formed
219 // with the required xml processing instruction and encoding attribute. If
220 // not wrap it in such. But maybe this should be handled by SpeechData::setText()?
223 #if KDE_VERSION >= KDE_MAKE_VERSION (3,3,0)
227 // Get a temporary output file name.
228 KTempFile
outFile(locateLocal("tmp", "kttsd-"), ".output");
229 m_outFilename
= outFile
.file()->name();
231 // outFile.unlink(); // only activate this if necessary.
233 /// Spawn an xsltproc process to apply our stylesheet to our SSML file.
234 m_xsltProc
= new KProcess
;
235 *m_xsltProc
<< "xsltproc";
236 *m_xsltProc
<< "-o" << m_outFilename
<< "--novalid"
237 << m_xsltFilename
<< m_inFilename
;
238 // Warning: This won't compile under KDE 3.2. See FreeTTS::argsToStringList().
239 // kdDebug() << "SSMLConvert::transform: executing command: " <<
240 // m_xsltProc->args() << endl;
242 connect(m_xsltProc
, SIGNAL(processExited(KProcess
*)),
243 this, SLOT(slotProcessExited(KProcess
*)));
244 if (!m_xsltProc
->start(KProcess::NotifyOnExit
, KProcess::NoCommunication
))
246 kdDebug() << "SSMLConvert::transform: Error starting xsltproc" << endl
;
249 m_state
= tsTransforming
;
253 void SSMLConvert::slotProcessExited(KProcess
* /*proc*/)
255 m_xsltProc
->deleteLater();
257 m_state
= tsFinished
;
258 emit
transformFinished();
262 * Returns current processing state.
264 int SSMLConvert::getState() { return m_state
; }
267 * Returns the output from call to transform.
269 QString
SSMLConvert::getOutput()
271 /// Read back the data that was written to /tmp/fileName.output.
272 QFile
readfile(m_outFilename
);
273 if(!readfile
.open(QIODevice::ReadOnly
)) {
274 /// uhh yeah... Issues writing to the SSML file.
275 kdDebug() << "SSMLConvert::slotProcessExited: Could not read file " << m_outFilename
<< endl
;
276 return QString::null
;
278 QTextStream
rstream(&readfile
);
279 QString convertedData
= rstream
.read();
282 // kdDebug() << "SSMLConvert::slotProcessExited: Read SSML file at " + m_inFilename + " and created " + m_outFilename + " based on the stylesheet at " << m_xsltFilename << endl;
285 QFile::remove(m_inFilename
);
286 m_inFilename
= QString::null
;
287 QFile::remove(m_outFilename
);
288 m_outFilename
= QString::null
;
290 // Ready for another transform.
293 return convertedData
;