Skip to content

Commit

Permalink
Improve grammar and rule processing for all SR engine back-ends
Browse files Browse the repository at this point in the history
Changes:

- All SR engine back-ends now have uniform support for the
  `process_recognition()', `process_recognition_other()' and
  `process_recognition_failure()' grammar callbacks.

- Recognition events are now delivered to observers via said grammar
  callbacks, as was done previously.

- The CMU Pocket Sphinx back-end and the text-input back-end now pass
  recognition results objects on to grammar callbacks like the other
  three engines do.

- The `on_post_recognition()' recognition event has been removed,
  along with its associated callback, registration mechanisms and
  test code.  This event was removed because it is not possible to
  implement with grammar callbacks.
  • Loading branch information
drmfinlay committed Feb 14, 2023
1 parent ecec487 commit 253d1f6
Show file tree
Hide file tree
Showing 22 changed files with 717 additions and 577 deletions.
2 changes: 0 additions & 2 deletions documentation/recobs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ recognition state events:
or to dictation.
* ``on_end()`` -- called when speech ends, either with a successful
recognition or in failure.
* ``on_post_recognition()`` -- called *after* all rule processing has
completed after a successful recognition.

.. automodule:: dragonfly.grammar.recobs
:members:
Expand Down
9 changes: 0 additions & 9 deletions documentation/test_recobs_doctest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ when its callback methods are called::
... print("on_failure()")
... def on_end(self):
... print("on_end()")
... def on_post_recognition(self, words, rule):
... print("on_post_recognition(): %r from %r" % (words, rule))
...
>>> recobs_demo = RecognitionObserverDemo()
>>> recobs_demo.register()
Expand All @@ -41,7 +39,6 @@ when its callback methods are called::
on_begin()
on_recognition(): (u'hello', u'world')
on_end()
on_post_recognition(): (u'hello', u'world') from _ElementTestRule(rule)
u'hello world'
>>> test_lit.recognize("hello universe")
on_begin()
Expand Down Expand Up @@ -183,14 +180,10 @@ Register recognition callback functions::
>>> def on_end():
... print("on_end()")
...
>>> def on_post_recognition(words, rule):
... print("on_post_recognition(): %r from %r" % (words, rule))
...
>>> on_begin_obs = register_beginning_callback(on_begin)
>>> on_success_obs = register_recognition_callback(on_recognition)
>>> on_failure_obs = register_failure_callback(on_failure)
>>> on_end_obs = register_ending_callback(on_end)
>>> on_post_recognition_obs = register_post_recognition_callback(on_post_recognition)

Callback functions are called during recognitions::

Expand All @@ -199,7 +192,6 @@ Callback functions are called during recognitions::
on_begin()
on_recognition(): (u'hello', u'world')
on_end()
on_post_recognition(): (u'hello', u'world') from _ElementTestRule(rule)
u'hello world'
>>> test_lit.recognize("hello universe")
on_begin()
Expand All @@ -214,4 +206,3 @@ each function::
>>> on_success_obs.unregister()
>>> on_failure_obs.unregister()
>>> on_end_obs.unregister()
>>> on_post_recognition_obs.unregister()
9 changes: 9 additions & 0 deletions documentation/text_engine.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,12 @@ Engine API

.. autoclass:: dragonfly.engines.backend_text.engine.TextInputEngine
:members:


.. _RefTextEngineResultsClass:

Text Recognition Results Class
----------------------------------------------------------------------------

.. autoclass:: dragonfly.engines.backend_text.engine.Results
:members:
3 changes: 1 addition & 2 deletions dragonfly/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@
register_beginning_callback,
register_recognition_callback,
register_failure_callback,
register_ending_callback,
register_post_recognition_callback)
register_ending_callback)

#---------------------------------------------------------------------------

Expand Down
56 changes: 26 additions & 30 deletions dragonfly/engines/backend_kaldi/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,7 @@ def _load_grammar(self, grammar):

self._log.info("Loading grammar %s" % grammar.name)
kaldi_rule_by_rule_dict = self._compiler.compile_grammar(grammar, self)
wrapper = GrammarWrapper(grammar, kaldi_rule_by_rule_dict, self,
self._recognition_observer_manager)
wrapper = GrammarWrapper(grammar, kaldi_rule_by_rule_dict, self)

def load():
for (rule, kaldi_rule) in kaldi_rule_by_rule_dict.items():
Expand Down Expand Up @@ -306,7 +305,6 @@ def mimic(self, words):
except Exception as e:
raise MimicFailure("Invalid mimic input %r: %s." % (words, e))

self._recognition_observer_manager.notify_begin()
kaldi_rules_activity = self._compute_kaldi_rules_activity()
self.prepare_for_recognition() # Redundant?

Expand Down Expand Up @@ -383,7 +381,6 @@ def _do_recognition(self, timeout=None, single=False, audio_iter=None):
elif block is not None:
if not self._in_phrase:
# Start of phrase
self._recognition_observer_manager.notify_begin()
with debug_timer(self._log.debug, "computing activity"):
kaldi_rules_activity = self._compute_kaldi_rules_activity()
self._in_phrase = True
Expand Down Expand Up @@ -673,17 +670,16 @@ def fail(self, expected_error_rate=None, confidence=None, mimic=False):
if confidence is not None: self.confidence = confidence
self.mimic = mimic
self.acceptable = False
self.engine._recognition_observer_manager.notify_failure(results=self)
self.engine.dispatch_recognition_failure(results=self)
self.finalized = True


#===========================================================================

class GrammarWrapper(GrammarWrapperBase):

def __init__(self, grammar, kaldi_rule_by_rule_dict, engine,
recobs_manager):
GrammarWrapperBase.__init__(self, grammar, engine, recobs_manager)
def __init__(self, grammar, kaldi_rule_by_rule_dict, engine):
GrammarWrapperBase.__init__(self, grammar, engine)
self.kaldi_rule_by_rule_dict = kaldi_rule_by_rule_dict

self.active = True
Expand All @@ -692,20 +688,34 @@ def __init__(self, grammar, kaldi_rule_by_rule_dict, engine,
def phrase_start_callback(self, executable, title, handle):
self.grammar.process_begin(executable, title, handle)

def _decode_grammar_rules(self, state, words, results, *args):
def _process_grammar_rules(self, state, words, results, dispatch_other, *args):
rule = args[0]
state.initialize_decoding()
for result in rule.decode(state):
if state.finished():
root = state.build_parse_tree()
notify_args = (words, rule, root, results)
self.recobs_manager.notify_recognition(*notify_args)
with debug_timer(self.engine._log.debug, "rule execution time"):
rule.process_recognition(root)
self.recobs_manager.notify_post_recognition(*notify_args)
self._process_final_rule(state, words, results, dispatch_other, rule, *args)
return True
return False

def _process_final_rule(self, state, words, results, dispatch_other,
rule, *args):
# Dispatch results to other grammars, if appropriate.
if dispatch_other:
self.engine.dispatch_recognition_other(self.grammar, words, results)

# Call the grammar's general process_recognition method, if it is present.
# Stop if it returns False.
stop = self.recognition_process_callback(words, results) is False
if stop: return

# Process the recognition.
try:
root = state.build_parse_tree()
with debug_timer(self.engine._log.debug, "rule execution time"):
rule.process_recognition(root)
except Exception as e:
self._log.exception("Failed to process rule %r: %s", rule.name, e)

def recognition_callback(self, recognition):
words = recognition.words
rule = recognition.kaldi_rule.parent_rule
Expand All @@ -719,24 +729,10 @@ def recognition_callback(self, recognition):
for (word, is_dictation) in zip(words, words_are_dictation_mask))

# Attempt to process the recognition.
if self.process_results(words_rules, rule_names, recognition, rule): return
if self.process_results(words_rules, rule_names, recognition, True, rule): return

except Exception as e:
self.engine._log.error("Grammar %s: exception: %s" % (self.grammar._name, e), exc_info=True)

# If this point is reached, then the recognition was not processed successfully
self.engine._log.error("Grammar %s: failed to decode rule %s recognition %r." % (self.grammar._name, rule.name, words))

# FIXME
# def recognition_other_callback(self, StreamNumber, StreamPosition):
# func = getattr(self.grammar, "process_recognition_other", None)
# if func:
# func(words=False)
# return

# FIXME
# def recognition_failure_callback(self, StreamNumber, StreamPosition, Result):
# func = getattr(self.grammar, "process_recognition_failure", None)
# if func:
# func()
# return
56 changes: 46 additions & 10 deletions dragonfly/engines/backend_kaldi/recobs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of Dragonfly.
# (c) Copyright 2018 by Dane Finlay
# (c) Copyright 2018-2023 by Dane Finlay
# Licensed under the LGPL.
#
# Dragonfly is free software: you can redistribute it and/or modify it
Expand All @@ -19,23 +19,59 @@
#

"""
Recognition observer class for Kaldi backend
Recognition observer class for the Kaldi engine back-end
============================================================================
"""

from dragonfly.engines.base import RecObsManagerBase
from dragonfly.engines.base import RecObsManagerBase
from dragonfly.grammar.grammar import Grammar
from dragonfly.grammar.rule_base import Rule
from dragonfly.grammar.elements import Impossible


#---------------------------------------------------------------------------

class KaldiRecObsManager(RecObsManagerBase):
"""
This class's methods are called by the engine directly, rather than through a
grammar.
"""

def __init__(self, engine):
RecObsManagerBase.__init__(self, engine)
self._grammar = None

