Skip to content

Commit

Permalink
Merge pull request #49 from Danesprite/feat/text_engine
Browse files Browse the repository at this point in the history
Closes #36.
  • Loading branch information
drmfinlay committed Dec 27, 2018
2 parents b3dcf84 + ec10d21 commit 16327c7
Show file tree
Hide file tree
Showing 22 changed files with 1,052 additions and 201 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ script:
# Run a selection of doctests and unit tests
- pytest --doctest-modules dragonfly/parser.py
- pytest dragonfly/test/test_parser.py
- pytest dragonfly/test/test_engine_text.py

32 changes: 32 additions & 0 deletions documentation/cli.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

.. _RefCLI:

Command-line Interface (CLI)
============================================================================

.. argparse::
:module: dragonfly.__main__
:func: make_arg_parser


Usage Examples
----------------------------------------------------------------------------
Below are some examples using the *test* sub-command. All of them should
work in Bash.

.. code:: shell

# Load a command module and mimic two commands separately.
echo "command one\n command two" | python -m dragonfly test module.py

# Same test without the pipe.
python -m dragonfly test module.py
command one
command two

# Same test with quieter output.
echo "command one\n command two" | python -m dragonfly test -q module.py

# Test loading two modules with the sphinx engine and exit without
# checking input.
python -m dragonfly test -e sphinx --no-input module1.py module2.py
2 changes: 1 addition & 1 deletion documentation/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def __getattr__(cls, name):
#---------------------------------------------------------------------------
# General configuration

extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"]
extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinxarg.ext"]
templates_path = ["templates"]
source_suffix = ".txt"
master_doc = "index"
Expand Down
3 changes: 3 additions & 0 deletions documentation/engines.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ Engine backends
.. automodule:: dragonfly.engines.backend_sphinx
:members:

.. automodule:: dragonfly.engines.backend_text
:members:


Dictation container classes
----------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions documentation/miscellaneous.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Contents:
.. toctree::
config
ccr
cli
language
windows
accessibility
1 change: 1 addition & 0 deletions documentation/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pyperclip>=1.7.0
six
enum34;python_version<'3.4'
regex
sphinx-argparse
137 changes: 137 additions & 0 deletions dragonfly/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import argparse
import logging
import sys


from dragonfly import get_engine, MimicFailure
from dragonfly.engines.base.engine import EngineContext
from dragonfly.loader import CommandModule

logging.basicConfig()
_log = logging.getLogger("command")


def test_with_engine(args):
# Initialise the specified engine, catching and reporting errors.
try:
engine = get_engine(args.engine)
_log.debug("Testing with engine '%s'" % args.engine)
except Exception as e:
_log.error(e)
return 1

# Set the logging level of the root logger.
if args.quiet:
args.log_level = "WARNING"
logging.root.setLevel(getattr(logging, args.log_level))

# Connect to the engine, load grammar modules, take input from stdin and
# disconnect from the engine if interrupted or if EOF is received.
with EngineContext(engine):
# Load each module. Errors during loading will be caught and logged.
failed_loads = 0
for f in args.files:
module_ = CommandModule(f.name)
module_.load()

if not module_.loaded:
failed_loads += 1

# Also close each file object created by argparse.
f.close()

# Read lines from stdin and pass them to engine.mimic. Strip excess
# white space from each line. Report any mimic failures.
# Use the success of the last call to engine.mimic as the return
# code. If there were no non-empty lines from stdin, the overall
# success of module loading will be used instead.
return_code = 1 if failed_loads else 0
if args.no_input:
# Return early if --no-input was specified.
return return_code
_log.info("Enter commands to mimic followed by new lines.")
try:
# Use iter to avoid a bug in Python 2.x:
# https://bugs.python.org/issue3907
for line in iter(sys.stdin.readline, ''):
line = line.strip()
if not line: # skip empty lines.
continue

try:
engine.mimic(line.split())
_log.info("Mimic success for words: %s" % line)
return_code = 0
except MimicFailure:
_log.error("Mimic failure for words: %s" % line)
return_code = 1
except KeyboardInterrupt:
pass

return return_code


_command_map = {
"test": test_with_engine
}


def make_arg_parser():
parser = argparse.ArgumentParser(
prog="python -m dragonfly",
description="Command-line interface to the Dragonfly speech "
"recognition framework"
)
subparsers = parser.add_subparsers(dest='subparser_name')

