Skip to content

Commit

Permalink
feat/auto_tx_skills_continued (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
JarbasAl authored Apr 22, 2023
1 parent 282d13d commit 1149a97
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 27 deletions.
134 changes: 121 additions & 13 deletions ovos_workshop/skills/auto_translatable.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,23 @@
from ovos_plugin_manager.language import OVOSLangDetectionFactory, OVOSLangTranslationFactory
from ovos_utils import get_handler_name
from ovos_utils.log import LOG

from ovos_workshop.resource_files import SkillResources
from ovos_workshop.skills.common_query_skill import CommonQuerySkill
from ovos_workshop.skills.ovos import OVOSSkill, OVOSFallbackSkill


class UniversalSkill(OVOSSkill):
''' Skill that auto translates input/output from any language '''
''' Skill that auto translates input/output from any language
intent handlers are ensured to receive utterances in self.internal_language
intent handlers are expected to produce utterances in self.internal_language
self.speak will always translate utterances from self.internal_lang to self.lang
NOTE: self.lang reflects the original query language
but received utterances are always in self.internal_language
'''

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand All @@ -16,7 +27,11 @@ def __init__(self, *args, **kwargs):

self.internal_language = None # the skill internally only works in this language
self.translate_tags = True # __tags__ private value will be translated (adapt entities)
self.translate_keys = [] # any keys added here will have values translated in message.data
self.translate_keys = ["utterance", "utterances"] # keys added here will have values translated in message.data

# autodetect will detect the lang of the utterance regardless of what has been reported
# to test just type in the cli in another language and watch answers still coming
self.autodetect = False # TODO from mycroft.conf
if self.internal_language is None:
lang = Configuration().get("lang", "en-us")
LOG.warning(f"UniversalSkill are expected to specify their internal_language, casting to {lang}")
Expand All @@ -41,7 +56,10 @@ def detect_language(self, utterance):
return self.lang.split("-")[0]

def translate_utterance(self, text, target_lang, sauce_lang=None):
sauce_lang = sauce_lang or self.detect_language(text)
if self.autodetect:
sauce_lang = self.detect_language(text)
else:
sauce_lang = sauce_lang or self.detect_language(text)
if sauce_lang.split("-")[0] != target_lang:
translated = self.translator.translate(text, source=sauce_lang, target=target_lang)
LOG.info("translated " + text + " to " + translated)
Expand All @@ -53,21 +71,37 @@ def _translate_message(self, message):
sauce_lang = self.lang # from message or config
out_lang = self.internal_language # skill wants input is in this language,

ut = message.data.get("utterance")
if ut:
message.data["utterance"] = self.translate_utterance(ut, target_lang=out_lang, sauce_lang=sauce_lang)
if "utterances" in message.data:
message.data["utterances"] = [self.translate_utterance(ut, target_lang=out_lang, sauce_lang=sauce_lang)
for ut in message.data["utterances"]]
if sauce_lang == out_lang and not self.autodetect:
# do nothing
return message

translation_data = {"original": {}, "translated": {},
"source_lang": sauce_lang, "internal_lang": self.internal_language}

def _do_tx(thing):
if isinstance(thing, str):
thing = self.translate_utterance(thing, target_lang=out_lang, sauce_lang=sauce_lang)
elif isinstance(thing, list):
thing = [_do_tx(t) for t in thing]
elif isinstance(thing, dict):
thing = {k: _do_tx(v) for k, v in thing.items()}
return thing

for key in self.translate_keys:
if key in message.data:
ut = message.data[key]
message.data[key] = self.translate_utterance(ut, target_lang=out_lang, sauce_lang=sauce_lang)
translation_data["original"][key] = message.data[key]
translation_data["translated"][key] = message.data[key] = _do_tx(message.data[key])

# special case
if self.translate_tags:
translation_data["original"]["__tags__"] = message.data["__tags__"]
for idx, token in enumerate(message.data["__tags__"]):
message.data["__tags__"][idx] = self.translate_utterance(token.get("key", ""),
target_lang=out_lang,
sauce_lang=sauce_lang)
translation_data["translated"]["__tags__"] = message.data["__tags__"]

message.context["translation_data"] = translation_data
return message

def create_universal_handler(self, handler):
Expand All @@ -91,12 +125,31 @@ def speak(self, utterance, *args, **kwargs):
# translate speech from input lang to output lang
out_lang = self.lang # from message or config
sauce_lang = self.internal_language # skill output is in this language
utterance = self.translate_utterance(utterance, sauce_lang, out_lang)
if out_lang != sauce_lang or self.autodetect:
meta = kwargs.get("meta") or {}
meta["translation_data"] = {
"original": utterance,
"internal_lang": self.internal_language,
"target_lang": out_lang
}
utterance = self.translate_utterance(utterance, sauce_lang, out_lang)
meta["translation_data"]["translated"] = utterance
kwargs["meta"] = meta
super().speak(utterance, *args, **kwargs)


class UniversalFallback(UniversalSkill, OVOSFallbackSkill):
''' Fallback Skill that auto translates input/output from any language '''
''' Fallback Skill that auto translates input/output from any language
fallback handlers are ensured to receive utterances in self.internal_language
fallback handlers are expected to produce utterances in self.internal_language
self.speak will always translate utterances from self.internal_lang to self.lang
NOTE: self.lang reflects the original query language
but received utterances are always in self.internal_language
'''

def create_universal_fallback_handler(self, handler):
def universal_fallback_handler(message):
Expand All @@ -113,3 +166,58 @@ def universal_fallback_handler(message):
def register_fallback(self, handler, priority):
handler = self.create_universal_fallback_handler(handler)
super().register_fallback(handler, priority)


class UniversalCommonQuerySkill(UniversalSkill, CommonQuerySkill):
''' CommonQuerySkill that auto translates input/output from any language
CQS_match_query_phrase and CQS_action are ensured to received phrase in self.internal_language
CQS_match_query_phrase is assumed to return a response in self.internal_lang
it will be translated back before speaking
self.speak will always translate utterances from self.internal_lang to self.lang
NOTE: self.lang reflects the original query language
but received utterances are always in self.internal_language
'''

def __handle_query_action(self, message):
"""Message handler for question:action.
Extracts phrase and data from message forward this to the skills
CQS_action method.
"""
if message.data["skill_id"] != self.skill_id:
# Not for this skill!
return
if self.lang != self.internal_language or self.autodetect:
message.data["phrase"] = self.translate_utterance(message.data["phrase"],
sauce_lang=self.lang,
target_lang=self.internal_language)

super().__handle_query_action(message)

def __get_cq(self, search_phrase):
if self.lang == self.internal_language and not self.autodetect:
return super().__get_cq(search_phrase)

# convert input into internal lang
search_phrase = self.translate_utterance(search_phrase, self.internal_language, self.lang)
result = super().__get_cq(search_phrase)
if not result:
return None
answer = result[2]
# convert response back into source lang
answer = self.translate_utterance(answer, self.lang, self.internal_language)
if len(result) > 3:
# optional callback_data
result = (result[0], result[1], answer, result[3])
else:
result = (result[0], result[1], answer)
return result

def remove_noise(self, phrase, lang=None):
"""remove noise to produce essence of question"""
return super().remove_noise(phrase, self.internal_language)

10 changes: 10 additions & 0 deletions ovos_workshop/skills/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from ovos_utils.intents import ConverseTracker
from ovos_utils.intents import Intent, IntentBuilder
from ovos_utils.intents.intent_service_interface import munge_regex, munge_intent_parser, IntentServiceInterface
from ovos_utils.json_helper import merge_dict
from ovos_utils.log import LOG
from ovos_utils.messagebus import get_handler_name, create_wrapper, EventContainer, get_message_lang
from ovos_utils.parse import match_one
Expand Down Expand Up @@ -1652,10 +1653,19 @@ def speak(self, utterance, expect_response=False, wait=False, meta=None):
'expect_response': expect_response,
'meta': meta,
'lang': self.lang}

