Skip to content

Instantly share code, notes, and snippets.

@drmfinlay
Last active July 5, 2020 13:57
Show Gist options
  • Save drmfinlay/413895d62a4a699f14a48796f9fda7e7 to your computer and use it in GitHub Desktop.
Save drmfinlay/413895d62a4a699f14a48796f9fda7e7 to your computer and use it in GitHub Desktop.
My formatted dictation Dragonfly grammar + related code
"""
Dictation & command mode Dragonfly grammar
============================================================================
This module defines a configurable grammar for using three different
command/dictation modes. The modes can be configured externally by modifying
the number in the grammar's status file. The modes and associated status
numbers (0-2) are defined as follows:
0. Command-only mode.
Only commands will be recognised in this mode. Dictation on its own will
not be recognised, at least not by this grammar.
1. Command and dictation mode.
Both commands and dictation will be recognised in this mode.
2. Dictation-only mode.
Only dictation will be recognised in this mode. This mode sets the
grammar as exclusive, so commands defined in other grammars will not be
recognised.
It should be noted that this module and grammar is only intended to be used
with engines such as Kaldi, WSR or CMU Pocket Sphinx that yield lowercase
text dictation output. It will *not* properly with Dragon's formatted
dictation and will clash with its built-in modes.
"""
import os
from dragonfly import (Grammar, Choice, Key, Text, FuncContext, IntegerRef,
CompoundRule, Dictation, Window)
from text_dictation_formatting import WordFormatter, StateFlags
class DictationModeGrammar(Grammar):
# Set the status file path.
status_file_path = ".dictation-grammar-status.txt"
# Define the initial word formatter state flags.
_initial_state_flags = StateFlags(
"no_space_before", "cap_next", "prev_ended_in_period"
)
def __init__(self):
Grammar.__init__(self, self.__class__.__name__)
self._window_stacks = {}
self._current_window_handle = -1
self._status = 0 # CHANGE DEFAULT STATE HERE
self._set_status_from_file()
self._word_formatter = WordFormatter()
def _write_status_to_file(self, value):
with open(self.status_file_path, 'w+') as f:
f.write(value)
def _get_status_from_file(self):
try:
with open(self.status_file_path, 'r+') as f:
return int(f.read().strip())
except (IOError, OSError):
self._write_status_to_file('1')
return 1
def _set_status_from_file(self):
self._status = self._get_status_from_file()
if self.loaded:
self.set_exclusiveness(self._status == 2)
def _get_window_stack(self):
handle = self._current_window_handle
stack = self._window_stacks.get(handle)
if stack is None:
stack = []
self._window_stacks[handle] = stack
return stack
def _set_formatting_state_flags(self):
handle = self._current_window_handle
stack = self._window_stacks.get(handle)
if not stack:
state = self._initial_state_flags
else:
state = stack[len(stack) - 1][1]
# Set the formatter flags for the latest utterance sent to the
# current window.
self._word_formatter.state = state.clone()
def push_window_stack_frame(self, frame):
self._get_window_stack().append(frame)
def load(self):
Grammar.load(self)
self.set_exclusiveness(self._status == 2)
def _process_begin(self, executable, title, handle):
self._current_window_handle = handle
# Enable / disable dictation mode according to the status file.
self._set_status_from_file()
@property
def status(self):
return self._status
@status.setter
def status(self, value):
value = int(value)
self._status = value
self._write_status_to_file(str(value))
if self.loaded:
self.set_exclusiveness(value == 2)
def type_dictated_words(self, words):
# Set the formatting state for the current window.
self._set_formatting_state_flags()
# Format the words and type them.
text = self._word_formatter.format_dictation(words)
Text(text).execute()
# Save the utterance state.
current_state = self._word_formatter.state.clone()
frame = (len(text), current_state)
self._get_window_stack().append(frame)
def do_scratch_n_times(self, n):
for _ in range(n):
try:
# Get the number of characters to delete from the current
# window's stack. Discard the state flags.
scratch_number, _ = self._get_window_stack().pop()
Key("backspace:%d" % scratch_number).execute()
except IndexError:
handle = self._current_window_handle
window = Window.get_window(handle)
exe = os.path.basename(window.executable)
title = window.title
print("Nothing in scratch memory for %r window "
"(title=%r, id=%d)" % (exe, title, handle))
break
def clear_formatting_state(self, option):
if option == "current":
stack = self._get_window_stack()
while stack:
stack.pop()
elif option == "all":
self._window_stacks.clear()
# Initialize the grammar here so we can use it to keep track of state.
grammar = DictationModeGrammar()
enabled_context = FuncContext(lambda: grammar.status)
disabled_context = FuncContext(lambda: not grammar.status)
class EnableRule(CompoundRule):
spec = "enable <mode>"
extras = [
Choice("mode", {
"command [only] mode": 0,
"dictation plus command mode": 1,
"command plus dictation mode": 1,
"dictation [only] mode ": 2,
}, default=1)
]
def _process_recognition(self, node, extras):
self.grammar.status = extras["mode"]
class DisableRule(CompoundRule):
context = enabled_context
spec = "disable dictation"
def _process_recognition(self, node, extras):
self.grammar.status = 0
class DisabledDictationRule(CompoundRule):
context = disabled_context
spec = "dictation <text>"
extras = [Dictation("text", default="")]
def _process_recognition(self, node, extras):
print("\n\n----DICTATION MODE IS DISABLED----\n\n")
self.grammar.type_dictated_words(extras["text"].words)
class DictationRule(CompoundRule):
context = enabled_context
spec = "[<modifier>] <text> [mimic <mimic_text>]"
extras = [
Choice("modifier", {
"dictation": (),
"cap": ("cap",),
"no space": ("no-space",),
}, default=()),
Dictation("text", default=""),
Dictation("mimic_text", default="")
]
def _process_recognition(self, node, extras):
# Process recognized words.
words = extras["modifier"] + extras["text"].words
self.grammar.type_dictated_words(words)
mimic_text = extras["mimic_text"].format()
if mimic_text:
self.grammar.engine.mimic(mimic_text)
class ScratchRule(CompoundRule):
context = enabled_context
spec = "(scratch | scratch that [<n> times])"
extras = [
IntegerRef("n", 1, 20, default=1)
]
def _process_recognition(self, node, extras):
self.grammar.do_scratch_n_times(extras["n"])
class ScratchAndReplaceRule(CompoundRule):
context = enabled_context
spec = "make that <text>"
extras = [
Dictation("text", default="")
]
def _process_recognition(self, node, extras):
self.grammar.do_scratch_n_times(1)
self.grammar.type_dictated_words(extras["text"].words)
class ResetDictationRule(CompoundRule):
spec = "reset dictation [<option>]"
extras = [
Choice("option", {
"all": "all",
"current": "current",
}, default="all")
]
def _process_recognition(self, node, extras):
option = extras["option"]
self.grammar.clear_formatting_state(option)
class StateChangeRule(CompoundRule):
context = enabled_context
spec = "<state_change>"
extras = [
Choice("state_change", {
"[start] new sentence": ("cap_next", "prev_ended_in_period"),
"[start] new paragraph": ("no_space_before", "cap_next", "prev_ended_in_period"),
})
]
def _process_recognition(self, node, extras):
# Append the new state flags to the current window's stack using length 0.
frame = (0, StateFlags(*extras["state_change"]))
self.grammar.push_window_stack_frame(frame)
grammar.add_rule(EnableRule())
grammar.add_rule(DisableRule())
grammar.add_rule(DisabledDictationRule())
grammar.add_rule(DictationRule())
grammar.add_rule(ScratchRule())
grammar.add_rule(ScratchAndReplaceRule())
grammar.add_rule(ResetDictationRule())
grammar.add_rule(StateChangeRule())
grammar.load()
def unload():
global grammar
if grammar:
grammar.unload()
grammar = None
# coding=utf-8
#
# This file is part of Dragonfly.
# (c) Copyright 2007, 2008 by Christo Butcher
# Licensed under the LGPL.
#
# Dragonfly is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Dragonfly 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with Dragonfly. If not, see
# <http://www.gnu.org/licenses/>.
#
"""
Dictation formatting for other Dragonfly engines
============================================================================
This module implements dictation formatting for Dragonfly engines that
output plain-text dictated words without formatting information,
i.e. engines other than Natlink. It has been adapted from formatting code
for the Natlink engine.
"""
from locale import getpreferredencoding
import logging
import re
from six import string_types
#===========================================================================
class FlagContainer(object):
""" Container for a predefined set of boolean flags. """
flag_names = ()
def __init__(self, *names):
self._flags_true = set()
for name in names:
setattr(self, name, True)
def flags_string(self):
flags_true_names = []
for flag in self.flag_names:
if flag in self._flags_true:
flags_true_names.append(flag)
return u", ".join(flags_true_names)
def __unicode__(self):
return u"%s(%s)" % (self.__class__.__name__, self.flags_string())
def __repr__(self):
return self.__unicode__().encode(getpreferredencoding())
def __getattr__(self, name):
if name not in self.flag_names:
raise AttributeError("Invalid flag name: %r" % (name,))
return (name in self._flags_true)
def __setattr__(self, name, value):
if name.startswith("_"):
return object.__setattr__(self, name, value)
if name not in self.flag_names:
raise AttributeError("Invalid flag name: received %r, expected"
" one of %r" % (name, self.flag_names))
if value:
self._flags_true.add(name)
else:
self._flags_true.discard(name)
def clone(self):
clone = self.__class__()
for name in self._flags_true:
setattr(clone, name, True)
return clone
#---------------------------------------------------------------------------
class WordFlags(FlagContainer):
"""
Container for formatting flags associated with DNS words.
The flags defined by this class are closely related to the
formatting information provided by DNS.
"""
flag_names = (
# Flags related to spacing
"no_space_before", # No space before this word (like right-paren)
"no_space_after", # No space after this word (like left-paren)
"two_spaces_after", # Two spaces after this word (like full-stop)
"no_space_mode", # Activate no-spacing mode (like No-Space-On)
"reset_no_space", # Reset spacing mode (like No-Space-Off)
"no_space_reset", # Don't reset spacing state (like Cap)
"spacebar", # Extra space after this word (like spacebar)
"no_space_between", # No spaces between adjacent words with this flag (like numbers)
# Flags related to newlines
"newline_after", # One newline after this word (like New-Line)
"two_newlines_after", # Two newlines after this word (like New-Paragraph)
# Flags related to capitalization
"cap_next", # Normally capitalize next word (eg full-stop)
"cap_next_force", # Always capitalize next word (eg Cap-Next)
"lower_next", # Lowercase next word (eg No-Caps-Next)
"upper_next", # Uppercase next word (eg All-Caps-Next)
"cap_mode", # Activate capitalization mode (like Caps-On)
"lower_mode", # Activate lowercase mode (like No-Caps-On)
"upper_mode", # Activate uppercase mode (like All-Caps-On)
"reset_cap", # Reset capitalization mode (like Caps-Off)
"no_cap_reset", # Don't reset the capitalization state (like left-paren)
"no_title_cap", # Don't capitalize in title (like and)
# Miscellaneous flags
"no_format", # Don't apply formatting (like Cap)
"not_after_period", # Suppress this word after a word ending in period (like full-stop after etc.)
)
#---------------------------------------------------------------------------
class StateFlags(FlagContainer):
"""
Container for keeping state for inter-word formatting.
The flags defined by this class are used by Dragonfly to store
formatting state between words as a formatter consumes
consecutive words.
"""
flag_names = (
# Flags related to spacing
"no_space_before", # No space before next word
"two_spaces_before", # Two spaces before next word
"no_space_between", # No space before next word if it has no_space_between flag
"no_space_mode", # No-spacing mode is active
# Flags related to capitalization
"cap_next", # Normally capitalize next word
"cap_next_force", # Always capitalize next word
"lower_next", # Lowercase next word
"upper_next", # Uppercase next word
"cap_mode", # Capitalization mode is active
"lower_mode", # Lowercase mode is active
"upper_mode", # Uppercase mode is active
# Miscellaneous flags
"prev_ended_in_period", # Previous word ended in period
)
#===========================================================================
class Word(object):
""" Word storing written and spoken forms with formatting flags. """
def __init__(self, written, spoken, flags):
self.written = written
self.spoken = spoken
self.flags = flags
def __unicode__(self):
info = [repr(self.written)]
if self.spoken and self.spoken != self.written:
info.append(repr(self.spoken))
flags_string = self.flags.flags_string()
if flags_string:
info.append(flags_string)
return u"%s(%s)" % (self.__class__.__name__, ", ".join(info))
def __repr__(self):
return self.__unicode__().encode(getpreferredencoding())
#===========================================================================
class WordParserTextInput(object):
"""
Dictation results parser for text-input engine.
Based on the DNS word parser classes.
"""
_log = logging.getLogger("dictation.word_parser")
property_map = {
"new-line": WordFlags("no_format", "no_space_after", "no_cap_reset", "newline_after"),
"new-paragraph": WordFlags("no_format", "no_space_after", "cap_next", "two_newlines_after"),
"no-space": WordFlags("no_format", "no_cap_reset", "no_space_after"),
"no-space-on": WordFlags("no_format", "no_cap_reset", "no_space_mode"),
"no-space-off": WordFlags("no_format", "no_cap_reset", "reset_no_space"),
"cap": WordFlags("no_format", "no_space_reset", "cap_next_force"),
"caps-on": WordFlags("no_format", "no_space_reset", "reset_cap", "cap_mode"),
"caps-off": WordFlags("no_format", "no_space_reset", "reset_cap"),
"all-caps": WordFlags("no_format", "no_space_reset", "upper_next"),
"all-caps-on": WordFlags("no_format", "no_space_reset", "reset_cap", "upper_mode"),
"all-caps-off": WordFlags("no_format", "no_space_reset", "reset_cap"),
"no-caps": WordFlags("no_format", "no_space_reset", "lower_next"),
"no-caps-on": WordFlags("no_format", "no_space_reset", "reset_cap", "lower_mode"),
"no-caps-off": WordFlags("no_format", "no_space_reset", "reset_cap"),
"space-bar": WordFlags("no_format", "spacebar", "no_space_after", "no_cap_reset", "no_space_before"),
"spelling-cap": WordFlags("no_format", "no_space_reset", "cap_next_force"),
"letter": WordFlags("no_space_after"),
"uppercase-letter": WordFlags("no_space_after"),
"numeral": WordFlags("no_space_after"),
"period": WordFlags("two_spaces_after", "cap_next", "no_space_before", "not_after_period"),
"question-mark": WordFlags("two_spaces_after", "cap_next", "no_space_before"),
"exclamation-mark": WordFlags("two_spaces_after", "cap_next", "no_space_before"),
"point": WordFlags("no_space_after", "no_space_between", "no_space_before"),
"dot": WordFlags("no_space_after", "no_space_between", "no_space_before"),
"ellipsis": WordFlags("no_space_before", "not_after_period", "cap_next"),
"comma": WordFlags("no_space_before"),
"hyphen": WordFlags("no_space_before", "no_space_after"),
"dash": WordFlags("no_space_before", "no_space_after"),
"at-sign": WordFlags("no_space_before", "no_space_after"),
"colon": WordFlags("no_space_before"),
"semicolon": WordFlags("no_space_before"),
"apostrophe-s": WordFlags("no_space_before"),
"apostrophe-ess": WordFlags("no_space_before"),
"left-*": WordFlags("no_cap_reset", "no_space_after"),
"right-*": WordFlags("no_cap_reset", "no_space_before", "no_space_reset"),
"open-paren": WordFlags("no_space_after"),
"close-paren": WordFlags("no_space_before"),
"slash": WordFlags("no_space_after", "no_space_before"),
# below are two examples of Dragon custom vocabulary with formatting
# these would have to be added to the Dragon vocabulary for users to use them
# "len": WordFlags("no_space_after"), # shorter name for (
# "ren": WordFlags("no_space_before"), # shorter name for )
}
# Add property aliases.
property_map["full-stop"] = property_map["period"]
property_map["enter"] = property_map["new-line"]
translation_table = {
"new-line": "", # lines and paragraphs are translated into new lines
"new-paragraph": "", # later on.
"enter": "",
"no-space": "",
"no-space-on": "",
"no-space-off": "",
"cap": "",
"caps-on": "",
"caps-off": "",
"all-caps": "",
"all-caps-on": "",
"all-caps-off": "",
"no-caps": "",
"no-caps-on": "",
"no-caps-off": "",
"spelling-cap": "",
"space-bar": " ",
"tab": "\t",
"period": ".",
"question-mark": "?",
"exclamation-mark": "!",
"full-stop": ".",
"point": ".",
"dot": ".",
"ellipsis": u"…",
"comma": ",",
"hyphen": "-",
"dash": "-",
"em-dash": u"—",
"at-sign": "@",
"colon": ":",
"semicolon": ";",
"apostrophe-ess": "'s",
"apostrophe-s": "'s",
"open-paren": "(",
"close-paren": ")",
"slash": "/",
"forward-slash": "/",
"back-slash": "\\",
"backslash": "\\",
}
def parse_input(self, word_str):
spoken = word_str
written = word_str
# The written and spoken forms of a word may be different. Use the
# translation table to translate special words.
written = self.translation_table.get(word_str, word_str)
word_flags = self.create_word_flags(word_str)
word = Word(written, spoken, word_flags)
# self._log.debug("Parsed input {0!r} -> {1}".format(word_str, word))
# print ("Parsed input {0!r} -> {1}".format(word_str, word))
return word
def create_word_flags(self, property):
if property in self.property_map:
flags = self.property_map[property].clone()
elif property.startswith("left-"):
flags = self.property_map["left-*"].clone()
elif property.startswith("right-"):
flags = self.property_map["right-*"].clone()
else:
flags = WordFlags()
return flags
#===========================================================================
class WordFormatter(object):
_log = logging.getLogger("dictation.format")
def __init__(self, state=None, parser=None,
two_spaces_after_period=False):
if state: self.state = state
else: self.state = StateFlags("no_space_before")
if parser: self.parser = parser
else: self.parser = WordParserTextInput()
self.two_spaces_after_period = two_spaces_after_period
def format_dictation(self, input_words):
if isinstance(input_words, string_types):
raise ValueError("Argument input_words must be a sequence of"
" words, but received a single string: {0!r}"
.format(input_words))
# Pre-process the input words: replace spaces with hyphens for
# special phrases.
input_str = " ".join(input_words)
for key in self.parser.translation_table.keys():
if "-" in key:
spaced_key = key.replace("-", " ") # e.g. "at sign"
input_str = input_str.replace(spaced_key, key)
# Get a list again.
input_words = input_str.split()
formatted_words = []
for input_word in input_words:
word = self.parser.parse_input(input_word)
formatted_words.append(self.apply_formatting(word))
new_state = self.update_state(word)
# self._log.debug("Processing {0}, formatted output: {1!r},"
# " {2} -> {3}"
# .format(word, formatted_words[-1],
# self.state, new_state))
self.state = new_state
return u"".join(formatted_words)
def apply_formatting(self, word):
state = self.state
# Determine prefix.
if word.flags.no_format: prefix = ""
elif state.no_space_mode: prefix = ""
elif word.flags.no_space_before: prefix = ""
elif state.no_space_before: prefix = ""
elif state.no_space_between and word.flags.no_space_between:
prefix = ""
elif state.two_spaces_before:
if self.two_spaces_after_period: prefix = " "
else: prefix = " "
else: prefix = " "
# Determine formatted written form.
if word.flags.no_format: written = word.written
elif state.cap_mode and not word.flags.no_title_cap:
written = self.capitalize_all(word.written)
elif state.upper_mode: written = self.upper_all(word.written)
elif state.lower_mode: written = self.lower_all(word.written)
elif state.cap_next: written = self.capitalize_first(word.written)
elif state.cap_next_force: written = self.capitalize_first(word.written)
elif state.upper_next: written = self.upper_first(word.written)
elif state.lower_next: written = self.lower_first(word.written)
else: written = word.written
# Remove first period character if needed.
if (state.prev_ended_in_period
and word.flags.not_after_period
and written.startswith(".")):
written = written[1:]
# Determine suffix.
if word.flags.newline_after: suffix = "\n"
elif word.flags.two_newlines_after: suffix = "\n\n"
elif word.flags.spacebar: suffix = " "
else: suffix = ""
return prefix + written + suffix
def update_state(self, word_object):
word = word_object.flags
prev = self.state
state = StateFlags()
# Update capitalization state.
state.cap_next_force = word.cap_next_force or (word.no_cap_reset and prev.cap_next_force)
state.cap_next = word.cap_next or word.cap_mode or (word.no_cap_reset and prev.cap_next)
state.upper_next = word.upper_next or (word.no_cap_reset and prev.upper_next)
state.lower_next = word.lower_next or (word.no_cap_reset and prev.lower_next)
state.cap_mode = word.cap_mode or (prev.cap_mode and not word.reset_cap)
state.upper_mode = word.upper_mode or (prev.upper_mode and not word.reset_cap)
state.lower_mode = word.lower_mode or (prev.lower_mode and not word.reset_cap)
# Update spacing state.
state.no_space_before = word.no_space_after or (prev.no_space_before and word.no_space_reset and word.no_format)
state.two_spaces_before = word.two_spaces_after or (prev.two_spaces_before and word.no_space_reset and word.no_format)
state.no_space_between = word.no_space_between
state.no_space_mode = word.no_space_mode or (prev.no_space_mode and not word.reset_no_space)
# Record whether this word ended in a period.
state.prev_ended_in_period = word_object.written.endswith(".")
# Return newly created state.
return state
def capitalize_first(self, written):
return written.capitalize()
def _capitalize_all_function(self, match):
return match.group(1) + match.group(2).upper()
def capitalize_all(self, written):
return re.sub(r"(^|\s)(\S)", self._capitalize_all_function, written)
def upper_first(self, written):
parts = written.split(" ", 1)
parts[0] = parts[0].upper()
return " ".join(parts)
def lower_first(self, written):
parts = written.split(" ", 1)
parts[0] = parts[0].lower()
return " ".join(parts)
def upper_all(self, written):
return written.upper()
def lower_all(self, written):
return written.lower()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment