Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented microphone modes #853

Merged
merged 4 commits into from
Oct 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified CasterQuickReference.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion castervoice/lib/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class CCRType(object):
# Original Caster CCR "core" set:
"Alphabet", "Navigation", "NavigationNon", "Numbers", "Punctuation",
# Rules which were split out of _caster.py:
"CasterRule", "HardwareRule", "MouseAlternativesRule", "WindowManagementRule",
"CasterRule", "CasterMicRule", "HardwareRule", "MouseAlternativesRule", "WindowManagementRule",
# Alternate mouse grid controls:
"DouglasGridRule", "RainbowGridRule", "SudokuGridRule",
# HMC GUI control rules:
Expand Down
27 changes: 12 additions & 15 deletions castervoice/lib/ctrl/configure_engine.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# TODO: Create and utilize base class. These classes should be initialized only once.
# TODO: Add a function for end-user user to overload in EngineConfigEarly and EngineConfigLate

class EngineConfigEarly():
class EngineConfigEarly:
"""
Initializes engine specific customizations before Nexus initializes.
Grammars are not loaded
Expand All @@ -23,43 +23,40 @@ def set_cancel_word(self):
SymbolSpecs.set_cancel_word("escape")


class EngineConfigLate():
class EngineConfigLate:
"""
Initializes engine specific customizations after Nexus has initialized.
Grammars are loaded into engine.
"""
from castervoice.lib import settings
from castervoice.lib import printer
from dragonfly import get_current_engine
engine = get_current_engine().name

def __init__(self):
from castervoice.lib import control # Access to Nexus instance
self.instannce = control.nexus()._engine_modes_manager
from castervoice.lib.ctrl.mgr.engine_manager import EngineModesManager
self.EngineModesManager = EngineModesManager.initialize()
if self.engine != "text":
self.set_default_mic_mode()
self.set_engine_default_mode()


def set_default_mic_mode(self):
"""
Sets the microphone state on Caster startup.
"""
# Only DNS supports mic_state 'off'. Substituts `sleep` mode on other engines"
if self.settings.SETTINGS["engine"]["default_mic"]: # Default is `False`
default_mic_state = self.settings.SETTINGS["engine"]["mic_mode"] # Default is `on`
if self.engine != "natlink" and default_mic_state == "off":
default_mic_state == "sleep"
self.instannce.set_mic_mode(default_mic_state)

if self.settings.SETTINGS["engine"]["default_mic"]: # Default is `False`
default_mic_state = self.settings.SETTINGS["engine"]["mic_mode"] # Default is `on`
if self.engine != "natlink" and default_mic_state == "off":
default_mic_state = "sleep"
self.EngineModesManager.set_mic_mode(default_mic_state)

def set_engine_default_mode(self):
"""
Sets the engine mode on Caster startup.
"""
# Only DNS supports 'normal'. Substituts `command` mode on other engines"
if self.settings.SETTINGS["engine"]["default_engine_mode"]: # Default is `False`
if self.settings.SETTINGS["engine"]["default_engine_mode"]: # Default is `False`
default_mode = self.settings.SETTINGS["engine"]["engine_mode"] # Default is `normal`
if self.engine != "natlink" and default_mode == "normal":
default_mode == "command"
self.instannce.set_engine_mode(mode=default_mode, state=True)
default_mode = "command"
self.EngineModesManager.set_engine_mode(mode=default_mode, state=True)
196 changes: 135 additions & 61 deletions castervoice/lib/ctrl/mgr/engine_manager.py
Original file line number Diff line number Diff line change
@@ -1,104 +1,178 @@
from dragonfly import get_engine, get_current_engine
from castervoice.lib import settings, printer
from dragonfly import get_engine, get_current_engine, FuncContext, Function, MappingRule, Grammar, Choice, Dictation
from castervoice.lib import printer

engine = get_current_engine().name
if engine == 'natlink':
import natlink

# TODO: Implement a grammar exclusivity for non-DNS engines in a separate class

class EngineModesManager(object):
"""
Manages engine modes and microphone states using backend engine API and through dragonfly grammar exclusivity.
"""
engine = get_current_engine().name
if engine == 'natlink':
import natlink

engine_modes = {"normal":0, "command":2, "dictation":1,"numbers":3, "spell":4}
mic_modes = ["on", "sleeping", "off"]
engine_modes = {"normal": 0, "command": 2,
"dictation": 1, "numbers": 3, "spell": 4}
mic_modes = {"on": 5, "sleeping": 6, "off": 7}
engine_state = None
previous_engine_state = None
mic_state = None
timer_callback = None

def initialize(self):
@classmethod
def initialize(cls):
# Remove "normal" and "off" from 'states' for non-DNS based engines.
if self.engine != 'natlink':
self.engine_modes.pop("normal", 0)
self.mic_modes.remove("off")
if engine != 'natlink':
cls.engine_modes.pop("normal", 0)
cls.mic_modes.pop("off", 7)
#if self.engine == 'natlink':
# Sets 1st index key ("normal" or "command") depending on engine type as default mode
self.engine_state = self.previous_engine_state = next(iter(self.engine_modes.keys()))

cls.engine_state = cls.previous_engine_state = next(
iter(cls.engine_modes.keys()))
# Timer to synchronize natlink.getMicState/SetRecognitionMode with mode_state in case of changed by end-user via DNS GUI.
if engine == 'natlink' and cls.timer_callback is None:
cls.timer_callback = get_current_engine().create_timer(callback=cls._sync_mode, interval=1)
cls.timer_callback.start()

def set_mic_mode(self, mode):
@classmethod
def set_mic_mode(cls, mode):
"""
Changes the engine microphone mode
'on': mic is on
'sleeping': mic from the sleeping and can be woken up by command
'off': mic off and cannot be turned back on by voice. (DNS Only)
:param mode: str
'on': mic is on
'sleeping': mic from the sleeping and can be woken up by command
'off': mic off and cannot be turned back on by voice. (DPI Only)
"""
if mode in self.mic_modes:
self.mic_state = mode
if self.engine == 'natlink':
self.natlink.setMicState(mode)
# From here other engines use grammar exclusivity to re-create the sleep mode
#if mode != "off": # off does not need grammar exclusivity
#pass
# TODO: Implement mic mode sleep mode using grammar exclusivity. This should override DNS is built in sleep grammar but kept in sync automatically with natlink.setmic_state
else:
printer.out("Caster: 'set_mic_mode' is not implemented for '{}'".format(self.engine))
if mode in cls.mic_modes:
cls.mic_state = mode
if engine == 'natlink':
natlink.setMicState(mode)
# Overrides DNS/DPI is built in sleep grammar
ExclusiveManager(mode, modetype="mic_mode")
else:
printer.out("Caster: '{}' is not a valid. set_mic_state modes are: 'off' - DNS Only, 'on', 'sleeping'".format(mode))
printer.out(
"Caster: '{}' is not valid. set_mic_state modes are: 'off' - DPI Only, 'on', 'sleeping'".format(mode))


def get_mic_mode(self):
@classmethod
def get_mic_mode(cls):
"""
Returns mic state.
mode: string
mode: str
"""
return self.mic_state

return cls.mic_state

def set_engine_mode(self, mode=None, state=True):
@classmethod
def set_engine_mode(cls, mode=None, state=True):
"""
Sets the engine mode so that only certain types of commands/dictation are recognized.
'state': Bool - enable/disable mode.
:param state: Bool - enable/disable mode.
'True': replaces current mode (Default)
'False': restores previous mode
'normal': dictation and command (Default: DNS only)
'dictation': Dictation only
'command': Commands only (Default: Other engines)
'numbers': Numbers only
'spell': Spelling only
:param mode: str
'normal': dictation and command (Default: DPI only)
'dictation': Dictation only
'command': Commands only (Default: Other engines)
'numbers': Numbers only
'spell': Spelling only
"""
if state and mode is not None:
# Track previous engine state
# TODO: Timer to synchronize natlink.getMicState() with mengine_state in case of changed by end-user via DNS GUI.
self.previous_engine_state = self.engine_state
cls.previous_engine_state = cls.engine_state
else:
if not state:
# Restore previous mode
mode = self.previous_engine_state
mode = cls.previous_engine_state
else:
printer.out("Caster: set_engine_mode: 'State' cannot be 'True' with a undefined a 'mode'")

if mode in self.engine_modes:
if self.engine == 'natlink':
printer.out(
"Caster: set_engine_mode: 'State' cannot be 'True' with a undefined a 'mode'")

if mode in cls.engine_modes:
if engine == 'natlink':
try:
self.natlink.execScript("SetRecognitionMode {}".format(self.engine_modes[mode])) # engine_modes[mode] is an integer
self.engine_state = mode
natlink.execScript("SetRecognitionMode {}".format(
cls.engine_modes[mode])) # engine_modes[mode] is an integer
cls.engine_state = mode
ExclusiveManager(mode, modetype="engine_mode")
except Exception as e:
printer.out("natlink.execScript failed \n {}".format(e))
else:
# TODO: Implement mode exclusivity. This should override DNS is built in sleep grammar but kept in sync automatically with natlinks SetRecognitionMode
# Once DNS enters its native mode exclusivity will override the any native DNS mode except for normal/command mode.
if self.engine == 'text':
self.engine_state = mode
# TODO: Implement set_engine_mode exclusivity. This should override DPI is built mode but kept in sync automatically.
if engine == 'text':
cls.engine_state = mode
else:
printer.out("Caster: 'set_engine_mode' is not implemented for '{}'".format(self.engine))
printer.out(
"Caster: 'set_engine_mode' is not implemented for '{}'".format(engine))
else:
printer.out("Caster: '{}' mode is not a valid. set_engine_mode: Modes: 'normal'- DNS Only, 'dictation', 'command', 'numbers', 'spell'".format(mode))

printer.out(
"Caster: '{}' mode is not valid. set_engine_mode: Modes: 'normal'- DPI Only, 'dictation', 'command', 'numbers', 'spell'".format(mode))

def get_engine_mode(self):
@classmethod
def get_engine_mode(cls):
"""
Returns engine mode.
mode: str
mode: str
"""
return cls.engine_state

@classmethod
def _sync_mode(cls):
"""
Synchronizes Caster exclusivity modes an with DNS/DPI GUI built-in modes state.
"""
return self.engine_state
# TODO: Implement set_engine_mode logic with modes not just mic_state.
mic_state = cls.get_mic_mode()
if mic_state is None:
cls.mic_state = natlink.getMicState()
else:
if natlink.getMicState() != mic_state:
cls.set_mic_mode(natlink.getMicState())


class ExclusiveManager:
"""
Loads and switches exclusivity for caster modes
:param mode: str
:param modetype: 'mic_mode' or 'engine_mode' str
"""
# TODO: Implement set_engine_mode exclusivity with mode rules.
# TODO: Implement timer for sleep mode.
# TODO: Implement hotkey for microphone on-off
sleep_grammar = None
sleeping = False

sleep_rule = MappingRule(
name="sleep_rule",
mapping={
"caster <mic_mode>": Function(lambda mic_mode: EngineModesManager.set_mic_mode(mode=mic_mode)),
"<text>": Function(lambda text: False)
},
extras=[Choice("mic_mode", {
"off": "off",
"on": "on",
"sleep": "sleeping",
}),
Dictation("text")],
context=FuncContext(lambda: ExclusiveManager.sleeping),
)

def __init__(self, mode, modetype):
if modetype == "mic_mode":
if not isinstance(ExclusiveManager.sleep_grammar, Grammar):
grammar = ExclusiveManager.sleep_grammar = Grammar("sleeping")
grammar.add_rule(self.sleep_rule)
grammar.load()
if mode == "sleeping":
self.set_exclusive(state=True)
printer.out("Caster: Microphone is sleeping")
if mode == "on":
self.set_exclusive(state=False)
printer.out("Caster: Microphone is on")
if mode == "off":
printer.out("Caster: Microphone is off")
else:
printer.out("{}, {} not implemented".format(mode, modetype))

def set_exclusive(self, state):
grammar = ExclusiveManager.sleep_grammar
ExclusiveManager.sleeping = state
grammar.set_exclusive(state)
get_engine().process_grammars_context()
5 changes: 0 additions & 5 deletions castervoice/lib/ctrl/nexus.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
from castervoice.lib.ctrl.mgr.validation.rules.rule_validation_delegator import CCRRuleValidationDelegator
from castervoice.lib.merge.ccrmerging2.ccrmerger2 import CCRMerger2
from castervoice.lib.merge.ccrmerging2.merging.classic_merging_strategy import ClassicMergingStrategy
from castervoice.lib.ctrl.mgr.engine_manager import EngineModesManager

class Nexus:
def __init__(self, content_loader):
Expand Down Expand Up @@ -79,13 +78,9 @@ def __init__(self, content_loader):
self._content_loader, hooks_runner, rules_config, smrc, mapping_rule_maker,
transformers_runner)

'''tracks engine grammar exclusivity and mic states -- TODO Grammar exclusivity should be managed through grammar manager'''
self._engine_modes_manager = EngineModesManager()

'''ACTION TIME:'''
self._load_and_register_all_content(rules_config, hooks_runner, transformers_runner)
self._grammar_manager.initialize()
self._engine_modes_manager.initialize()

def _load_and_register_all_content(self, rules_config, hooks_runner, transformers_runner):
"""
Expand Down
2 changes: 0 additions & 2 deletions castervoice/rules/apps/speech_engine/dragon_rules/dragon.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@