# grab message that triggered speech so we can keep context
message = dig_for_message()
m = message.forward("speak", data) if message \
else Message("speak", data)
m.context["skill_id"] = self.skill_id

# update any auto-translation metadata in message.context
if "translation_data" in meta:
tx_data = merge_dict(m.context.get("translation_data", {}),
meta["translation_data"])
m.context["translation_data"] = tx_data

self.bus.emit(m)

if wait:
Expand Down
43 changes: 29 additions & 14 deletions ovos_workshop/skills/common_query_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ def __init__(self, name=None, bus=None):
default_res = f"{dirname(dirname(__file__))}/res/text/{self.lang}/noise_words.list"
noise_words_filename = resolve_resource_file(noise_words_filepath) or \
resolve_resource_file(default_res)
self.translated_noise_words = []

self._translated_noise_words = {}
if noise_words_filename:
with open(noise_words_filename) as f:
self.translated_noise_words = f.read().strip()
self.translated_noise_words = self.translated_noise_words.split()
translated_noise_words = f.read().strip()
self._translated_noise_words[self.lang] = translated_noise_words.split()

# these should probably be configurable
self.level_confidence = {
Expand All @@ -75,6 +76,16 @@ def __init__(self, name=None, bus=None):
CQSMatchLevel.GENERAL: 0.5
}