# The following methods must be implemented by RecObsManagers.
def _activate(self):
pass
if not self._grammar:
self._grammar = KaldiRecObsGrammar(self)
self._grammar.load()

def _deactivate(self):
pass
if self._grammar:
self._grammar.unload()


#---------------------------------------------------------------------------

class KaldiRecObsGrammar(Grammar):

def __init__(self, manager):
self._manager = manager
name = "_recobs_grammar"
Grammar.__init__(self, name, description=None, context=None)

rule = Rule(element=Impossible(), exported=True)
self.add_rule(rule)

#-----------------------------------------------------------------------
# Callback methods for handling utterances and recognitions.

def process_begin(self, executable, title, handle):
self._manager.notify_begin()

def process_recognition(self, words):
raise RuntimeError("Recognition observer received an unexpected"
" recognition: %s" % (words,))

def process_recognition_other(self, words, results):
self._manager.notify_recognition(words, results)

def process_recognition_failure(self, results):
self._manager.notify_failure(results)
60 changes: 21 additions & 39 deletions dragonfly/engines/backend_natlink/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,7 @@ def _load_grammar(self, grammar):
% (self, grammar.name))

grammar_object = self.natlink.GramObj()
wrapper = GrammarWrapper(grammar, grammar_object, self,
self._recognition_observer_manager)
wrapper = GrammarWrapper(grammar, grammar_object, self)
grammar_object.setBeginCallback(wrapper.begin_callback)
grammar_object.setResultsCallback(wrapper.results_callback)
grammar_object.setHypothesisCallback(None)
Expand Down Expand Up @@ -416,8 +415,8 @@ class GrammarWrapper(GrammarWrapperBase):
# always report accurate rule IDs.
_dictated_word_guesses_enabled = True

def __init__(self, grammar, grammar_object, engine, recobs_manager):
GrammarWrapperBase.__init__(self, grammar, engine, recobs_manager)
def __init__(self, grammar, grammar_object, engine):
GrammarWrapperBase.__init__(self, grammar, engine)
self.grammar_object = grammar_object
self.rule_names = None
self.active_rules_set = set()
Expand Down Expand Up @@ -462,47 +461,16 @@ def deactivate_rule(self, rule_name):
grammar_object = self.grammar_object
grammar_object.deactivate(rule_name)

def _decode_grammar_rules(self, state, words, results, *args):
# Iterate through this grammar's rules, attempting to decode each.
# If successful, call that rule's method for processing the
# recognition and return.
for rule in self.grammar.rules:
if not (rule.active and rule.exported): continue
state.initialize_decoding()
for _ in rule.decode(state):
if state.finished():
self._retain_audio(words, results, rule.name)
root = state.build_parse_tree()

# Notify observers using the manager *before*
# processing.
# TODO Use words="other" instead, with a special
# recobs grammar wrapper at index 0.
notify_args = (words, rule, root, results)
self.recobs_manager.notify_recognition(
*notify_args
)
try:
rule.process_recognition(root)
except Exception as e:
self._log.exception("Failed to process rule "
"'%s': %s" % (rule.name, e))
self.recobs_manager.notify_post_recognition(
*notify_args
)
return True
return False

def results_callback(self, words, results):
self._log.debug("Grammar %s: received recognition %r."
% (self.grammar.name, words))

if words == "other":
result_words = tuple(map_word(w) for w in results.getWords(0))
self.process_special_results(words, result_words, results)
self.recognition_other_callback(result_words, results)
return
elif words == "reject":
self.process_special_results(words, None, results)
self.recognition_failure_callback(results)
return

# If the words argument was not "other" or "reject", then
Expand All @@ -511,14 +479,28 @@ def results_callback(self, words, results):
words_rules = tuple((map_word(w), r) for w, r in words)
words = tuple(w for w, r in words_rules)

# Process this recognition.
if self.process_results(words_rules, self.rule_names, results):
# Process this recognition without dispatching results to other
# grammars; Natlink handles this for us perfectly.
if self.process_results(words_rules, self.rule_names, results,
dispatch_other=False):
return

# Failed to decode recognition.
self._log.error("Grammar %s: failed to decode recognition %r."
% (self.grammar._name, words))

def _process_final_rule(self, state, words, results, dispatch_other,
rule, *args):
# Retain audio, if appropriate.
self._retain_audio(words, results, rule.name)

# Call the base class method.
GrammarWrapperBase._process_final_rule(self, state, words, results,
dispatch_other, rule, *args)

# TODO Extract the retain audio feature into an example command module.
# A grammar with a `process_recognition_other' function should be able
# to handle this without issue.
def _retain_audio(self, words, results, rule_name):
# Only write audio data and metadata if the directory exists.
retain_dir = self.engine._retain_dir
Expand Down

0 comments on commit 253d1f6

Please sign in to comment.