# Create the parser for the "test" command.
parser_test = subparsers.add_parser(
"test",
help="Load grammars from Python files for testing with a "
"dragonfly engine. By default input from stdin is passed to "
"engine.mimic() after command modules are loaded."
)
parser_test.add_argument(
"files", metavar="file", nargs="+", type=argparse.FileType("r"),
help="Command module file(s)."
)
parser_test.add_argument(
"-e", "--engine", default="text",
help="Name of the engine to use for testing."
)
parser_test.add_argument(
"-l", "--log-level", default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="Log level to use."
)
parser_test.add_argument(
"-n", "--no-input", default=False, action="store_true",
help="Whether to load command modules and then exit without "
"reading input from stdin."
)
parser_test.add_argument(
"-q", "--quiet", default=False, action="store_true",
help="Equivalent to '-l WARNING' -- suppresses INFO and DEBUG "
"logging."
)
return parser


def main():
# Parse the arguments and get the relevant function. Exit if the command
# is not implemented.
args = make_arg_parser().parse_args()

def not_implemented(_):
print("Command '%s' is not implemented" % args.subparser_name)
return 1

func = _command_map.get(args.subparser_name, not_implemented)

# Call the function and exit using the result.
return_code = func(args)
exit(return_code)


if __name__ == '__main__':
main()
21 changes: 21 additions & 0 deletions dragonfly/engines/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,27 @@ def get_engine(name=None):
if name:
raise EngineError(message)

# Only retrieve the text input engine if explicitly specified; it is not
# an actual SR engine implementation and is mostly intended to be used
# for testing.
if name == "text":
# Attempt to retrieve the TextInput engine instance.
try:
from .backend_text import is_engine_available
from .backend_text import get_engine as get_specific_engine
if is_engine_available():
_default_engine = get_specific_engine()
_engines_by_name["text"] = _default_engine
return _default_engine
except Exception as e:
message = ("Exception while initializing text-input engine:"
" %s" % (e,))
log.exception(message)
traceback.print_exc()
print(message)
if name:
raise EngineError(message)

if not name:
raise EngineError("No usable engines found.")
else:
Expand Down
64 changes: 64 additions & 0 deletions dragonfly/engines/backend_text/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#
# This file is part of Dragonfly.
# (c) Copyright 2018 by Dane Finlay
# 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/>.
#

"""
SR back-end package for the text input engine
============================================================================
The text input engine is a convenient, always available implementation
designed to be used via the :meth:`engine.mimic` method.
To initialise the text input engine, do the following::
get_engine("text")
Note that :meth:`dragonfly.engines.get_engine` called without ``"text"``
will **never** initialise the text input engine. This is because real speech
recognition backends should be returned from the function by default.
All dragonfly elements and rule classes should be supported. Use all
uppercase words to mimic input for :class:`Dictation` elements, e.g.
`"find SOME TEXT"` to match the dragonfly spec `"find <text>"`.
Dragonfly's command-line interface can be used to test command modules with
the text input engine. See the :ref:`CLI page <RefCLI>` for more details.
"""

import logging
_log = logging.getLogger("engine.text")


# Module level singleton instance of this engine implementation.
_engine = None


def is_engine_available():
""" Check whether the engine is available. """
return True


def get_engine():
""" Retrieve the back-end engine object. """
global _engine
if not _engine:
from .engine import TextInputEngine
_engine = TextInputEngine()
return _engine
57 changes: 57 additions & 0 deletions dragonfly/engines/backend_text/dictation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#
# This file is part of Dragonfly.
# (c) Copyright 2018 by Dane Finlay
# 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 container class for the text engine
============================================================================
This class is used to store the recognized results of dictation elements
within voice-commands. It offers access to both the raw spoken-form words
and be formatted written-form text.
The formatted text can be retrieved using
:meth:`~DictationContainerBase.format` or simply by calling ``str(...)``
on a dictation container object. A tuple of the raw spoken words can be
retrieved using :attr:`~DictationContainerBase.words`.
"""

from six import PY2

from ..base import DictationContainerBase


class TextDictationContainer(DictationContainerBase):
"""
Container class for dictated words as recognized by the
:class:`Dictation` element.
"""

def __init__(self, words):
DictationContainerBase.__init__(self, words=words)

def __repr__(self):
message = u"%s(%s)" % (self.__class__.__name__,
u", ".join(self._words))
if PY2:
return message.encode()
else:
return message

0 comments on commit 16327c7

Please sign in to comment.