1 /***************************************************** vim:set ts=4 sw=4 sts=4:
2 Sentence Boundary Detection Filter class.
5 (C) 2005 by Gary Cramblitt <garycramblitt@comcast.net>
7 Original author: Gary Cramblitt <garycramblitt@comcast.net>
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation; either version 2 of the License, or
12 (at your option) any later version.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 ******************************************************************************/
28 #include <QtCore/QRegExp>
29 #include <QtXml/qdom.h>
30 #include <QtGui/QApplication>
31 #include <QtCore/QCustomEvent>
37 #include <kconfiggroup.h>
41 #include "talkercode.h"
46 SbdThread::SbdThread( QObject
*parent
) :
54 /*virtual*/ SbdThread::~SbdThread()
59 * Get/Set text being processed.
61 void SbdThread::setText( const QString
& text
) { m_text
= text
; }
62 QString
SbdThread::text() { return m_text
; }
67 void SbdThread::setTalkerCode( TalkerCode
* talkerCode
) { m_talkerCode
= talkerCode
; }
68 TalkerCode
* SbdThread::talkerCode() { return m_talkerCode
; }
71 * Set Sentence Boundary Regular Expression.
72 * This method will only be called if the application overrode the default.
74 * @param re The sentence delimiter regular expression.
76 void SbdThread::setSbRegExp( const QString
& re
) { m_re
= re
; }
79 * The configured Sentence Boundary Regular Expression.
81 * @param re The sentence delimiter regular expression.
83 void SbdThread::setConfiguredSbRegExp( const QString
& re
) { m_configuredRe
= re
; }
86 * The configured Sentence Boundary that replaces SB regular expression.
88 * @param sb The sentence boundary replacement.
91 void SbdThread::setConfiguredSentenceBoundary( const QString
& sb
) { m_configuredSentenceBoundary
= sb
; }
94 * Did this filter do anything? If the filter returns the input as output
95 * unmolested, it should return False when this method is called.
97 void SbdThread::setWasModified(bool wasModified
) { m_wasModified
= wasModified
; }
98 bool SbdThread::wasModified() { return m_wasModified
; }
100 // Given a tag name, returns SsmlElemType.
101 SbdThread::SsmlElemType
SbdThread::tagToSsmlElemType( const QString tagName
)
103 if ( tagName
== "speak" ) return etSpeak
;
104 if ( tagName
== "voice" ) return etVoice
;
105 if ( tagName
== "prosody" ) return etProsody
;
106 if ( tagName
== "emphasis" ) return etEmphasis
;
107 if ( tagName
== "break" ) return etBreak
;
108 if ( tagName
== "s" ) return etPS
;
109 if ( tagName
== "p" ) return etPS
;
113 // Parses an SSML element, pushing current settings onto the context stack.
114 void SbdThread::pushSsmlElem( SsmlElemType et
, const QDomElement
& elem
)
116 // TODO: Need to convert relative values into absolute values and also convert
117 // only to values recognized by SSML2SABLE stylesheet. Either that or enhance all
118 // the synth stylesheets.
119 QDomNamedNodeMap attrList
= elem
.attributes();
120 int attrCount
= attrList
.count();
124 SpeakElem e
= m_speakStack
.top();
125 for ( int ndx
=0; ndx
< attrCount
; ++ndx
)
127 QDomAttr a
= attrList
.item( ndx
).toAttr();
128 if ( a
.name() == "lang" ) e
.lang
= a
.value();
130 m_speakStack
.push( e
);
133 VoiceElem e
= m_voiceStack
.top();
134 // TODO: Since Festival chokes on <voice> tags, don't output them at all.
135 // This means we can't support voice changes, and probably more irritatingly,
136 // gender changes either.
137 m_voiceStack
.push( e
);
140 ProsodyElem e
= m_prosodyStack
.top();
141 for ( int ndx
=0; ndx
< attrCount
; ++ndx
)
143 QDomAttr a
= attrList
.item( ndx
).toAttr();
144 if ( a
.name() == "pitch" ) e
.pitch
= a
.value();
145 if ( a
.name() == "contour" ) e
.contour
= a
.value();
146 if ( a
.name() == "range" ) e
.range
= a
.value();
147 if ( a
.name() == "rate" ) e
.rate
= a
.value();
148 if ( a
.name() == "duration" ) e
.duration
= a
.value();
149 if ( a
.name() == "volume" ) e
.volume
= a
.value();
151 m_prosodyStack
.push( e
);
154 EmphasisElem e
= m_emphasisStack
.top();
155 for ( int ndx
=0; ndx
< attrCount
; ++ndx
)
157 QDomAttr a
= attrList
.item( ndx
).toAttr();
158 if ( a
.name() == "level" ) e
.level
= a
.value();
160 m_emphasisStack
.push( e
);
163 PSElem e
= m_psStack
.top();
164 for ( int ndx
=0; ndx
< attrCount
; ++ndx
)
166 QDomAttr a
= attrList
.item( ndx
).toAttr();
167 if ( a
.name() == "lang" ) e
.lang
= a
.value();
175 // Given an attribute name and value, constructs an XML representation of the attribute,
176 // i.e., name="value".
177 QString
SbdThread::makeAttr( const QString
& name
, const QString
& value
)
179 if ( value
.isEmpty() ) return QString();
180 return " " + name
+ "=\"" + value
+ "\"";
183 // Returns an XML representation of an SSML tag from the top of the context stack.
184 QString
SbdThread::makeSsmlElem( SsmlElemType et
)
190 // Must always output speak tag, otherwise kttsd won't think each sentence is SSML.
191 // For all other tags, only output the tag if it contains at least one attribute.
193 SpeakElem e
= m_speakStack
.top();
195 if ( !e
.lang
.isEmpty() ) s
+= makeAttr( "lang", e
.lang
);
199 VoiceElem e
= m_voiceStack
.top();
200 a
+= makeAttr( "lang", e
.lang
);
201 a
+= makeAttr( "gender", e
.gender
);
202 a
+= makeAttr( "age", QString::number(e
.age
) );
203 a
+= makeAttr( "name", e
.name
);
204 a
+= makeAttr( "variant", e
.variant
);
205 if ( !a
.isEmpty() ) s
= "<voice" + a
+ '>';
208 ProsodyElem e
= m_prosodyStack
.top();
209 a
+= makeAttr( "pitch", e
.pitch
);
210 a
+= makeAttr( "contour", e
.contour
);
211 a
+= makeAttr( "range", e
.range
);
212 a
+= makeAttr( "rate", e
.rate
);
213 a
+= makeAttr( "duration", e
.duration
);
214 a
+= makeAttr( "volume", e
.volume
);
215 if ( !a
.isEmpty() ) s
= "<prosody" + a
+ '>';
218 EmphasisElem e
= m_emphasisStack
.top();
219 a
+= makeAttr( "level", e
.level
);
220 if ( !a
.isEmpty() ) s
= "<emphasis" + a
+ '>';
229 // Pops element from the indicated context stack.
230 void SbdThread::popSsmlElem( SsmlElemType et
)
234 case etSpeak
: m_speakStack
.pop(); break;
235 case etVoice
: m_voiceStack
.pop(); break;
236 case etProsody
: m_prosodyStack
.pop(); break;
237 case etEmphasis
: m_emphasisStack
.pop(); break;
238 case etPS
: m_psStack
.pop(); break;
243 // Returns an XML representation of a break element.
244 QString
SbdThread::makeBreakElem( const QDomElement
& e
)
246 QString s
= "<break";
247 QDomNamedNodeMap attrList
= e
.attributes();
248 int attrCount
= attrList
.count();
249 for ( int ndx
=0; ndx
< attrCount
; ++ndx
)
251 QDomAttr a
= attrList
.item( ndx
).toAttr();
252 s
+= makeAttr( a
.name(), a
.value() );
258 // Converts a text fragment into a CDATA section.
259 QString
SbdThread::makeCDATA( const QString
& text
)
261 QString s
= "<![CDATA[";
267 // Returns an XML representation of an utterance node consisting of voice,
268 // prosody, and emphasis elements.
269 QString
SbdThread::makeSentence( const QString
& text
)
272 QString v
= makeSsmlElem( etVoice
);
273 QString p
= makeSsmlElem( etProsody
);
274 QString e
= makeSsmlElem( etEmphasis
);
275 // TODO: Lang settings from psStack.
276 if ( !v
.isEmpty() ) s
+= v
;
277 if ( !p
.isEmpty() ) s
+= p
;
278 if ( !e
.isEmpty() ) s
+= e
;
279 // Escape ampersands and less thans.
280 QString newText
= text
;
281 newText
.replace(QRegExp("&(?!amp;)"), "&");
282 newText
.replace(QRegExp("<(?!lt;)"), "<");
284 if ( !e
.isEmpty() ) s
+= "</emphasis>";
285 if ( !p
.isEmpty() ) s
+= "</prosody>";
286 if ( !v
.isEmpty() ) s
+= "</voice>";
290 // Starts a sentence by returning a speak tag.
291 QString
SbdThread::startSentence()
293 if ( m_sentenceStarted
) return QString();
295 s
+= makeSsmlElem( etSpeak
);
296 m_sentenceStarted
= true;
300 // Ends a sentence and appends a Tab.
301 QString
SbdThread::endSentence()
303 if ( !m_sentenceStarted
) return QString();
304 QString s
= "</speak>";
306 m_sentenceStarted
= false;
310 // Parses a node of the SSML tree and recursively parses its children.
311 // Returns the filtered text with each sentence a complete ssml tree.
312 QString
SbdThread::parseSsmlNode( QDomNode
& n
, const QString
& re
)
315 switch ( n
.nodeType() )
317 case QDomNode::ElementNode
: { // = 1
318 QDomElement e
= n
.toElement();
319 QString tagName
= e
.tagName();
320 SsmlElemType et
= tagToSsmlElemType( tagName
);
329 pushSsmlElem( et
, e
);
330 QDomNode t
= n
.firstChild();
331 while ( !t
.isNull() )
333 result
+= parseSsmlNode( t
, re
);
338 result
+= endSentence();
343 // Break elements are empty.
344 result
+= makeBreakElem( e
);
346 // Ignore any elements we don't recognize.
350 case QDomNode::AttributeNode
: { // = 2
352 case QDomNode::TextNode
: { // = 3
353 QString s
= parsePlainText( n
.toText().data(), re
);
355 // d.replace("\t", "\\t");
356 // kDebug() << "SbdThread::parseSsmlNode: parsedPlainText = [" << d << "]";
357 QStringList sentenceList
= s
.split( '\t', QString::SkipEmptyParts
);
358 int lastNdx
= sentenceList
.count() - 1;
359 for ( int ndx
=0; ndx
< lastNdx
; ++ndx
)
361 result
+= startSentence();
362 result
+= makeSentence( sentenceList
[ndx
] );
363 result
+= endSentence();
365 // Only output sentence boundary if last text fragment ended a sentence.
368 result
+= startSentence();
369 result
+= makeSentence( sentenceList
[lastNdx
] );
370 if ( s
.endsWith( '\t' ) ) result
+= endSentence();
373 case QDomNode::CDATASectionNode
: { // = 4
374 QString s
= parsePlainText( n
.toCDATASection().data(), re
);
375 QStringList sentenceList
= s
.split( '\t', QString::SkipEmptyParts
);
376 int lastNdx
= sentenceList
.count() - 1;
377 for ( int ndx
=0; ndx
< lastNdx
; ++ndx
)
379 result
+= startSentence();
380 result
+= makeSentence( makeCDATA( sentenceList
[ndx
] ) );
381 result
+= endSentence();
383 // Only output sentence boundary if last text fragment ended a sentence.
386 result
+= startSentence();
387 result
+= makeSentence( makeCDATA( sentenceList
[lastNdx
] ) );
388 if ( s
.endsWith( '\t' ) ) result
+= endSentence();
391 case QDomNode::EntityReferenceNode
: { // = 5
393 case QDomNode::EntityNode
: { // = 6
395 case QDomNode::ProcessingInstructionNode
: { // = 7
397 case QDomNode::CommentNode
: { // = 8
399 case QDomNode::DocumentNode
: { // = 9
401 case QDomNode::DocumentTypeNode
: { // = 10
403 case QDomNode::DocumentFragmentNode
: { // = 11
405 case QDomNode::NotationNode
: { // = 12
407 case QDomNode::BaseNode
: { // = 21
409 case QDomNode::CharacterDataNode
: { // = 22
416 QString
SbdThread::parseSsml( const QString
& inputText
, const QString
& re
)
418 QRegExp sentenceDelimiter
= QRegExp( re
);
420 // Read the text into xml dom tree.
421 QDomDocument
doc( "" );
422 // If an error occurs parsing the SSML, return "invalid S S M L".
423 if ( !doc
.setContent( inputText
) ) return i18n("Invalid S S M L.");
425 // Set up context stacks and set defaults for all element attributes.
426 m_speakStack
.clear();
427 m_voiceStack
.clear();
428 m_prosodyStack
.clear();
429 m_emphasisStack
.clear();
431 SpeakElem se
= { "" };
432 m_speakStack
.push ( se
);
433 VoiceElem ve
= {"", "neutral", 40, "", ""};
434 m_voiceStack
.push( ve
);
435 ProsodyElem pe
= { "medium", "", "medium", "medium", "", "medium" };
436 m_prosodyStack
.push( pe
);
437 EmphasisElem em
= { "" };
438 m_emphasisStack
.push( em
);
440 m_psStack
.push ( pse
);
442 // This flag is used to close out a previous sentence.
443 m_sentenceStarted
= false;
445 // Get the root element (speak) and recursively process its children.
446 QDomElement docElem
= doc
.documentElement();
447 QDomNode n
= docElem
.firstChild();
448 QString ssml
= parseSsmlNode( docElem
, re
);
450 // Close out last sentence.
451 if ( m_sentenceStarted
) ssml
+= "</speak>";
456 // Parses code. Each newline is converted into a tab character (\t).
457 QString
SbdThread::parseCode( const QString
& inputText
)
459 QString temp
= inputText
;
460 // Replace newlines with tabs.
461 temp
.replace('\n','\t');
462 // Remove leading spaces.
463 temp
.replace(QRegExp("\\t +"), "\t");
464 // Remove trailing spaces.
465 temp
.replace(QRegExp(" +\\t"), "\t");
466 // Remove blank lines.
467 temp
.replace(QRegExp("\t\t+"),"\t");
471 // Parses plain text.
472 QString
SbdThread::parsePlainText( const QString
& inputText
, const QString
& re
)
474 // kDebug() << "SbdThread::parsePlainText: parsing " << inputText << " with re " << re;
475 QRegExp sentenceDelimiter
= QRegExp( re
);
476 QString temp
= inputText
;
477 // Replace sentence delimiters with tab.
478 temp
.replace(sentenceDelimiter
, m_configuredSentenceBoundary
);
479 // Replace remaining newlines with spaces.
480 temp
.replace('\n',' ');
481 temp
.replace('\r',' ');
482 // Remove leading spaces.
483 temp
.replace(QRegExp("\\t +"), "\t");
484 // Remove trailing spaces.
485 temp
.replace(QRegExp(" +\\t"), "\t");
486 // Remove blank lines.
487 temp
.replace(QRegExp("\t\t+"),"\t");
488 // kDebug() << "SbdThread::parsePlainText: returning " << temp;
492 // This is where the real work takes place.
493 /*virtual*/ void SbdThread::run()
495 // kDebug() << "SbdThread::run: processing text = " << m_text;
497 // TODO: Determine if we should do anything or not.
498 m_wasModified
= true;
500 // Determine what kind of input text we are dealing with.
502 if ( KttsUtils::hasRootElement( m_text
, "speak" ) )
506 // Examine just the first 500 chars to see if it is code.
507 QString p
= m_text
.left( 500 );
508 if ( p
.contains( QRegExp( "(/\\*)|(if\\b\\()|(^#include\\b)" ) ) )
514 // If application specified a sentence delimiter regular expression, use that,
515 // otherwise use configured default.
517 if ( re
.isEmpty() ) re
= m_configuredRe
;
519 // Replace spaces, tabs, and formfeeds with a single space.
520 m_text
.replace(QRegExp("[ \\t\\f]+"), " ");
522 // kDebug() << "SbdThread::run: textType = " << textType;
524 // Perform the filtering based on type of text.
528 m_text
= parseSsml( m_text
, re
);
532 m_text
= parseCode( m_text
);
536 m_text
= parsePlainText( m_text
, re
);
540 // Clear app-specified sentence delimiter. App must call setSbRegExp for each conversion.
543 // kDebug() << "SbdThread::run: filtered text = " << m_text;
545 // Result is in m_text;
547 // Post an event. We need to emit filterFinished signal, but not from the
549 QEvent
* ev
= new QEvent(QEvent::Type(QEvent::User
+ 301));
550 QApplication::postEvent(this, ev
);
553 bool SbdThread::event ( QEvent
* e
)
555 if ( e
->type() == (QEvent::User
+ 301) )
557 // kDebug() << "SbdThread::event: emitting filteringFinished signal.";
558 emit
filteringFinished();
564 // ----------------------------------------------------------------------------
569 SbdProc::SbdProc( QObject
*parent
, const QVariantList
& args
) :
570 KttsFilterProc(parent
, args
)
572 // kDebug() << "SbdProc::SbdProc: Running";
573 m_sbdThread
= new SbdThread(this);
574 connect( m_sbdThread
, SIGNAL(filteringFinished()), this, SLOT(slotSbdThreadFilteringFinished()) );
582 // kDebug() << "SbdProc::~SbdProc: Running";
585 if ( m_sbdThread
->isRunning() )
587 m_sbdThread
->terminate();
595 * Initialize the filter.
596 * @param config Settings object.
597 * @param configGroup Settings Group.
598 * @return False if filter is not ready to filter.
600 * Note: The parameters are for reading from kttsdrc file. Plugins may wish to maintain
601 * separate configuration files of their own.
603 bool SbdProc::init(KConfig
* c
, const QString
& configGroup
){
604 // kDebug() << "SbdProc::init: Running";
605 KConfigGroup
config(c
, configGroup
);
606 // m_configuredRe = config.readEntry( "SentenceDelimiterRegExp", "([\\.\\?\\!\\:\\;])\\s|(\\n *\\n)" );
607 m_configuredRe
= config
.readEntry( "SentenceDelimiterRegExp", "([\\.\\?\\!\\:\\;])(\\s|$|(\\n *\\n))" );
608 m_sbdThread
->setConfiguredSbRegExp( m_configuredRe
);
609 QString sb
= config
.readEntry( "SentenceBoundary", "\\1\t" );
610 sb
.replace( "\\t", "\t" );
611 m_sbdThread
->setConfiguredSentenceBoundary( sb
);
612 m_appIdList
= config
.readEntry( "AppID", QStringList() );
613 m_languageCodeList
= config
.readEntry( "LanguageCodes", QStringList() );
618 * Returns True if this filter is a Sentence Boundary Detector.
619 * If so, the filter should implement @ref setSbRegExp() .
620 * @return True if this filter is a SBD.
622 /*virtual*/ bool SbdProc::isSBD() { return true; }
625 * Returns True if the plugin supports asynchronous processing,
626 * i.e., supports asyncConvert method.
627 * @return True if this plugin supports asynchronous processing.
629 * If the plugin returns True, it must also implement @ref getState .
630 * It must also emit @ref filteringFinished when filtering is completed.
631 * If the plugin returns True, it must also implement @ref stopFiltering .
632 * It must also emit @ref filteringStopped when filtering has been stopped.
634 /*virtual*/ bool SbdProc::supportsAsync() { return true; }
637 * Convert input, returning output. Runs synchronously.
638 * @param inputText Input text.
639 * @param talkerCode TalkerCode structure for the talker that KTTSD intends to
640 * use for synthing the text. Useful for extracting hints about
641 * how to filter the text. For example, languageCode.
642 * @param appId The DCOP appId of the application that queued the text.
643 * Also useful for hints about how to do the filtering.
645 /*virtual*/ QString
SbdProc::convert(const QString
& inputText
, TalkerCode
* talkerCode
,
646 const QString
& appId
)
648 if ( asyncConvert( inputText
, talkerCode
, appId
) )
651 // kDebug() << "SbdProc::convert: returning " << getOutput();
653 } else return inputText
;
657 * Convert input. Runs asynchronously.
658 * @param inputText Input text.
659 * @param talkerCode TalkerCode structure for the talker that KTTSD intends to
660 * use for synthing the text. Useful for extracting hints about
661 * how to filter the text. For example, languageCode.
662 * @param appId The DCOP appId of the application that queued the text.
663 * Also useful for hints about how to do the filtering.
664 * @return False if the filter cannot perform the conversion.
666 * When conversion is completed, emits signal @ref filteringFinished. Calling
667 * program may then call @ref getOutput to retrieve converted text. Calling
668 * program must call @ref ackFinished to acknowledge the conversion.
670 /*virtual*/ bool SbdProc::asyncConvert(const QString
& inputText
, TalkerCode
* talkerCode
,
671 const QString
& appId
)
673 // kDebug() << "SbdProc::asyncConvert: running";
674 m_sbdThread
->setWasModified( false );
675 // If language doesn't match, return input unmolested.
676 if ( !m_languageCodeList
.isEmpty() )
678 QString languageCode
= talkerCode
->languageCode();
679 // kDebug() << "StringReplacerProc::convert: converting " << inputText <<
680 // " if language code " << languageCode << " matches " << m_languageCodeList << endl;
681 if ( !m_languageCodeList
.contains( languageCode
) )
683 if ( !talkerCode
->countryCode().isEmpty() )
685 languageCode
+= '_' + talkerCode
->countryCode();
686 // kDebug() << "StringReplacerProc::convert: converting " << inputText <<
687 // " if language code " << languageCode << " matches " << m_languageCodeList << endl;
688 if ( !m_languageCodeList
.contains( languageCode
) ) return false;
692 // If appId doesn't match, return input unmolested.
693 if ( !m_appIdList
.isEmpty() )
695 // kDebug() << "SbdProc::convert: converting " << inputText << " if appId "
696 // << appId << " matches " << m_appIdList << endl;
698 QString appIdStr
= appId
;
699 for ( int ndx
=0; ndx
< m_appIdList
.count(); ++ndx
)
701 if ( appIdStr
.contains(m_appIdList
[ndx
]) )
707 if ( !found
) return false;
709 m_sbdThread
->setText( inputText
);
710 m_sbdThread
->setTalkerCode( talkerCode
);
711 m_state
= fsFiltering
;
712 m_sbdThread
->start();
717 * Waits for a previous call to asyncConvert to finish.
719 /*virtual*/ void SbdProc::waitForFinished()
721 if ( m_sbdThread
->isRunning() )
723 // kDebug() << "SbdProc::waitForFinished: waiting";
725 // kDebug() << "SbdProc::waitForFinished: finished waiting";
726 m_state
= fsFinished
;
731 * Returns the state of the Filter.
733 /*virtual*/ int SbdProc::getState() { return m_state
; }
736 * Returns the filtered output.
738 /*virtual*/ QString
SbdProc::getOutput() { return m_sbdThread
->text(); }
741 * Acknowledges the finished filtering.
743 /*virtual*/ void SbdProc::ackFinished()
746 m_sbdThread
->setText( QString() );
750 * Stops filtering. The filteringStopped signal will emit when filtering
751 * has in fact stopped and state returns to fsIdle;
753 /*virtual*/ void SbdProc::stopFiltering()
755 if ( m_sbdThread
->isRunning() )
757 m_sbdThread
->terminate();
760 m_sbdThread
= new SbdThread(this);
761 m_sbdThread
->setConfiguredSbRegExp( m_configuredRe
);
762 connect( m_sbdThread
, SIGNAL(filteringFinished()), this, SLOT(slotSbdThreadFilteringFinished()) );
764 emit
filteringStopped();
769 * Did this filter do anything? If the filter returns the input as output
770 * unmolested, it should return False when this method is called.
772 /*virtual*/ bool SbdProc::wasModified() { return m_sbdThread
->wasModified(); }
775 * Set Sentence Boundary Regular Expression.
776 * This method will only be called if the application overrode the default.
778 * @param re The sentence delimiter regular expression.
780 /*virtual*/ void SbdProc::setSbRegExp(const QString
& re
) { m_sbdThread
->setSbRegExp( re
); }
782 // Received when SBD Thread finishes.
783 void SbdProc::slotSbdThreadFilteringFinished()
785 m_state
= fsFinished
;
786 // kDebug() << "SbdProc::slotSbdThreadFilteringFinished: emitting filterFinished signal.";
787 emit
filteringFinished();
790 #include "sbdproc.moc"