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

feat:skilljson and homescreen #283

Merged
merged 8 commits into from
Nov 15, 2024
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
22 changes: 21 additions & 1 deletion ovos_workshop/decorators/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions ovos_workshop/res/text/en/skill.error.dialog
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
An error occurred while processing a request in {skill}
127 changes: 46 additions & 81 deletions ovos_workshop/resource_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,7 +42,8 @@
"template",
"vocabulary",
"word",
"qml"
"qml",
"json"
]
)

Expand All @@ -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():
Expand All @@ -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 = []
Expand All @@ -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]:
"""
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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}")

Expand All @@ -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 {}

JarbasAl marked this conversation as resolved.
Show resolved Hide resolved

class DialogFile(ResourceFile):
"""Defines a dialog file, which is used instruct TTS what to speak."""

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -646,14 +606,19 @@ 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:
resource_type.locate_user_directory(self.skill_id)
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()
JarbasAl marked this conversation as resolved.
Show resolved Hide resolved

def load_dialog_file(self, name: str,
data: Optional[dict] = None) -> List[str]:
"""
Expand All @@ -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.

Expand Down Expand Up @@ -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]:
"""
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions ovos_workshop/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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
Expand Down
Loading
Loading