diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f5273e..0d95d389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,12 @@ # Changelog -## [2.3.2a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/2.3.2a1) (2024-11-12) +## [2.4.0a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/2.4.0a1) (2024-11-15) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/2.3.1...2.3.2a1) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/2.3.2...2.4.0a1) **Merged pull requests:** -- fix:es\_euphony [\#281](https://github.com/OpenVoiceOS/OVOS-workshop/pull/281) ([JarbasAl](https://github.com/JarbasAl)) +- feat:skilljson and homescreen [\#283](https://github.com/OpenVoiceOS/OVOS-workshop/pull/283) ([JarbasAl](https://github.com/JarbasAl)) diff --git a/ovos_workshop/decorators/__init__.py b/ovos_workshop/decorators/__init__.py index e42d8029..3dbc8432 100644 --- a/ovos_workshop/decorators/__init__.py +++ b/ovos_workshop/decorators/__init__.py @@ -1,5 +1,5 @@ from functools import wraps - +from typing import Optional from ovos_utils.log import log_deprecation from ovos_workshop.decorators.killable import killable_intent, killable_event @@ -157,3 +157,23 @@ def real_decorator(func): return func return real_decorator + + +def homescreen_app(icon: str, name: Optional[str] = None): + """ + Decorator for adding a method as a homescreen app + + the icon file MUST be located under 'gui' subfolder + + @param icon: icon file to use in app drawer (relative to "gui" folder) + @param name: short name to show under the icon in app drawer + """ + + def real_decorator(func): + # Store the icon inside the function + # This will be used later to call register_homescreen_app + func.homescreen_app_icon = icon + func.homescreen_app_name = name + return func + + return real_decorator diff --git a/ovos_workshop/res/text/en/skill.error.dialog b/ovos_workshop/res/text/en/skill.error.dialog new file mode 100644 index 00000000..03f10b6e --- /dev/null +++ b/ovos_workshop/res/text/en/skill.error.dialog @@ -0,0 +1 @@ +An error occurred while processing a request in {skill} \ No newline at end of file diff --git a/ovos_workshop/resource_files.py b/ovos_workshop/resource_files.py index 4bdd42a7..20e777f0 100644 --- a/ovos_workshop/resource_files.py +++ b/ovos_workshop/resource_files.py @@ -13,12 +13,14 @@ # limitations under the License. # """Handling of skill data such as intents and regular expressions.""" +import abc +import json import re from collections import namedtuple from os import walk from os.path import dirname from pathlib import Path -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Dict, Any from langcodes import tag_distance from ovos_config.config import Configuration @@ -40,7 +42,8 @@ "template", "vocabulary", "word", - "qml" + "qml", + "json" ] ) @@ -56,8 +59,7 @@ def locate_base_directories(skill_directory: str, """ base_dirs = [Path(skill_directory, resource_subdirectory)] if \ resource_subdirectory else [] - base_dirs += [Path(skill_directory, "locale"), - Path(skill_directory, "text")] + base_dirs += [Path(skill_directory, "locale")] candidates = [] for directory in base_dirs: if directory.exists(): @@ -76,8 +78,7 @@ def locate_lang_directories(lang: str, skill_directory: str, @param resource_subdirectory: optional extra resource directory to prepend @return: list of existing skill resource directories for the given lang """ - base_dirs = [Path(skill_directory, "locale"), - Path(skill_directory, "text")] + base_dirs = [Path(skill_directory, "locale")] if resource_subdirectory: base_dirs.append(Path(skill_directory, resource_subdirectory)) candidates = [] @@ -100,41 +101,6 @@ def locate_lang_directories(lang: str, skill_directory: str, return [c[0] for c in candidates] -def resolve_resource_file(res_name: str) -> Optional[str]: - """Convert a resource into an absolute filename. - - Resource names are in the form: 'filename.ext' - or 'path/filename.ext' - - The system wil look for $XDG_DATA_DIRS/mycroft/res_name first - (defaults to ~/.local/share/mycroft/res_name), and if not found will - look at /opt/mycroft/res_name, then finally it will look for res_name - in the 'mycroft/res' folder of the source code package. - - Example: - With mycroft running as the user 'bob', if you called - ``resolve_resource_file('snd/beep.wav')`` - it would return either: - '$XDG_DATA_DIRS/mycroft/beep.wav', - '/home/bob/.mycroft/snd/beep.wav' or - '/opt/mycroft/snd/beep.wav' or - '.../mycroft/res/snd/beep.wav' - where the '...' is replaced by the path - where the package has been installed. - - Args: - res_name (str): a resource path/name - - Returns: - (str) path to resource or None if no resource found - """ - log_deprecation(f"This method has moved to `ovos_utils.file_utils`", - "0.1.0") - from ovos_utils.file_utils import resolve_resource_file - config = Configuration() - return resolve_resource_file(res_name, config=config) - - def find_resource(res_name: str, root_dir: str, res_dirname: str, lang: Optional[str] = None) -> Optional[Path]: """ @@ -254,12 +220,13 @@ def locate_base_directory(self, skill_directory: str) -> Optional[str]: return # check for lang resources shipped by the skill - possible_directories = ( - Path(skill_directory, "locale", self.language), - Path(skill_directory, resource_subdirectory, self.language), - Path(skill_directory, resource_subdirectory), - Path(skill_directory, "text", self.language), - ) + possible_directories = [Path(skill_directory, "locale", self.language)] + if resource_subdirectory: + possible_directories += [ + Path(skill_directory, resource_subdirectory, self.language), + Path(skill_directory, resource_subdirectory) + ] + for directory in possible_directories: if directory.exists(): self.base_directory = directory @@ -279,7 +246,7 @@ def locate_base_directory(self, skill_directory: str) -> Optional[str]: if self.user_directory: self.base_directory = self.user_directory - def _get_resource_subdirectory(self) -> str: + def _get_resource_subdirectory(self) -> Optional[str]: """Returns the subdirectory for this resource type. In the older directory schemes, several resource types were stored @@ -295,10 +262,10 @@ def _get_resource_subdirectory(self) -> str: template="dialog", vocab="vocab", word="dialog", - qml="ui" + qml="gui" ) - return subdirectories[self.resource_type] + return subdirectories.get(self.resource_type) class ResourceFile: @@ -315,14 +282,13 @@ def __init__(self, resource_type: ResourceType, resource_name: str): self.resource_name = resource_name self.file_path = self._locate() - def _locate(self) -> str: + def _locate(self) -> Optional[str]: """Locates a resource file in the skill's locale directory. A skill's locale directory can contain a subdirectory structure defined by the skill author. Walk the directory and any subdirectories to find the resource file. """ - from ovos_utils.file_utils import resolve_resource_file file_path = None if self.resource_name.endswith(self.resource_type.file_extension): file_name = self.resource_name @@ -345,22 +311,12 @@ def _locate(self) -> str: if file_name in file_names: file_path = Path(directory, file_name) - # check the core resources - if file_path is None and self.resource_type.language: - sub_path = Path("text", self.resource_type.language, file_name) - file_path = resolve_resource_file(str(sub_path), - config=Configuration()) - - # check non-lang specific core resources if file_path is None: - file_path = resolve_resource_file(file_name, - config=Configuration()) - - if file_path is None: - LOG.error(f"Could not find resource file {file_name}") + LOG.debug(f"Could not find resource file {file_name} for lang: {self.resource_type.language}") return file_path + @abc.abstractmethod def load(self): """Override in subclass to define resource type loading behavior.""" pass @@ -377,7 +333,6 @@ def _read(self) -> str: class QmlFile(ResourceFile): def _locate(self): """ QML files are special because we do not want to walk the directory """ - from ovos_utils.file_utils import resolve_resource_file file_path = None if self.resource_name.endswith(self.resource_type.file_extension): file_name = self.resource_name @@ -398,13 +353,6 @@ def _locate(self): if x.is_file() and file_name == x.name: file_path = Path(self.resource_type.base_directory, file_name) - # check the core resources - if file_path is None: - file_path = resolve_resource_file(file_name, - config=Configuration()) or \ - resolve_resource_file(f"ui/{file_name}", - config=Configuration()) - if file_path is None: LOG.error(f"Could not find resource file {file_name}") @@ -414,6 +362,17 @@ def load(self): return str(self.file_path) +class JsonFile(ResourceFile): + def load(self) -> Dict[str, Any]: + if self.file_path is not None: + try: + with open(self.file_path) as f: + return json.load(f) + except Exception as e: + LOG.error(f"Failed to load {self.file_path}: {e}") + return {} + + class DialogFile(ResourceFile): """Defines a dialog file, which is used instruct TTS what to speak.""" @@ -474,6 +433,7 @@ def load(self) -> List[List[str]]: class IntentFile(ResourceFile): """Defines an intent file, which skill use to form intents.""" + def __init__(self, resource_type, resource_name): super().__init__(resource_type, resource_name) self.data = None @@ -646,7 +606,8 @@ def _define_resource_types(self) -> SkillResourceTypes: template=ResourceType("template", ".template", self.language), vocabulary=ResourceType("vocab", ".voc", self.language), word=ResourceType("word", ".word", self.language), - qml=ResourceType("qml", ".qml") + qml=ResourceType("qml", ".qml"), + json=ResourceType("json", ".json", self.language) ) for resource_type in resource_types.values(): if self.skill_id: @@ -654,6 +615,10 @@ def _define_resource_types(self) -> SkillResourceTypes: resource_type.locate_base_directory(self.skill_directory) return SkillResourceTypes(**resource_types) + def load_json_file(self, name: str = "skill.json") -> Dict[str, str]: + jsonf = JsonFile(self.types.json, name) + return jsonf.load() + def load_dialog_file(self, name: str, data: Optional[dict] = None) -> List[str]: """ @@ -671,10 +636,10 @@ def load_dialog_file(self, name: str, dialog_file = DialogFile(self.types.dialog, name) dialog_file.data = data return dialog_file.load() - + def load_intent_file(self, name: str, - data: Optional[dict] = None, - entities: bool = True) -> List[str]: + data: Optional[dict] = None, + entities: bool = True) -> List[str]: """ Loads the contents of an intent file. @@ -858,7 +823,7 @@ def load_skill_regex(self, alphanumeric_skill_id: str) -> List[str]: ) return skill_regexes - + @classmethod def get_available_languages(cls, skill_directory: str) -> List[str]: """ @@ -885,22 +850,22 @@ def get_inventory(self, specific_type: str = "", language: str = "en-us"): if language not in languages: raise ValueError(f"Language {language} not available for skill") - inventory = dict() + inventory = dict() for type_ in self.types: if specific_type and type_.resource_type != specific_type: continue inventory[type_.resource_type] = list() - + # search all files in the directory and subdirectories and dump its name in a list base_dirs = locate_lang_directories(language, self.skill_directory) for directory in base_dirs: for file in directory.iterdir(): if file.suffix == type_.file_extension: inventory[type_.resource_type].append(file.stem) - + inventory["languages"] = languages - + return inventory @staticmethod diff --git a/ovos_workshop/settings.py b/ovos_workshop/settings.py index ca826806..2a6c15c7 100644 --- a/ovos_workshop/settings.py +++ b/ovos_workshop/settings.py @@ -116,7 +116,7 @@ def save_meta(self, generate: bool = False): @requires_backend def upload(self, generate: bool = False): if not is_paired(): - LOG.error("Device needs to be paired to upload settings") + LOG.debug("Device needs to be paired to upload settings") return self.remote_settings.settings = dict(self.skill.settings) if generate: @@ -126,7 +126,7 @@ def upload(self, generate: bool = False): @requires_backend def upload_meta(self, generate: bool = False): if not is_paired(): - LOG.error("Device needs to be paired to upload settingsmeta") + LOG.debug("Device needs to be paired to upload settingsmeta") return if generate: self.remote_settings.settings = dict(self.skill.settings) @@ -136,7 +136,7 @@ def upload_meta(self, generate: bool = False): @requires_backend def download(self): if not is_paired(): - LOG.error("Device needs to be paired to download remote settings") + LOG.debug("Device needs to be paired to download remote settings") return self.remote_settings.download() # we do not update skill object settings directly diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index c96c9ddb..3d452082 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -2,6 +2,7 @@ import json import os import re +import shutil import sys import time import traceback @@ -16,8 +17,6 @@ import binascii from json_database import JsonStorage from langcodes import closest_match -from ovos_number_parser import pronounce_number, extract_number -from ovos_yes_no_solver import YesNoSolver from ovos_bus_client import MessageBusClient from ovos_bus_client.apis.enclosure import EnclosureAPI from ovos_bus_client.apis.gui import GUIInterface @@ -26,10 +25,12 @@ from ovos_bus_client.session import SessionManager, Session from ovos_bus_client.util import get_message_lang from ovos_config.config import Configuration +from ovos_config.locations import get_xdg_cache_save_path from ovos_config.locations import get_xdg_config_save_path +from ovos_number_parser import pronounce_number, extract_number from ovos_plugin_manager.language import OVOSLangTranslationFactory, OVOSLangDetectionFactory from ovos_utils import camel_case_split, classproperty -from ovos_utils.dialog import get_dialog, MustacheDialogRenderer +from ovos_utils.dialog import MustacheDialogRenderer from ovos_utils.events import EventContainer, EventSchedulerInterface from ovos_utils.events import get_handler_name, create_wrapper from ovos_utils.file_utils import FileWatcher @@ -38,9 +39,12 @@ from ovos_utils.lang import standardize_lang_tag from ovos_utils.log import LOG from ovos_utils.parse import match_one -from ovos_utils.process_utils import ProcessStatus, StatusCallbackMap, ProcessState +from ovos_utils.process_utils import ProcessStatus, StatusCallbackMap from ovos_utils.process_utils import RuntimeRequirements from ovos_utils.skills import get_non_properties +from ovos_yes_no_solver import YesNoSolver +from padacioso import IntentContainer + from ovos_workshop.decorators.killable import AbortEvent, killable_event, \ AbortQuestion from ovos_workshop.decorators.layers import IntentLayers @@ -51,7 +55,6 @@ CoreResources, find_resource, SkillResources from ovos_workshop.settings import PrivateSettings from ovos_workshop.settings import SkillSettingsManager -from padacioso import IntentContainer def simple_trace(stack_trace: List[str]) -> str: @@ -811,7 +814,9 @@ def _startup(self, bus: MessageBusClient, skill_id: str = ""): if self._enable_settings_manager: self._init_settings_manager() self.load_data_files() + self._register_skill_json() self._register_decorated() + self._register_app_launcher() self.register_resting_screen() self.status.set_started() @@ -829,6 +834,42 @@ def _startup(self, bus: MessageBusClient, skill_id: str = ""): LOG.debug(e2) raise e + def _register_skill_json(self, root_directory: Optional[str] = None): + """Load skill.json metadata found under locale folder and register with homescreen""" + root_directory = root_directory or self.res_dir + for lang in self.native_langs: + resources = self.load_lang(root_directory, lang) + if resources.types.json.base_directory is None: + self.log.debug(f'No skill.json loaded for {lang}') + else: + skill_meta = resources.load_json_file("skill.json") + utts = skill_meta.get("examples", []) + if utts: + self.log.info(f"Registering example utterances with homescreen for lang: {lang} - {utts}") + self.bus.emit(Message("homescreen.register.examples", + {"skill_id": self.skill_id, "utterances": utts, "lang": lang})) + + def _register_app_launcher(self): + # register app launcher if registered via decorator + for attr_name in get_non_properties(self): + method = getattr(self, attr_name) + if hasattr(method, 'homescreen_app_icon'): + name = getattr(method, 'homescreen_app_name') + event = f"{self.skill_id}.{name or method.__name__}.homescreen.app" + icon = getattr(method, 'homescreen_app_icon') + name = name or self.__skill_id2name + LOG.debug(f"homescreen app registered: {name} - '{event}'") + self.register_homescreen_app(icon=icon, + name=name or self.skill_id, + event=event) + self.add_event(event, method, speak_errors=False) + + @property + def __skill_id2name(self) -> str: + """helper to make a nice string out of a skill_id""" + return (self.skill_id.split(".")[0].replace("_", " "). + replace("-", " ").replace("skill", "").title().strip()) + def _init_settings(self): """ Set up skill settings. Defines settings in the specified file path, @@ -853,7 +894,7 @@ def _init_settings(self): # starting on ovos-core 0.0.8 a bus event is emitted # all settings.json files are monitored for changes in ovos-core - self.add_event("ovos.skills.settings_changed", self._handle_settings_changed) + self.add_event("ovos.skills.settings_changed", self._handle_settings_changed, speak_errors=False) if self._monitor_own_settings: self._start_filewatcher() @@ -882,6 +923,23 @@ def _init_settings_manager(self): """ self.settings_manager = SkillSettingsManager(self) + def register_homescreen_app(self, icon: str, name: str, event: str): + """the icon file MUST be located under 'gui' subfolder""" + # this path is hardcoded in ovos_gui.constants and follows XDG spec + # we use it to ensure resource availability between containers + # it is the only path assured to be accessible both by skills and GUI + GUI_CACHE_PATH = get_xdg_cache_save_path('ovos_gui') + + full_icon_path = f"{self.root_dir}/gui/{icon}" + shared_path = f"{GUI_CACHE_PATH}/{self.skill_id}/{icon}" + shutil.copy(full_icon_path, shared_path) + + self.bus.emit(Message("homescreen.register.app", + {"skill_id": self.skill_id, + "icon": shared_path, + "name": name, + "event": event})) + def register_resting_screen(self): """ Registers resting screen from the resting_screen_handler decorator. @@ -889,30 +947,27 @@ def register_resting_screen(self): This only allows one screen and if two is registered only one will be used. """ - resting_name = None for attr_name in get_non_properties(self): handler = getattr(self, attr_name) if hasattr(handler, 'resting_handler'): resting_name = handler.resting_handler + LOG.debug(f"{get_handler_name(handler)} is a resting screen, name: {resting_name}") - def register(message=None): - self.log.info(f'Registering resting screen {resting_name} for {self.skill_id}.') + def register(message=None, name=resting_name): + self.log.info(f'Registering resting screen {name} for {self.skill_id}.') self.bus.emit(Message("homescreen.manager.add", - {"class": "IdleDisplaySkill", # TODO - rm in ovos-gui, only for compat - "name": resting_name, - "id": self.skill_id})) + {"name": name, "id": self.skill_id})) register() # initial registering - self.add_event("homescreen.manager.reload.list", register, - speak_errors=False) + self.add_event("homescreen.manager.reload.list", register, speak_errors=False) - def wrapper(message): + def wrapper(message, cb=handler): if message.data["homescreen_id"] == self.skill_id: - handler(message) + LOG.debug(f"triggering resting_handler: {get_handler_name(cb)}") + cb(message) - self.add_event("homescreen.manager.activate.display", wrapper, - speak_errors=False) + self.add_event("homescreen.manager.activate.display", wrapper, speak_errors=False) def shutdown_handler(message): if message.data["id"] == self.skill_id: @@ -920,14 +975,8 @@ def shutdown_handler(message): {"id": self.skill_id}) self.bus.emit(msg) - self.add_event("mycroft.skills.shutdown", shutdown_handler, - speak_errors=False) - - # backwards compat listener - self.add_event(f'{self.skill_id}.idle', handler, - speak_errors=False) - - return + self.add_event("mycroft.skills.shutdown", shutdown_handler, speak_errors=False) + break # TODO - if multiple decorators are used what do? this is not deterministic def _start_filewatcher(self): """ @@ -976,11 +1025,9 @@ def _upload_settings(self): """ Upload settings to a remote backend if configured. """ - if self.settings_manager and self.config_core.get("skills", - {}).get("sync2way"): + if self.settings_manager and self.config_core.get("skills", {}).get("sync2way"): # upload new settings to backend - generate = self.config_core.get("skills", {}).get("autogen_meta", - True) + generate = self.config_core.get("skills", {}).get("autogen_meta", True) # this will check global sync flag self.settings_manager.upload(generate) if generate: @@ -1058,31 +1105,22 @@ def _register_system_event_handlers(self): self.add_event(f"{self.skill_id}.stop", self._handle_session_stop, speak_errors=False) self.add_event(f"{self.skill_id}.stop.ping", self._handle_stop_ack, speak_errors=False) - self.add_event(f"{self.skill_id}.converse.ping", self._handle_converse_ack, - speak_errors=False) - self.add_event(f"{self.skill_id}.converse.request", self._handle_converse_request, - speak_errors=False) - self.add_event(f"{self.skill_id}.activate", self.handle_activate, - speak_errors=False) - self.add_event(f"{self.skill_id}.deactivate", self.handle_deactivate, - speak_errors=False) - self.add_event("intent.service.skills.deactivated", - self._handle_skill_deactivated, speak_errors=False) - self.add_event("intent.service.skills.activated", - self._handle_skill_activated, speak_errors=False) - self.add_event('mycroft.skill.enable_intent', self.handle_enable_intent, - speak_errors=False) - self.add_event('mycroft.skill.disable_intent', - self.handle_disable_intent, speak_errors=False) - self.add_event('mycroft.skill.set_cross_context', - self.handle_set_cross_context, speak_errors=False) - self.add_event('mycroft.skill.remove_cross_context', - self.handle_remove_cross_context, speak_errors=False) - self.add_event('mycroft.skills.settings.changed', - self.handle_settings_change, speak_errors=False) - - self.add_event(f"{self.skill_id}.converse.get_response", self.__handle_get_response, - speak_errors=False) + self.add_event(f"{self.skill_id}.converse.ping", self._handle_converse_ack, speak_errors=False) + self.add_event(f"{self.skill_id}.converse.request", self._handle_converse_request, speak_errors=False) + self.add_event(f"{self.skill_id}.activate", self.handle_activate, speak_errors=False) + self.add_event(f"{self.skill_id}.deactivate", self.handle_deactivate, speak_errors=False) + self.add_event("intent.service.skills.deactivated", self._handle_skill_deactivated, speak_errors=False) + self.add_event("intent.service.skills.activated", self._handle_skill_activated, speak_errors=False) + self.add_event('mycroft.skill.enable_intent', self.handle_enable_intent, speak_errors=False) + self.add_event('mycroft.skill.disable_intent', self.handle_disable_intent, speak_errors=False) + self.add_event('mycroft.skill.set_cross_context', self.handle_set_cross_context, speak_errors=False) + self.add_event('mycroft.skill.remove_cross_context', self.handle_remove_cross_context, speak_errors=False) + self.add_event('mycroft.skills.settings.changed', self.handle_settings_change, speak_errors=False) + + self.add_event(f"{self.skill_id}.converse.get_response", self.__handle_get_response, speak_errors=False) + + # homescreen might load after this skill and miss the original events + self.add_event("homescreen.metadata.get", self.handle_homescreen_loaded, speak_errors=False) def _send_public_api(self, message: Message): """ @@ -1464,6 +1502,11 @@ def register_regex(self, regex_str: str, lang: Optional[str] = None): self.intent_service.register_adapt_regex(regex, lang=standardize_lang_tag(lang or self.lang)) # event/intent registering internal handlers + def handle_homescreen_loaded(self, message: Message): + """homescreen loaded, we should re-register any metadata we want to provide""" + self._register_skill_json() + self._register_app_launcher() + def handle_enable_intent(self, message: Message): """ Listener to enable a registered intent if it belongs to this skill. @@ -1554,7 +1597,7 @@ def _on_event_error(self, error: str, message: Message, handler_info: str, # Convert "MyFancySkill" to "My Fancy Skill" for speaking handler_name = camel_case_split(self.name) msg_data = {'skill': handler_name} - speech = get_dialog('skill.error', self.lang, msg_data) + speech = _get_dialog('skill.error', self.lang, msg_data) if speak_errors: self.speak(speech) self.log.exception(error) @@ -1671,7 +1714,7 @@ def speak_dialog(self, key: str, data: Optional[dict] = None, expect_response, wait, meta={'dialog': key, 'data': data} ) else: - self.log.warning( + self.log.error( 'dialog_render is None, does the locale/dialog folder exist?' ) self.speak(key, expect_response, wait, {}) @@ -2044,7 +2087,8 @@ def ask_selection(self, options: List[str], dialog: str = '', Returns: string: list element selected by user, or None """ - assert isinstance(options, list) + if not isinstance(options, list): + raise ValueError("invalid value for 'options', must be a list of strings") if not len(options): return None @@ -2164,7 +2208,7 @@ def add_event(self, name: str, handler: callable, Create event handler for executing intent or other event. Args: - name (string): IntentParser name + name (string): event name handler (func): Method to call handler_info (string): Base message when reporting skill event handler status on messagebus. @@ -2183,7 +2227,8 @@ def on_error(error, message): self._on_event_end(message, handler_info, skill_data, is_intent=is_intent) return - self._on_event_error(error, message, handler_info, skill_data, + LOG.error(f"Error handling event '{name}' : {error}") + self._on_event_error(str(error), message, handler_info, skill_data, speak_errors) def on_start(message): @@ -2496,6 +2541,33 @@ def __init__(self, skill: OVOSSkill): ui_directories=ui_directories) +def _get_dialog(phrase: str, lang: str, context: Optional[dict] = None) -> str: + """ + Looks up a resource file for the given phrase in the specified language. + + Meant only for resources bundled with ovos-workshop and shared across skills + + Args: + phrase (str): resource phrase to retrieve/translate + lang (str): the language to use + context (dict): values to be inserted into the string + + Returns: + str: a randomized and/or translated version of the phrase + """ + filename = f"{dirname(dirname(__file__))}/res/text/{lang.split('-')[0]}/{phrase}.dialog" + + if not isfile(filename): + LOG.debug('Resource file not found: {}'.format(filename)) + return phrase + + stache = MustacheDialogRenderer() + stache.load_template_file('template', filename) + if not context: + context = {} + return stache.render('template', context) + + def _get_word(lang, connector): """ Helper to get word translations diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 2b024b65..b7348728 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -1,6 +1,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 2 -VERSION_MINOR = 3 -VERSION_BUILD = 2 -VERSION_ALPHA = 0 +VERSION_MINOR = 4 +VERSION_BUILD = 0 +VERSION_ALPHA = 1 # END_VERSION_BLOCK diff --git a/test/unittests/test_resource_files.py b/test/unittests/test_resource_files.py index e2ae7148..da25ef71 100644 --- a/test/unittests/test_resource_files.py +++ b/test/unittests/test_resource_files.py @@ -15,10 +15,6 @@ def test_locate_lang_directories(self): from ovos_workshop.resource_files import locate_lang_directories # TODO - def test_resolve_resource_file(self): - from ovos_workshop.resource_files import resolve_resource_file - # TODO - def test_find_resource(self): from ovos_workshop.resource_files import find_resource test_dir = join(dirname(__file__), "test_res")