@property
def translated_noise_words(self):
LOG.warning("self.translated_noise_words will become a private variable in next release")
return self._translated_noise_words.get(self.lang, [])

@translated_noise_words.setter
def translated_noise_words(self, val):
LOG.warning("self.translated_noise_words will become a private variable in next release")
self._translated_noise_words[self.lang] = val

def bind(self, bus):
"""Overrides the default bind method of MycroftSkill.
Expand All @@ -95,20 +106,14 @@ def __handle_question_query(self, message):
"skill_id": self.skill_id,
"searching": True}))

# Now invoke the CQS handler to let the skill perform its search
try:
result = self.CQS_match_query_phrase(search_phrase)
except:
LOG.exception(f"error matching {search_phrase} with {self.skill_id}")
result = None
result = self.__get_cq(search_phrase)

if result:
match = result[0]
level = result[1]
answer = result[2]
callback = result[3] if len(result) > 3 else None
confidence = self.__calc_confidence(
match, search_phrase, level, answer)
confidence = self.__calc_confidence(match, search_phrase, level, answer)
self.bus.emit(message.response({"phrase": search_phrase,
"skill_id": self.skill_id,
"answer": answer,
Expand All @@ -120,10 +125,20 @@ def __handle_question_query(self, message):
"skill_id": self.skill_id,
"searching": False}))

def remove_noise(self, phrase):
def __get_cq(self, search_phrase):
# Now invoke the CQS handler to let the skill perform its search
try:
result = self.CQS_match_query_phrase(search_phrase)
except:
LOG.exception(f"error matching {search_phrase} with {self.skill_id}")
result = None
return result

def remove_noise(self, phrase, lang=None):
"""remove noise to produce essence of question"""
lang = lang or self.lang
phrase = ' ' + phrase + ' '
for word in self.translated_noise_words:
for word in self._translated_noise_words.get(lang, []):
mtch = ' ' + word + ' '
if phrase.find(mtch) > -1:
phrase = phrase.replace(mtch, " ")
Expand Down Expand Up @@ -183,7 +198,7 @@ def __handle_query_action(self, message):

@abstractmethod
def CQS_match_query_phrase(self, phrase):
"""Analyze phrase to see if it is a play-able phrase with this skill.
"""Analyze phrase to see if it is a answer-able phrase with this skill.
Needs to be implemented by the skill.
Expand Down

0 comments on commit 1149a97

Please sign in to comment.