class DragonRule(MappingRule):
mapping = {
'(lock Dragon | deactivate)':
R(Playback([(["go", "to", "sleep"], 0.0)])),
'(number|numbers) mode':
R(Playback([(["numbers", "mode", "on"], 0.0)])),
'spell mode':
Expand Down
24 changes: 24 additions & 0 deletions castervoice/rules/core/engine_manager_rules/mic_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from dragonfly import Dictation, MappingRule, Choice, Function
from castervoice.lib.actions import Text

from castervoice.lib.ctrl.mgr.rule_details import RuleDetails
from castervoice.lib.merge.state.short import R

from castervoice.lib.ctrl.mgr.engine_manager import EngineModesManager

class CasterMicRule(MappingRule):
mapping = {
"caster <mic_mode>": Function(lambda mic_mode: EngineModesManager.set_mic_mode(mode=mic_mode)),
}
extras = [
Choice(
"mic_mode", {
"off": "off",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get this when saying "caster off"

Caster: 'off' is not valid. set_mic_state modes are: 'off' - DPI Only, 'on', 'sleeping'

Is that deliberate? We should also have "caster wake" as an option.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LexiconCode thanks for doing this!

Copy link
Member Author

@LexiconCode LexiconCode Oct 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is intentional ('off' - DPI Only). For other engines besides DPI/DNS to have an off state they need a method to turn the microphone on without voice activation like a keyboard shortcut/GUI toggle. DPI/DNS has that capability. Why sleep with sleeping?

There's a difference between sleep the command and sleeping which is passed as a parameter. sleeping is what Natlink uses as a string to put DNS to sleep natlink.setMicState(mode).

The mode needs to be checked in EngineModesManager because they can be set in the caster settings for the engine to start up in a certain mode. Because the user is defining a string (a mode) within the settings which could be anything must be checked against valid modes.

"caster wake" would be possible but I don't really know why unless there is recognition issues. It's nice to provide something that's concise and intuitive. "wake" does feel natural but that's because of my experience of DNS. I would think most people without that experience would think "on" "off" "sleep".

One other bit of feedback is that the dictation element needs to be weighted for kaldi in sleep_rule?

"on": "on",
"sleep": "sleeping",
}),
]
defaults = {}

def get_rule():
return CasterMicRule, RuleDetails(name="caster mic mode", transformer_exclusion=True)
Binary file modified docs/CasterQuickReference.pdf
Binary file not shown.
2 changes: 2 additions & 0 deletions docs/CasterQuickReference.tex
Original file line number Diff line number Diff line change
Expand Up @@ -437,11 +437,13 @@ \section*{Caster Quick Reference} % Title

\sectiontitle{Update and Caster Management}

\command{caster <mode> \footnotemark[2]}{microphone on, off, sleep}
\command{clear caster log}{Clears Log Window}
\command{update dragonfly}{Updates Dragonfly}
\command{reboot caster}{Restarts Caster}
\command{caster settings editor}{Caster settings editor}

\footnotetext[2]{'Off' Implemented for DNS/DPI only}
%----------------------------------------------------------------------------------------
\end{minipage}
}
Expand Down