qt3support--
[kdeaccessibility.git] / kttsd / kttsd / ssmlconvert.cpp
blob932ebaeccc46aafa2b47343342dee66c97470481
1 /***************************************************** vim:set ts=4 sw=4 sts=4:
2 SSMLConvert class
4 This class is in charge of converting SSML text into a format that can
5 be handled by individual synths.
6 -------------------
7 Copyright:
8 (C) 2004 by Paul Giannaros <ceruleanblaze@gmail.com>
9 (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net>
10 -------------------
11 Original author: Paul Giannaros <ceruleanblaze@gmail.com>
12 ******************************************************************************/
14 /***************************************************************************
15 * *
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. *
19 * *
20 ***************************************************************************/
22 // Qt includes.
23 #include <qstring.h>
24 #include <qstringlist.h>
25 #include <qdom.h>
26 #include <qfile.h>
27 #include <qtextstream.h>
29 // KDE includes.
30 #include <kdeversion.h>
31 #include <kstandarddirs.h>
32 #include <kprocess.h>
33 #include <ktempfile.h>
34 #include <kdebug.h>
36 // SSMLConvert includes.
37 #include "ssmlconvert.h"
38 #include "ssmlconvert.moc"
40 /// Constructor.
41 SSMLConvert::SSMLConvert() {
42 m_talkers = QStringList();
43 m_xsltProc = 0;
44 m_state = tsIdle;
47 /// Constructor. Set the talkers to be used as reference for entered text.
48 SSMLConvert::SSMLConvert(const QStringList &talkers) {
49 m_talkers = talkers;
50 m_xsltProc = 0;
51 m_state = tsIdle;
54 /// Destructor.
55 SSMLConvert::~SSMLConvert() {
56 delete m_xsltProc;
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) {
63 m_talkers = 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"))
70 return "flite";
71 else
72 return t.left(t.find(" ")).toLower();
75 /**
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:
88 * - Language
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">)
92 * - Gender
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.
96 * - Prosody
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 {
107 QDomDocument ssml;
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") {
115 // Not SSML.
116 return QString::null;
119 /**
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;;
129 * Language searching
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;
137 lang = "en";
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);
143 else {
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;;
151 * Gender searching
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);
165 else {
166 kdDebug() << "SSMLConvert::appropriateTalker: no gender found." << endl;
170 * Prosody
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;
185 else
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.
190 return matches[0];
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();
212 if (wstream == 0) {
213 /// wtf...
214 kdDebug() << "SSMLConvert::transform: Can't write to " << m_inFilename << endl;;
215 return false;
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()?
221 *wstream << text;
222 inFile.close();
223 #if KDE_VERSION >= KDE_MAKE_VERSION (3,3,0)
224 inFile.sync();
225 #endif
227 // Get a temporary output file name.
228 KTempFile outFile(locateLocal("tmp", "kttsd-"), ".output");
229 m_outFilename = outFile.file()->name();
230 outFile.close();
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;
247 return false;
249 m_state = tsTransforming;
250 return true;
253 void SSMLConvert::slotProcessExited(KProcess* /*proc*/)
255 m_xsltProc->deleteLater();
256 m_xsltProc = 0;
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();
280 readfile.close();
282 // kdDebug() << "SSMLConvert::slotProcessExited: Read SSML file at " + m_inFilename + " and created " + m_outFilename + " based on the stylesheet at " << m_xsltFilename << endl;
284 // Clean up.
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.
291 m_state = tsIdle;
293 return convertedData;