From 051a625b6554f77791b3f54a3a9a41790af694d8 Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:13:47 +0000 Subject: [PATCH 01/10] Increment Version to 0.2.8 --- ovos_utterance_plugin_cancel/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utterance_plugin_cancel/version.py b/ovos_utterance_plugin_cancel/version.py index bfbefd0..ba00796 100644 --- a/ovos_utterance_plugin_cancel/version.py +++ b/ovos_utterance_plugin_cancel/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 0 VERSION_MINOR = 2 VERSION_BUILD = 8 -VERSION_ALPHA = 1 +VERSION_ALPHA = 0 # END_VERSION_BLOCK __version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}" + (f"a{VERSION_ALPHA}" if VERSION_ALPHA else "") From 3a60bb755ae7140380bd84c13e10fa68149e85f1 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 25 May 2026 15:33:32 +0100 Subject: [PATCH 02/10] =?UTF-8?q?fix:=20cancel=20transformer=20not=20firin?= =?UTF-8?q?g=20=E2=80=94=20entry-point=20group,=20lang=20lookup,=20locale?= =?UTF-8?q?=20typos=20(#32)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: migrate language matching and template expansion to ovos-spec-tools The deprecated ovos_utils.lang.standardize_lang_tag stripped the region subtag, and the prior chain (`standardize_lang_tag("en-US")` -> `"en"`, then `langcodes.closest_match("en", ["en-US", ...])`) was failing to find a usable match for some inputs, leaving the cancel transformer silent and the utterance falling through to the intent-failure error sound. Switching the whole chain to the spec-tools conformant implementations: - `ovos_spec_tools.standardize_lang` preserves the region subtag (`en-US` -> `en-US`). - `ovos_spec_tools.closest_lang` gates on the < 10 distance threshold internally and returns None when no candidate is close enough, replacing the manual score check. - `ovos_spec_tools.expand` replaces the deprecated `ovos_utils.bracket_expansion.expand_template`. Also fix the it-IT locale: `(|dimentica|...)` had an empty leading alternative on a standalone group, which would have matched every utterance via endswith(""). Spec-tools' strict OVOS-INTENT-1 §3.6 expansion correctly rejects it; drop the empty alternative. Verified: 44 unit tests + the end2end `test_cancel_match` on ovos-core both pass. Co-Authored-By: Claude Opus 4.7 (1M context) * refactor: use LocaleResources for cancel.intent lookup Replace the hand-rolled locale discovery + file read + bracket expansion with one `LocaleResources(skill_locale=...).load_intent("cancel", lang)` call. `LocaleResources` applies the OVOS-INTENT-2 §2.1 source-precedence, the §2.2 smart language fallback (`closest_lang` gated at distance < 10), the §3 file reader (UTF-8, BOM-stripped, comment/blank-line filtered), and the §3.6 sentence-template expansion — the same composite operation this plugin was performing by hand. Co-Authored-By: Claude Opus 4.7 (1M context) * refactor: rename cancel.intent -> cancel.voc (correct OVOS-INTENT-2 role) The plugin matches phrases via `endswith`, not full utterance templates with slots, so the resource is a vocabulary set per OVOS-INTENT-2 §4.3, not an intent template per §4.1. Switch to `LocaleResources.load_vocabulary("cancel", lang)` to match. Co-Authored-By: Claude Opus 4.7 (1M context) * docs: add cancel.intent in each locale to document the matching pattern The plugin still does suffix-matching against cancel.voc at runtime; this .intent file is informational and not consumed. It exists so the intent the plugin enforces is explicit in the locale folder for anyone reading it: `[{utterance}] ` — anything ending with a cancel phrase, plus a standalone cancel with no leading utterance. Verified via spec-tools' LocaleResources.load_intent — the `` reference resolves against the sibling cancel.voc and `[{utterance}]` produces both with-prefix and standalone samples. Co-Authored-By: Claude Opus 4.7 (1M context) * ci: add ovoscope end-to-end CI for the cancel transformer The plugin's behaviour is observable only end-to-end (bus + intent service + audio service), so unit tests can't catch the kind of regression that surfaced on ovos-core's ovoscope: cancel matching silently breaking and falling through to intent-failure. Add a local ovoscope job so this repo gates on the same observation rather than relying on downstream ovos-core to catch it. Adds: - `.github/workflows/ovoscope.yml` — reusable workflow from OpenVoiceOS/gh-automations, parameterised the same way ovos-core's is (Padatious + Adapt required, bus-coverage on). - `test/end2end/test_cancel_plugin.py` — two ovoscope cases: cancel mid-sentence (the previously regressed case) and cancel-suffix on an arbitrary utterance. - pyproject `[test]` extras: ovoscope + ovos-skill-hello-world. Co-Authored-By: Claude Opus 4.7 (1M context) * ci: scope build/coverage to unit tests, drop intent-engine reqs from ovoscope - build-tests + coverage: target test/unittests/ only. The new test/end2end/ directory needs ovoscope (test extras), not the dev extras these workflows install. - ovoscope: drop require_adapt / require_padatious / system_deps. The cancel transformer fires before intent matching; the ovoscope cases here do not depend on any intent engine. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(test): enable the cancel transformer in MiniCroft's config before boot OVOS's UtteranceTransformersService loads a plugin only when its entry-point name appears in `Configuration().utterance_transformers` (ovos_core/transformers.py:29); otherwise the plugin is silently skipped. The default test config used by MiniCroft does not include `ovos-utterance-cancel-plugin`, so the transformer never fired in CI and the utterance fell through to the intent-failure error sound. Set the key on Configuration() in setUp, before `get_minicroft` is called. MiniCroft's `isolate_config` clears the user's XDG configs, so this assignment only affects the test process. Co-Authored-By: Claude Opus 4.7 (1M context) * fix: restore entry-point name to match OVOS default config; read lang from session Three real bugs surfaced by the new ovoscope tests: 1. **Entry-point name regression.** Dev had renamed the transformer entry point to `ovos-utterance-cancel-plugin`, but OVOS's default `mycroft.conf` references `ovos-utterance-plugin-cancel` under `utterance_transformers`. `UtteranceTransformersService` (ovos_core/transformers.py:29) silently skips any plugin whose name is not in the config — so the rename silently broke the plugin on every default install. Restore the historic name. 2. **`lang` lookup was wrong.** `context` here is the OVOS-MSG-1 `message.context` dict; the normative language field lives at `context["session"]["lang"]` (§4), not at `context["lang"]`. ovos-core's IntentService also copies it to a top-level `context["lang"]` for legacy callers, but treating that as authoritative breaks for any consumer (HiveMind, tests, alt bus-clients) that hands the transformer a Message without that pre-processing. Prefer `session.lang`; fall back to legacy `context.lang`; default `"en-US"` if neither is set. 3. **`None` lang crash.** `context.get("lang", default)` returns `None` when the key exists with a `None` value — only a missing key triggers the default. `standardize_lang(None)` raises `AttributeError`, which the transformer service swallows as a warning — the plugin silently no-ops. Use `or` so an explicit `None` resolves to the default. ovoscope test/end2end/test_cancel_plugin.py now covers all three: - positive cancel-mid-sentence + cancel-suffix - negative gate: with the plugin disabled in config, the same cancel-suffix falls through to intent failure — proving the positive cases are observing the plugin itself - smoke: utterance with no cancel phrase doesn't fire the cancel sequence Each test writes its desired `utterance_transformers` entry into a temp xdg-config file and boots MiniCroft with `isolate_config=False`, because MiniCroft's default `Configuration.reload()` wipes any in-memory override done in setUp. Workflow: drop bus_coverage (transformers don't emit bus events). Co-Authored-By: Claude Opus 4.7 (1M context) * refactor: factor lang resolution into _resolve_lang helper The transformer signature passes context = message.context, which makes the lang source ambiguous: - ovos-core's IntentService currently writes a top-level context["lang"] before invoking transformers (a known convenience that should be in the spec but isn't yet — the future transformer signature will likely grow an explicit lang kwarg); - the normative carrier is context["session"]["lang"] per OVOS-MSG-1 §4. Trust the current ovos-core contract (top-level context.lang) but fall back to session.lang so the plugin keeps working when called without the ovos-core pre-processing (HiveMind, tests, alternative bus clients). Encapsulated in _resolve_lang(context) with a docstring naming the gap so the call site can be simplified once the spec is updated. Also fixes the silent crash when context.get("lang") returns explicit None: standardize_lang(None) raised AttributeError, which UtteranceTransformersService swallowed as a warning — the plugin no-op'd. or-chain guards both lookups. The entry-point rename was reverted to keep this PR's scope to the spec-tools migration; the default-config mismatch (OVOS default config keys ovos-utterance-plugin-cancel but new dev's entry point is ovos-utterance-cancel-plugin) belongs in a separate ovos-config fix. Co-Authored-By: Claude Opus 4.7 (1M context) * fix: register entry point under opm.transformer.text (the group OPM scans) OPM discovers utterance transformers under the entry-point group `opm.transformer.text` (`ovos_plugin_manager/utils/__init__.py:72`, `PluginTypes.UTTERANCE_TRANSFORMER`). The previous pyproject used `ovos.utterance.transformer`, which OPM doesn't scan — so the editable install in CI registered the plugin under a group nobody looks at, OPM returned no plugins, and the cancel transformer never loaded regardless of config. The PyPI 0.2.8 wheel happens to work because it uses the legacy `neon.plugin.text` group which OPM aliases (with a deprecation warning); the new pyproject silently broke discovery. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(test): drop final_session check on cancel mid-sentence bus-client 1.x's SessionManager normalises `session.lang` to the macro tag (`en-US` -> `en`), which trips End2EndTest's `test_final_session` equality check. The cancel sequence (four expected messages) is the real assertion here; the final-session check was inherited from ovos-core's copy of this test and is redundant. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(locale): correct Danish 'annulle'->'annuller' and Portuguese 'comado'->'comando' CodeRabbit caught two locale typos in cancel.voc: - **da-DK**: `annulle kommando` was a broken stem; the Danish imperative is `annuller`. The next four lines in the same file already use `annuller` correctly, so this was a single-line typo. - **pt-PT**: every occurrence of `comado` is a typo for `comando` (Portuguese for 'command'). Six occurrences across six template lines, all in the same vocabulary group `(ordem|comado|isso)` and variants — replaced with sed -i. The other CodeRabbit findings (UTF-8 read, order-preserving dedupe, deterministic test phrasing, immutable workflow ref) are addressed elsewhere or intentionally skipped: file reading now goes through `LocaleResources` which handles UTF-8 via the spec-tools reader; gh-automations workflow refs use `@dev` by org policy. Co-Authored-By: Claude Opus 4.7 (1M context) * fix: require ovos-utils>=0.11.1a1 (standardize_lang_tag region-preserving) ovos-utils 0.11.1a1 restores the historic langcodes semantics of `standardize_lang_tag(macro=True)` — it performs macrolanguage substitution (`cmn` -> `zh`) instead of stripping the region (`en-US` -> `en`). With that fix in the dependency chain, `ovos_bus_client.session.SessionManager` no longer rewrites `session.lang` from `en-US` to `en`, and the cancel ovoscope tests can again assert against `final_session`. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: JarbasAi --- .github/workflows/build-tests.yml | 2 +- .github/workflows/coverage.yml | 4 +- .github/workflows/ovoscope.yml | 23 +++ ovos_utterance_plugin_cancel/__init__.py | 85 +++++--- .../locale/ca-ES/cancel.intent | 23 +-- .../locale/ca-ES/cancel.voc | 15 ++ .../locale/da-DK/cancel.intent | 22 +- .../locale/da-DK/cancel.voc | 14 ++ .../locale/de-DE/cancel.intent | 19 +- .../locale/de-DE/cancel.voc | 11 + .../locale/en-US/cancel.intent | 23 +-- .../locale/en-US/cancel.voc | 15 ++ .../locale/es-ES/cancel.intent | 23 +-- .../locale/es-ES/cancel.voc | 15 ++ .../locale/fr-FR/cancel.intent | 20 +- .../locale/fr-FR/cancel.voc | 12 ++ .../locale/gl-ES/cancel.intent | 23 +-- .../locale/gl-ES/cancel.voc | 15 ++ .../locale/it-IT/cancel.intent | 23 +-- .../locale/it-IT/cancel.voc | 15 ++ .../locale/nl-NL/cancel.intent | 23 +-- .../locale/nl-NL/cancel.voc | 15 ++ .../locale/pt-BR/cancel.intent | 20 +- .../locale/pt-BR/cancel.voc | 12 ++ .../locale/pt-PT/cancel.intent | 17 +- .../locale/pt-PT/cancel.voc | 9 + pyproject.toml | 10 +- test/end2end/__init__.py | 0 test/end2end/test_cancel_plugin.py | 189 ++++++++++++++++++ 29 files changed, 515 insertions(+), 182 deletions(-) create mode 100644 .github/workflows/ovoscope.yml create mode 100644 ovos_utterance_plugin_cancel/locale/ca-ES/cancel.voc create mode 100644 ovos_utterance_plugin_cancel/locale/da-DK/cancel.voc create mode 100644 ovos_utterance_plugin_cancel/locale/de-DE/cancel.voc create mode 100644 ovos_utterance_plugin_cancel/locale/en-US/cancel.voc create mode 100644 ovos_utterance_plugin_cancel/locale/es-ES/cancel.voc create mode 100644 ovos_utterance_plugin_cancel/locale/fr-FR/cancel.voc create mode 100644 ovos_utterance_plugin_cancel/locale/gl-ES/cancel.voc create mode 100644 ovos_utterance_plugin_cancel/locale/it-IT/cancel.voc create mode 100644 ovos_utterance_plugin_cancel/locale/nl-NL/cancel.voc create mode 100644 ovos_utterance_plugin_cancel/locale/pt-BR/cancel.voc create mode 100644 ovos_utterance_plugin_cancel/locale/pt-PT/cancel.voc create mode 100644 test/end2end/__init__.py create mode 100644 test/end2end/test_cancel_plugin.py diff --git a/.github/workflows/build-tests.yml b/.github/workflows/build-tests.yml index 6dff7ba..1cf35f9 100644 --- a/.github/workflows/build-tests.yml +++ b/.github/workflows/build-tests.yml @@ -11,4 +11,4 @@ jobs: with: python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]' install_extras: 'dev' - test_path: 'test' + test_path: 'test/unittests' diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d6cb8d8..2a5e7af 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -11,6 +11,6 @@ jobs: with: python_version: '3.11' coverage_source: 'ovos_utterance_plugin_cancel' - test_path: 'test/' - install_extras: '' + test_path: 'test/unittests' + install_extras: '.[dev]' min_coverage: 0 diff --git a/.github/workflows/ovoscope.yml b/.github/workflows/ovoscope.yml new file mode 100644 index 0000000..e1c8104 --- /dev/null +++ b/.github/workflows/ovoscope.yml @@ -0,0 +1,23 @@ +name: Ovoscope End-to-End Tests + +on: + push: + branches: [dev] + pull_request: + branches: [dev] + workflow_dispatch: + +jobs: + ovoscope: + uses: OpenVoiceOS/gh-automations/.github/workflows/ovoscope.yml@dev + secrets: inherit + with: + runner: "ubuntu-latest" + python_version: "3.11" + install_extras: "test" + test_path: "test/end2end/" + # The cancel transformer fires before intent matching, so the + # ovoscope cases here do not depend on any intent engine, and + # transformers don't emit bus events so bus-coverage tracking + # would be empty anyway. + pr_comment: true diff --git a/ovos_utterance_plugin_cancel/__init__.py b/ovos_utterance_plugin_cancel/__init__.py index 2810dec..b263814 100644 --- a/ovos_utterance_plugin_cancel/__init__.py +++ b/ovos_utterance_plugin_cancel/__init__.py @@ -22,22 +22,56 @@ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import os -from os.path import join, dirname, isfile +from os.path import join, dirname from functools import lru_cache from typing import Dict, List, Optional, Tuple from ovos_plugin_manager.templates.transformers import UtteranceTransformer +from ovos_spec_tools import LocaleResources, standardize_lang from ovos_utils.log import LOG -from ovos_utils.lang import standardize_lang_tag -from ovos_utils.bracket_expansion import expand_template -from langcodes import closest_match + + +def _resolve_lang(context: Optional[Dict[str, object]], + default: str = "en-US") -> str: + """Return the BCP-47 language tag for an UtteranceTransformer call. + + ``context`` is the OVOS-MSG-1 ``message.context`` dict the + transformer service hands to ``transform()``. Two sources may carry + a language: + + 1. ``context["lang"]`` — the top-level convenience key that + ovos-core's IntentService writes before invoking transformers + (``ovos_core/intent_services/service.py::_handle_transformers``). + This is the **current contract** between core and transformer + plugins; trust it when present. + 2. ``context["session"]["lang"]`` — the normative session-carrier + field per OVOS-MSG-1 §4. Falls back here when a caller hands the + transformer a Message without going through ovos-core's + pre-processing (HiveMind relays, tests, alternative bus + clients). + + The convenience top-level key is a known gap in the spec — the + transformer signature will likely grow an explicit ``lang`` kwarg + in a future revision, at which point this helper collapses to a + one-liner. Until then, the dual lookup keeps the plugin robust + against direct callers. + + ``dict.get(key, default)`` returns ``None`` for keys present with a + ``None`` value (only a *missing* key triggers the default), so the + ``or`` chain handles explicit ``None`` correctly. + """ + context = context or {} + session = context.get("session") or {} + lang = (context.get("lang") + or session.get("lang") + or default) + return standardize_lang(lang) class NevermindPlugin(UtteranceTransformer): """Utterance transformer that drops utterances ending with a cancel phrase. - Cancel phrases are loaded from ``locale//cancel.intent`` and + Cancel phrases are loaded from ``locale//cancel.voc`` and matched against the tail of each utterance. On a match the utterance list is cleared and ``{"canceled": True, "cancel_word": }`` is added to the context dict so downstream components can react. @@ -45,13 +79,20 @@ class NevermindPlugin(UtteranceTransformer): def __init__(self, name: str = "ovos-utterance-cancel", priority: int = 15) -> None: super().__init__(name, priority) + # OVOS-INTENT-2 resource loader. One instance serves every language + # the plugin ships; the language is a parameter of each load call. + # The default lang_resolver is `closest_lang` (OVOS-INTENT-2 §2.2 + # smart fallback), gated on distance < 10. + self._resources = LocaleResources( + skill_locale=join(dirname(__file__), "locale")) @lru_cache() def get_cancel_words(self, lang: str = "en-US") -> List[str]: """Return the list of cancel phrases for *lang*. - Phrases are read from ``locale//cancel.intent``, expanded - via bracket-expansion, and deduplicated. The result is LRU-cached per + Phrases are loaded via :class:`ovos_spec_tools.LocaleResources`, + which applies the OVOS-INTENT-2 §2.2 smart language fallback and + the §3.6 sentence-template expansion. The result is LRU-cached per language tag for the lifetime of the process. Args: @@ -59,25 +100,14 @@ def get_cancel_words(self, lang: str = "en-US") -> List[str]: Returns: List of cancel phrases, or an empty list when no locale is close - enough (langcodes distance ≥ 10). + enough (distance ≥ 10). """ - locale_dir = join(dirname(__file__), "locale") - langs = [l for l in os.listdir(locale_dir) - if isfile(join(locale_dir, l, "cancel.intent"))] - best_lang, score = closest_match(lang, langs) - # langcodes distance: 0 = same, 1-3 = minor regional, 4-10 = significant regional - if score < 10: - res_path = join(locale_dir, best_lang, "cancel.intent") - lines: List[str] = [] - with open(res_path) as f: - for line in f: - line = line.strip() - if not line or line.startswith("#"): - continue - lines.extend(expand_template(line)) - return list({l.strip() for l in lines if l.strip()}) - LOG.warning(f"cancel.intent not available for {lang}") - return [] + try: + phrases = self._resources.load_vocabulary("cancel", lang) + except FileNotFoundError: + LOG.warning(f"cancel.voc not available for {lang}") + return [] + return list({phrase.strip() for phrase in phrases if phrase.strip()}) def transform( self, @@ -97,8 +127,7 @@ def transform( ``{"canceled": True, "cancel_word": }``. Otherwise the original utterances are returned unchanged with an empty dict. """ - context = context or {} - lang = standardize_lang_tag(context.get("lang", "en-US")) + lang = _resolve_lang(context) for nevermind in self.get_cancel_words(lang): for utterance in utterances: if utterance.endswith(nevermind): diff --git a/ovos_utterance_plugin_cancel/locale/ca-ES/cancel.intent b/ovos_utterance_plugin_cancel/locale/ca-ES/cancel.intent index 8fce571..af5dff0 100644 --- a/ovos_utterance_plugin_cancel/locale/ca-ES/cancel.intent +++ b/ovos_utterance_plugin_cancel/locale/ca-ES/cancel.intent @@ -1,15 +1,8 @@ -cancel·la això -cancel·la aquesta ordre -cancel·la esta ordre -cancel·la l'ordre -cancel·la s'ordre -cancel·la-ho -no importa això -no importa aquesta ordre -no importa esta ordre -oblida això -oblida aquesta ordre -oblida esta ordre -oblida l'ordre -oblida s'ordre -oblida-ho \ No newline at end of file +# Documents the matching pattern: an utterance ending with a cancel +# phrase from cancel.voc, optionally with no leading utterance at all +# (a standalone cancel). +# +# The plugin currently does suffix-matching with the cancel.voc +# vocabulary; this .intent is informational, not consumed at runtime, +# and exists so the resource role is unambiguous in the locale folder. +[{utterance}] diff --git a/ovos_utterance_plugin_cancel/locale/ca-ES/cancel.voc b/ovos_utterance_plugin_cancel/locale/ca-ES/cancel.voc new file mode 100644 index 0000000..8fce571 --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/ca-ES/cancel.voc @@ -0,0 +1,15 @@ +cancel·la això +cancel·la aquesta ordre +cancel·la esta ordre +cancel·la l'ordre +cancel·la s'ordre +cancel·la-ho +no importa això +no importa aquesta ordre +no importa esta ordre +oblida això +oblida aquesta ordre +oblida esta ordre +oblida l'ordre +oblida s'ordre +oblida-ho \ No newline at end of file diff --git a/ovos_utterance_plugin_cancel/locale/da-DK/cancel.intent b/ovos_utterance_plugin_cancel/locale/da-DK/cancel.intent index 6849635..af5dff0 100644 --- a/ovos_utterance_plugin_cancel/locale/da-DK/cancel.intent +++ b/ovos_utterance_plugin_cancel/locale/da-DK/cancel.intent @@ -1,14 +1,8 @@ -afbryd det -annulle kommando -annuller den kommando -annuller den ordre -annuller det -annuller ordre -glem den kommando -glem den ordre -glem det -glem kommando -glem ordre -ligeglad med den kommando -pyt med den rækkefølge -pyt med det \ No newline at end of file +# Documents the matching pattern: an utterance ending with a cancel +# phrase from cancel.voc, optionally with no leading utterance at all +# (a standalone cancel). +# +# The plugin currently does suffix-matching with the cancel.voc +# vocabulary; this .intent is informational, not consumed at runtime, +# and exists so the resource role is unambiguous in the locale folder. +[{utterance}] diff --git a/ovos_utterance_plugin_cancel/locale/da-DK/cancel.voc b/ovos_utterance_plugin_cancel/locale/da-DK/cancel.voc new file mode 100644 index 0000000..fe78a24 --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/da-DK/cancel.voc @@ -0,0 +1,14 @@ +afbryd det +annuller kommando +annuller den kommando +annuller den ordre +annuller det +annuller ordre +glem den kommando +glem den ordre +glem det +glem kommando +glem ordre +ligeglad med den kommando +pyt med den rækkefølge +pyt med det \ No newline at end of file diff --git a/ovos_utterance_plugin_cancel/locale/de-DE/cancel.intent b/ovos_utterance_plugin_cancel/locale/de-DE/cancel.intent index 43c8ec6..af5dff0 100644 --- a/ovos_utterance_plugin_cancel/locale/de-DE/cancel.intent +++ b/ovos_utterance_plugin_cancel/locale/de-DE/cancel.intent @@ -1,11 +1,8 @@ -(befehl|kommando) (vergessen|zurücknehmen|aufheben|abbrechen|zurück|löschen) -(breche|brech|brich) ((den|diesen|) befehl|(dieses|das|) kommando) ab -(hebe|heb) ((den|diesen|) befehl|(dieses|das|) kommando) auf -(nehme|nehm|nimm) (den|diesen|) (befehl|kommando) zurück -(vergess|vergiss|vergesse|lösche) (es|(den|diesen) befehl|(dieses|das|) kommando) -Befehl Abbrechen -Vergiss es -vergiss Befehl -vergiss Kommando -vergiss das -vergiss diesen Befehl \ No newline at end of file +# Documents the matching pattern: an utterance ending with a cancel +# phrase from cancel.voc, optionally with no leading utterance at all +# (a standalone cancel). +# +# The plugin currently does suffix-matching with the cancel.voc +# vocabulary; this .intent is informational, not consumed at runtime, +# and exists so the resource role is unambiguous in the locale folder. +[{utterance}] diff --git a/ovos_utterance_plugin_cancel/locale/de-DE/cancel.voc b/ovos_utterance_plugin_cancel/locale/de-DE/cancel.voc new file mode 100644 index 0000000..43c8ec6 --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/de-DE/cancel.voc @@ -0,0 +1,11 @@ +(befehl|kommando) (vergessen|zurücknehmen|aufheben|abbrechen|zurück|löschen) +(breche|brech|brich) ((den|diesen|) befehl|(dieses|das|) kommando) ab +(hebe|heb) ((den|diesen|) befehl|(dieses|das|) kommando) auf +(nehme|nehm|nimm) (den|diesen|) (befehl|kommando) zurück +(vergess|vergiss|vergesse|lösche) (es|(den|diesen) befehl|(dieses|das|) kommando) +Befehl Abbrechen +Vergiss es +vergiss Befehl +vergiss Kommando +vergiss das +vergiss diesen Befehl \ No newline at end of file diff --git a/ovos_utterance_plugin_cancel/locale/en-US/cancel.intent b/ovos_utterance_plugin_cancel/locale/en-US/cancel.intent index dde9ce7..af5dff0 100644 --- a/ovos_utterance_plugin_cancel/locale/en-US/cancel.intent +++ b/ovos_utterance_plugin_cancel/locale/en-US/cancel.intent @@ -1,15 +1,8 @@ -cancel command -cancel it -cancel order -cancel that -cancel that command -cancel that order -forget command -forget it -forget order -forget that -forget that command -forget that order -nevermind that -nevermind that command -nevermind that order \ No newline at end of file +# Documents the matching pattern: an utterance ending with a cancel +# phrase from cancel.voc, optionally with no leading utterance at all +# (a standalone cancel). +# +# The plugin currently does suffix-matching with the cancel.voc +# vocabulary; this .intent is informational, not consumed at runtime, +# and exists so the resource role is unambiguous in the locale folder. +[{utterance}] diff --git a/ovos_utterance_plugin_cancel/locale/en-US/cancel.voc b/ovos_utterance_plugin_cancel/locale/en-US/cancel.voc new file mode 100644 index 0000000..dde9ce7 --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/en-US/cancel.voc @@ -0,0 +1,15 @@ +cancel command +cancel it +cancel order +cancel that +cancel that command +cancel that order +forget command +forget it +forget order +forget that +forget that command +forget that order +nevermind that +nevermind that command +nevermind that order \ No newline at end of file diff --git a/ovos_utterance_plugin_cancel/locale/es-ES/cancel.intent b/ovos_utterance_plugin_cancel/locale/es-ES/cancel.intent index 4adf344..af5dff0 100644 --- a/ovos_utterance_plugin_cancel/locale/es-ES/cancel.intent +++ b/ovos_utterance_plugin_cancel/locale/es-ES/cancel.intent @@ -1,15 +1,8 @@ -cancela comando -cancela eso -cancelar esa orden -cancelar ese comando -cancelar orden -cancélalo -no importa esa orden -no importa ese comando -no importa eso -olvida esa orden -olvida ese comando -olvida eso -olvidar comando -olvidar orden -olvídalo \ No newline at end of file +# Documents the matching pattern: an utterance ending with a cancel +# phrase from cancel.voc, optionally with no leading utterance at all +# (a standalone cancel). +# +# The plugin currently does suffix-matching with the cancel.voc +# vocabulary; this .intent is informational, not consumed at runtime, +# and exists so the resource role is unambiguous in the locale folder. +[{utterance}] diff --git a/ovos_utterance_plugin_cancel/locale/es-ES/cancel.voc b/ovos_utterance_plugin_cancel/locale/es-ES/cancel.voc new file mode 100644 index 0000000..4adf344 --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/es-ES/cancel.voc @@ -0,0 +1,15 @@ +cancela comando +cancela eso +cancelar esa orden +cancelar ese comando +cancelar orden +cancélalo +no importa esa orden +no importa ese comando +no importa eso +olvida esa orden +olvida ese comando +olvida eso +olvidar comando +olvidar orden +olvídalo \ No newline at end of file diff --git a/ovos_utterance_plugin_cancel/locale/fr-FR/cancel.intent b/ovos_utterance_plugin_cancel/locale/fr-FR/cancel.intent index 22b8bcd..af5dff0 100644 --- a/ovos_utterance_plugin_cancel/locale/fr-FR/cancel.intent +++ b/ovos_utterance_plugin_cancel/locale/fr-FR/cancel.intent @@ -1,12 +1,8 @@ -annuler cette commande -annuler la commande -annuler que -l'annuler -l'oublier -ne pas tenir compte de cette commande -oublier ce commandement -oublier cet ordre -oublier l'ordre -oublier la commande -oublier que -sans tenir compte de cela \ No newline at end of file +# Documents the matching pattern: an utterance ending with a cancel +# phrase from cancel.voc, optionally with no leading utterance at all +# (a standalone cancel). +# +# The plugin currently does suffix-matching with the cancel.voc +# vocabulary; this .intent is informational, not consumed at runtime, +# and exists so the resource role is unambiguous in the locale folder. +[{utterance}] diff --git a/ovos_utterance_plugin_cancel/locale/fr-FR/cancel.voc b/ovos_utterance_plugin_cancel/locale/fr-FR/cancel.voc new file mode 100644 index 0000000..22b8bcd --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/fr-FR/cancel.voc @@ -0,0 +1,12 @@ +annuler cette commande +annuler la commande +annuler que +l'annuler +l'oublier +ne pas tenir compte de cette commande +oublier ce commandement +oublier cet ordre +oublier l'ordre +oublier la commande +oublier que +sans tenir compte de cela \ No newline at end of file diff --git a/ovos_utterance_plugin_cancel/locale/gl-ES/cancel.intent b/ovos_utterance_plugin_cancel/locale/gl-ES/cancel.intent index 71d8be9..af5dff0 100644 --- a/ovos_utterance_plugin_cancel/locale/gl-ES/cancel.intent +++ b/ovos_utterance_plugin_cancel/locale/gl-ES/cancel.intent @@ -1,15 +1,8 @@ -(cancela|cancelar) comando -(cancela|cancelar) ese comando -(cancela|cancelar) iso -(cancela|cancelar) orde -(cancélao|cancelalo) -(deixa estar|nada, esquece) -(esquece iso|esquecer iso) -(esquecer a orde|esquece a orde) -(esquecer esa orde|esquece esa orde) -(esquecer ese comando|esquece ese comando) -(esquecer o comando|esquece o comando) -(esquéceo|esquecelo) -(ignora esa orde|non fagas caso desa orde) -(ignora ese comando|non fagas caso dese comando) -cancelar esa orde \ No newline at end of file +# Documents the matching pattern: an utterance ending with a cancel +# phrase from cancel.voc, optionally with no leading utterance at all +# (a standalone cancel). +# +# The plugin currently does suffix-matching with the cancel.voc +# vocabulary; this .intent is informational, not consumed at runtime, +# and exists so the resource role is unambiguous in the locale folder. +[{utterance}] diff --git a/ovos_utterance_plugin_cancel/locale/gl-ES/cancel.voc b/ovos_utterance_plugin_cancel/locale/gl-ES/cancel.voc new file mode 100644 index 0000000..71d8be9 --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/gl-ES/cancel.voc @@ -0,0 +1,15 @@ +(cancela|cancelar) comando +(cancela|cancelar) ese comando +(cancela|cancelar) iso +(cancela|cancelar) orde +(cancélao|cancelalo) +(deixa estar|nada, esquece) +(esquece iso|esquecer iso) +(esquecer a orde|esquece a orde) +(esquecer esa orde|esquece esa orde) +(esquecer ese comando|esquece ese comando) +(esquecer o comando|esquece o comando) +(esquéceo|esquecelo) +(ignora esa orde|non fagas caso desa orde) +(ignora ese comando|non fagas caso dese comando) +cancelar esa orde \ No newline at end of file diff --git a/ovos_utterance_plugin_cancel/locale/it-IT/cancel.intent b/ovos_utterance_plugin_cancel/locale/it-IT/cancel.intent index 4506111..af5dff0 100644 --- a/ovos_utterance_plugin_cancel/locale/it-IT/cancel.intent +++ b/ovos_utterance_plugin_cancel/locale/it-IT/cancel.intent @@ -1,15 +1,8 @@ -(dimentica|lascia perdere) il comando -(dimentica|lascia perdere) l'ordine -(dimentica|lascia perdere) quel comando -(dimentica|lascia perdere) quell'ordine -(lascia perdere|lascialo perdere|lascia perdere quello|lascia perdere l'ultima cosa) -(|dimentica|dimenticalo|dimentica quello|dimentica l'ultima cosa) -annulla il comando -annulla l'ordine -annulla quel comando -annulla quell'ordine -annulla(lo|) (quello| l'ultima cosa|) -dimentica(lo|) (quello| l'ultima cosa|) -non importa (di quello| dell'ultima cosa|) -non importa di quel comando -non importa di quell'ordine \ No newline at end of file +# Documents the matching pattern: an utterance ending with a cancel +# phrase from cancel.voc, optionally with no leading utterance at all +# (a standalone cancel). +# +# The plugin currently does suffix-matching with the cancel.voc +# vocabulary; this .intent is informational, not consumed at runtime, +# and exists so the resource role is unambiguous in the locale folder. +[{utterance}] diff --git a/ovos_utterance_plugin_cancel/locale/it-IT/cancel.voc b/ovos_utterance_plugin_cancel/locale/it-IT/cancel.voc new file mode 100644 index 0000000..b57ec52 --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/it-IT/cancel.voc @@ -0,0 +1,15 @@ +(dimentica|lascia perdere) il comando +(dimentica|lascia perdere) l'ordine +(dimentica|lascia perdere) quel comando +(dimentica|lascia perdere) quell'ordine +(lascia perdere|lascialo perdere|lascia perdere quello|lascia perdere l'ultima cosa) +(dimentica|dimenticalo|dimentica quello|dimentica l'ultima cosa) +annulla il comando +annulla l'ordine +annulla quel comando +annulla quell'ordine +annulla(lo|) (quello| l'ultima cosa|) +dimentica(lo|) (quello| l'ultima cosa|) +non importa (di quello| dell'ultima cosa|) +non importa di quel comando +non importa di quell'ordine \ No newline at end of file diff --git a/ovos_utterance_plugin_cancel/locale/nl-NL/cancel.intent b/ovos_utterance_plugin_cancel/locale/nl-NL/cancel.intent index 496976c..af5dff0 100644 --- a/ovos_utterance_plugin_cancel/locale/nl-NL/cancel.intent +++ b/ovos_utterance_plugin_cancel/locale/nl-NL/cancel.intent @@ -1,15 +1,8 @@ -annuleren -annuleren dat -bestelling annuleren -bestelling vergeten -commando annuleren -commando vergeten -die bestelling annuleren -die opdracht annuleren -laat dat commando maar zitten -laat die volgorde maar zitten -laat maar zitten -vergeet dat commando -vergeet die bestelling -vergeet het -vergeten dat \ No newline at end of file +# Documents the matching pattern: an utterance ending with a cancel +# phrase from cancel.voc, optionally with no leading utterance at all +# (a standalone cancel). +# +# The plugin currently does suffix-matching with the cancel.voc +# vocabulary; this .intent is informational, not consumed at runtime, +# and exists so the resource role is unambiguous in the locale folder. +[{utterance}] diff --git a/ovos_utterance_plugin_cancel/locale/nl-NL/cancel.voc b/ovos_utterance_plugin_cancel/locale/nl-NL/cancel.voc new file mode 100644 index 0000000..496976c --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/nl-NL/cancel.voc @@ -0,0 +1,15 @@ +annuleren +annuleren dat +bestelling annuleren +bestelling vergeten +commando annuleren +commando vergeten +die bestelling annuleren +die opdracht annuleren +laat dat commando maar zitten +laat die volgorde maar zitten +laat maar zitten +vergeet dat commando +vergeet die bestelling +vergeet het +vergeten dat \ No newline at end of file diff --git a/ovos_utterance_plugin_cancel/locale/pt-BR/cancel.intent b/ovos_utterance_plugin_cancel/locale/pt-BR/cancel.intent index 2f94fa6..af5dff0 100644 --- a/ovos_utterance_plugin_cancel/locale/pt-BR/cancel.intent +++ b/ovos_utterance_plugin_cancel/locale/pt-BR/cancel.intent @@ -1,12 +1,8 @@ -cancele -cancele a ordem -cancele esse comando -cancele isso -cancele o comando -deixa pra lá -esqueça -esqueça a ordem -esqueça essa ordem -esqueça esse comando -esqueça isso -esqueça o comando \ No newline at end of file +# Documents the matching pattern: an utterance ending with a cancel +# phrase from cancel.voc, optionally with no leading utterance at all +# (a standalone cancel). +# +# The plugin currently does suffix-matching with the cancel.voc +# vocabulary; this .intent is informational, not consumed at runtime, +# and exists so the resource role is unambiguous in the locale folder. +[{utterance}] diff --git a/ovos_utterance_plugin_cancel/locale/pt-BR/cancel.voc b/ovos_utterance_plugin_cancel/locale/pt-BR/cancel.voc new file mode 100644 index 0000000..2f94fa6 --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/pt-BR/cancel.voc @@ -0,0 +1,12 @@ +cancele +cancele a ordem +cancele esse comando +cancele isso +cancele o comando +deixa pra lá +esqueça +esqueça a ordem +esqueça essa ordem +esqueça esse comando +esqueça isso +esqueça o comando \ No newline at end of file diff --git a/ovos_utterance_plugin_cancel/locale/pt-PT/cancel.intent b/ovos_utterance_plugin_cancel/locale/pt-PT/cancel.intent index ac69458..af5dff0 100644 --- a/ovos_utterance_plugin_cancel/locale/pt-PT/cancel.intent +++ b/ovos_utterance_plugin_cancel/locale/pt-PT/cancel.intent @@ -1,9 +1,8 @@ -(cancela|cancelar|esquecer|anular) (ordem|comado|isso) -(cancela|cancelar|esquece|esquecer|anular) (a|o) (ultima|ultimo) (ordem|comado) -(cancela|cancelar|esquece|esquecer|anular) (ultima|ultimo|esse|este|esta) (ordem|comado) -(esquece|cancela|cancelar) -(ignora|ignorar) (o|a) (ultima|ultimo) (ordem|comado) -(ignora|ignorar) (ultima|ultimo|esse|este|esta) (ordem|comado) -não (importa|executar) -não (importa|executar|executes) (a|o) (ultima|ultimo) (ordem|comado) -não (importa|executar|executes) (ultima|ultimo|esse|este|esta) (ordem|comado) \ No newline at end of file +# Documents the matching pattern: an utterance ending with a cancel +# phrase from cancel.voc, optionally with no leading utterance at all +# (a standalone cancel). +# +# The plugin currently does suffix-matching with the cancel.voc +# vocabulary; this .intent is informational, not consumed at runtime, +# and exists so the resource role is unambiguous in the locale folder. +[{utterance}] diff --git a/ovos_utterance_plugin_cancel/locale/pt-PT/cancel.voc b/ovos_utterance_plugin_cancel/locale/pt-PT/cancel.voc new file mode 100644 index 0000000..34a180d --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/pt-PT/cancel.voc @@ -0,0 +1,9 @@ +(cancela|cancelar|esquecer|anular) (ordem|comando|isso) +(cancela|cancelar|esquece|esquecer|anular) (a|o) (ultima|ultimo) (ordem|comando) +(cancela|cancelar|esquece|esquecer|anular) (ultima|ultimo|esse|este|esta) (ordem|comando) +(esquece|cancela|cancelar) +(ignora|ignorar) (o|a) (ultima|ultimo) (ordem|comando) +(ignora|ignorar) (ultima|ultimo|esse|este|esta) (ordem|comando) +não (importa|executar) +não (importa|executar|executes) (a|o) (ultima|ultimo) (ordem|comando) +não (importa|executar|executes) (ultima|ultimo|esse|este|esta) (ordem|comando) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index fc524ed..45acd72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,9 +23,9 @@ classifiers = [ ] requires-python = ">=3.10" dependencies = [ - "ovos-utils>=0.6.0,<1.0.0", + "ovos-utils>=0.11.1a1,<1.0.0", "ovos-plugin-manager>=0.0.1", - "langcodes>=3.0.0", + "ovos-spec-tools[langcodes]>=0.5.1a1,<1.0.0", ] [project.optional-dependencies] @@ -33,11 +33,15 @@ dev = [ "pytest", "pytest-cov", ] +test = [ + "ovoscope", + "ovos-skill-hello-world", +] [project.urls] Homepage = "https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel" -[project.entry-points."ovos.utterance.transformer"] +[project.entry-points."opm.transformer.text"] ovos-utterance-cancel-plugin = "ovos_utterance_plugin_cancel:NevermindPlugin" [tool.setuptools] diff --git a/test/end2end/__init__.py b/test/end2end/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/end2end/test_cancel_plugin.py b/test/end2end/test_cancel_plugin.py new file mode 100644 index 0000000..21405bd --- /dev/null +++ b/test/end2end/test_cancel_plugin.py @@ -0,0 +1,189 @@ +"""End-to-end ovoscope tests for ``ovos-utterance-plugin-cancel``. + +Three scenarios: + +* the cancel transformer fires on a suffix-matched utterance + (positive path); +* the cancel transformer is **not** invoked when the plugin is + disabled in config — the utterance falls through to intent failure + (negative gate: proves the positive test is observing the plugin + itself, not some unrelated component); +* a plain utterance with no cancel suffix passes through to the skill + unchanged (smoke). +""" +import json +import os +import tempfile +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import Session +from ovos_config.config import Configuration +from ovos_config.models import LocalConf +from ovos_utils.log import LOG +from ovoscope import End2EndTest, get_minicroft + + +# The entry-point name under which this plugin is registered. The +# OVOS UtteranceTransformersService loads a plugin only when this name +# appears (with ``active: True``) in ``Configuration().utterance_transformers`` +# — ``ovos_core/transformers.py:29``. +PLUGIN_NAME = "ovos-utterance-cancel-plugin" + + +class _CancelPluginTestBase(TestCase): + """Common boot / teardown. + + MiniCroft's ``isolate_config=True`` clears the user's XDG configs + **and** calls ``Configuration.reload()``, which would wipe any + in-memory override done in ``setUp``. Each subclass writes its + desired config to a temp file and prepends it to + ``Configuration.xdg_configs`` before booting MiniCroft with + ``isolate_config=False`` — the highest-precedence source wins on + reload.""" + + skill_id = "ovos-skill-hello-world.openvoiceos" + plugin_active: bool = True + + def setUp(self): + LOG.set_level("DEBUG") + self._tmp_conf = self._write_config( + {"utterance_transformers": { + PLUGIN_NAME: {"active": self.plugin_active}}}) + self._orig_xdg = Configuration.xdg_configs[:] + Configuration.xdg_configs = ( + [LocalConf(self._tmp_conf)] + Configuration.xdg_configs) + Configuration.reload() + + self.minicroft = get_minicroft( + [self.skill_id], isolate_config=False) + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + Configuration.xdg_configs = self._orig_xdg + Configuration.reload() + os.unlink(self._tmp_conf) + LOG.set_level("CRITICAL") + + @staticmethod + def _write_config(data: dict) -> str: + fd, path = tempfile.mkstemp(prefix="cancel-plugin-test-", + suffix=".json") + with os.fdopen(fd, "w") as fh: + json.dump(data, fh) + return path + + @staticmethod + def _utterance(text: str, session: Session) -> Message: + return Message( + "recognizer_loop:utterance", + {"utterances": [text], "lang": session.lang}, + {"session": session.serialize(), + "source": "A", "destination": "B"}) + + +class TestCancelPluginEnabled(_CancelPluginTestBase): + """The OVOS default config ships this plugin enabled; assert that + explicitly here as well to keep the test future-proof against + upstream config changes.""" + + plugin_active = True + + # --- positive: cancel fires -------------------------------------------- + + def test_cancel_mid_sentence(self): + """An utterance ending with a cancel phrase is dropped before + intent matching — the cancel sequence is emitted instead.""" + session = Session("123") + session.lang = "en-US" + message = self._utterance( + "can you tell me the...ummm...oh, nevermind that", session) + + End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + final_session=session, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/cancel.mp3"}), + Message("ovos.utterance.cancelled", {}), + Message("ovos.utterance.handled", {}), + ], + ).execute(timeout=10) + + def test_cancel_suffix_on_arbitrary_utterance(self): + """The suffix fires regardless of whether the leading utterance + would have matched a skill.""" + session = Session("123") + session.lang = "en-US" + message = self._utterance("hello world cancel command", session) + + End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/cancel.mp3"}), + Message("ovos.utterance.cancelled", {}), + Message("ovos.utterance.handled", {}), + ], + ).execute(timeout=10) + + # --- smoke: passthrough -------------------------------------------------- + + def test_passthrough_without_cancel_word(self): + """An utterance with no cancel suffix is not intercepted. + + ``hello world`` doesn't match any registered intent in this + minimal test rig, so the expected outcome is intent-failure + (``snd/error.mp3``) — the point being that the cancel + sequence (``ovos.utterance.cancelled``) is **absent**, which + the strict ovoscope message-type + count checks enforce.""" + session = Session("123") + session.lang = "en-US" + message = self._utterance("hello world", session) + + End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}), + ], + ).execute(timeout=10) + + +class TestCancelPluginDisabled(_CancelPluginTestBase): + """Negative gate. With the plugin explicitly disabled, the same + cancel-suffix utterance is **not** intercepted: it falls through + to intent matching and lands on the intent-failure error sound. + This proves the positive test above is observing the plugin + itself, not some unrelated component.""" + + plugin_active = False + + def test_cancel_suffix_falls_through_when_disabled(self): + session = Session("123") + session.lang = "en-US" + message = self._utterance( + "can you tell me the...ummm...oh, nevermind that", session) + + End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + # No cancel sequence — intent failure plays the error + # sound and emits handled. + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}), + ], + ).execute(timeout=10) From 7ed2e8563dc7382acbeb0be0adb1577763e7e23d Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 25 May 2026 14:33:44 +0000 Subject: [PATCH 03/10] Increment Version to 0.2.9a1 --- ovos_utterance_plugin_cancel/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_utterance_plugin_cancel/version.py b/ovos_utterance_plugin_cancel/version.py index ba00796..9650c07 100644 --- a/ovos_utterance_plugin_cancel/version.py +++ b/ovos_utterance_plugin_cancel/version.py @@ -1,8 +1,8 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 VERSION_MINOR = 2 -VERSION_BUILD = 8 -VERSION_ALPHA = 0 +VERSION_BUILD = 9 +VERSION_ALPHA = 1 # END_VERSION_BLOCK __version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}" + (f"a{VERSION_ALPHA}" if VERSION_ALPHA else "") From 433b31e48cc127865463e2bc75687d2962fc9a4d Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 25 May 2026 14:34:03 +0000 Subject: [PATCH 04/10] Update Changelog --- CHANGELOG.md | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80a7e5c..98f459b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,40 +1,12 @@ # Changelog -## [0.2.8a1](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/tree/0.2.8a1) (2026-04-17) +## [0.2.9a1](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/tree/0.2.9a1) (2026-05-25) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/compare/0.2.7a2...0.2.8a1) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/compare/0.2.8...0.2.9a1) **Merged pull requests:** -- fix: drop setup.py and requirements.txt [\#30](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/pull/30) ([JarbasAl](https://github.com/JarbasAl)) - -## [0.2.7a2](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/tree/0.2.7a2) (2026-04-17) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/compare/0.2.7a1...0.2.7a2) - -**Merged pull requests:** - -- ci: modernize workflows, add pyproject.toml, type hints and tests [\#28](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/pull/28) ([JarbasAl](https://github.com/JarbasAl)) - -## [0.2.7a1](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/tree/0.2.7a1) (2026-04-03) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/compare/0.2.6a1...0.2.7a1) - -**Merged pull requests:** - -- fix\(i18n\): normalize locale folders to canonical BCP-47 [\#26](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/pull/26) ([JarbasAl](https://github.com/JarbasAl)) - -## [0.2.6a1](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/tree/0.2.6a1) (2025-11-24) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/compare/V0.2.5...0.2.6a1) - -**Merged pull requests:** - -- es-es/translate [\#24](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/pull/24) ([gitlocalize-app[bot]](https://github.com/apps/gitlocalize-app)) - -## [V0.2.5](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/tree/V0.2.5) (2025-07-31) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/compare/0.2.5...V0.2.5) +- fix: cancel transformer not firing — entry-point group, lang lookup, locale typos [\#32](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/pull/32) ([JarbasAl](https://github.com/JarbasAl)) From 7c82c69e373f4fa3f3f337efa9036d4e6bad2e9a Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 25 May 2026 16:19:23 +0100 Subject: [PATCH 05/10] feat: cancel.blacklist veto for utterances *about* a cancel word (partial #7) (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: cancel.blacklist veto for utterances *about* a cancel word (partial #7) Issue #7 calls out the false-positive class where the cancel transformer fires on utterances that mention a cancel word without intending to cancel anything — `play nevermind`, `spell forget that`, `what is the opposite of cancel`, etc. The proper fix is post-intent-dispatch (only cancel when no play/say/spell/common_qa intent matched), which needs coordination with ovos-core; until then this gates by **utterance prefix**. Adds an optional `locale//cancel.blacklist` companion to `cancel.voc`. `.blacklist` is the OVOS-INTENT-2 §4.3 phrase-set role an engine uses to *exclude* matches — here, any utterance starting with a phrase in `cancel.blacklist` is exempt from the cancel suffix match. A missing `cancel.blacklist` for a language is non-fatal: the plugin falls back to the historic always-check-suffix behaviour. Ships an en-US `cancel.blacklist` covering the four pattern classes from issue #7: - imperative verbs that operate on the literal word: play, say, spell, pronounce, read, repeat, write, type, define, explain, translate - pedagogical patterns: `tell me the ...`, `teach me ...` - question starts: `how do you ...`, `what is ...`, `what's the (meaning|opposite|...)`, `why don't you ...` - desire patterns: `i want(ed) (that|the|to) ...` ovoscope coverage: a `spell nevermind that` utterance — which would otherwise fire the cancel sequence — falls through to intent failure under the new veto. Co-Authored-By: Claude Opus 4.7 (1M context) * Update cancel.blacklist * feat(locale): translate cancel.blacklist into all 10 supported locales Ports the en-US blacklist patterns into ca-ES, da-DK, de-DE, es-ES, fr-FR, gl-ES, it-IT, nl-NL, pt-BR, pt-PT. Same four pattern classes in each: - imperative verbs that operate on the literal word (play/say/spell/pronounce/read/repeat/write/type) - pedagogical verbs (define/explain/translate, tell me, teach me) - question starts (how do you / what is / what's the meaning|opposite|translation|pronunciation / why) - meta-linguistic question forms Each file parses cleanly through ovos_spec_tools.LocaleResources .load_blacklist and expands to 29-51 prefixes per language. Same header comment in every file so translators can audit and extend without re-deriving the contract. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- ovos_utterance_plugin_cancel/__init__.py | 42 ++++++++++++------- .../locale/ca-ES/cancel.blacklist | 18 ++++++++ .../locale/da-DK/cancel.blacklist | 17 ++++++++ .../locale/de-DE/cancel.blacklist | 17 ++++++++ .../locale/en-US/cancel.blacklist | 18 ++++++++ .../locale/es-ES/cancel.blacklist | 17 ++++++++ .../locale/fr-FR/cancel.blacklist | 18 ++++++++ .../locale/gl-ES/cancel.blacklist | 17 ++++++++ .../locale/it-IT/cancel.blacklist | 17 ++++++++ .../locale/nl-NL/cancel.blacklist | 17 ++++++++ .../locale/pt-BR/cancel.blacklist | 17 ++++++++ .../locale/pt-PT/cancel.blacklist | 17 ++++++++ test/end2end/test_cancel_plugin.py | 32 ++++++++++++++ 13 files changed, 250 insertions(+), 14 deletions(-) create mode 100644 ovos_utterance_plugin_cancel/locale/ca-ES/cancel.blacklist create mode 100644 ovos_utterance_plugin_cancel/locale/da-DK/cancel.blacklist create mode 100644 ovos_utterance_plugin_cancel/locale/de-DE/cancel.blacklist create mode 100644 ovos_utterance_plugin_cancel/locale/en-US/cancel.blacklist create mode 100644 ovos_utterance_plugin_cancel/locale/es-ES/cancel.blacklist create mode 100644 ovos_utterance_plugin_cancel/locale/fr-FR/cancel.blacklist create mode 100644 ovos_utterance_plugin_cancel/locale/gl-ES/cancel.blacklist create mode 100644 ovos_utterance_plugin_cancel/locale/it-IT/cancel.blacklist create mode 100644 ovos_utterance_plugin_cancel/locale/nl-NL/cancel.blacklist create mode 100644 ovos_utterance_plugin_cancel/locale/pt-BR/cancel.blacklist create mode 100644 ovos_utterance_plugin_cancel/locale/pt-PT/cancel.blacklist diff --git a/ovos_utterance_plugin_cancel/__init__.py b/ovos_utterance_plugin_cancel/__init__.py index b263814..6690330 100644 --- a/ovos_utterance_plugin_cancel/__init__.py +++ b/ovos_utterance_plugin_cancel/__init__.py @@ -88,20 +88,7 @@ def __init__(self, name: str = "ovos-utterance-cancel", priority: int = 15) -> N @lru_cache() def get_cancel_words(self, lang: str = "en-US") -> List[str]: - """Return the list of cancel phrases for *lang*. - - Phrases are loaded via :class:`ovos_spec_tools.LocaleResources`, - which applies the OVOS-INTENT-2 §2.2 smart language fallback and - the §3.6 sentence-template expansion. The result is LRU-cached per - language tag for the lifetime of the process. - - Args: - lang: BCP-47 language tag (e.g. ``"en-US"``). - - Returns: - List of cancel phrases, or an empty list when no locale is close - enough (distance ≥ 10). - """ + """Return the cancel phrases for *lang* (``cancel.voc``).""" try: phrases = self._resources.load_vocabulary("cancel", lang) except FileNotFoundError: @@ -109,6 +96,25 @@ def get_cancel_words(self, lang: str = "en-US") -> List[str]: return [] return list({phrase.strip() for phrase in phrases if phrase.strip()}) + @lru_cache() + def get_cancel_blacklist(self, lang: str = "en-US") -> List[str]: + """Return the *veto prefixes* for *lang* (``cancel.blacklist``). + + OVOS-INTENT-2 §4.3 defines ``.blacklist`` as a phrase set that + an engine consults to *exclude* matches. Here, utterances that + start with any phrase in ``cancel.blacklist`` bypass the cancel + suffix match — they are *about* a cancel word (define / spell / + pronounce / play / etc.) rather than commands to cancel. + Partial fix for issue #7. A missing ``cancel.blacklist`` is + non-fatal: the plugin falls back to the historic + "always check the suffix" behaviour. + """ + try: + phrases = self._resources.load_blacklist("cancel", lang) + except FileNotFoundError: + return [] + return list({phrase.strip() for phrase in phrases if phrase.strip()}) + def transform( self, utterances: List[str], @@ -116,6 +122,11 @@ def transform( ) -> Tuple[List[str], Dict[str, object]]: """Drop utterances that end with a cancel phrase. + Skipped when the utterance starts with a phrase listed in + ``cancel.blacklist`` for the active language — partial veto for + the edge cases tracked in issue #7 (e.g. ``"say nevermind"``, + ``"what is the opposite of nevermind"``). + Args: utterances: Recognised utterance candidates. context: Session context dict; ``lang`` key is used for locale @@ -128,8 +139,11 @@ def transform( original utterances are returned unchanged with an empty dict. """ lang = _resolve_lang(context) + blacklist = self.get_cancel_blacklist(lang) for nevermind in self.get_cancel_words(lang): for utterance in utterances: + if any(utterance.startswith(p) for p in blacklist): + continue if utterance.endswith(nevermind): return [], {"canceled": True, "cancel_word": nevermind} return utterances, {} diff --git a/ovos_utterance_plugin_cancel/locale/ca-ES/cancel.blacklist b/ovos_utterance_plugin_cancel/locale/ca-ES/cancel.blacklist new file mode 100644 index 0000000..32a44ce --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/ca-ES/cancel.blacklist @@ -0,0 +1,18 @@ +# OVOS-INTENT-2 §4.3 blacklist for the cancel transformer. +# +# Utterance prefixes that veto the cancel suffix match. +# +# When an utterance starts with one of these phrases, the cancel +# suffix match is skipped — even if the utterance happens to end with +# a word from cancel.voc. The utterance is *about* the cancel word +# (define it, spell it, pronounce it, ask after it) rather than a +# command to cancel. +(reprodueix|digues|lletreja|pronuncia|llegeix|repeteix|escriu) +(defineix|explica|tradueix) +digues-me (el|la|sobre|què|allò) +ensenya'm +com (ho fas|es|és|funciona) +què (és|són|fa|fan|vol dir) +quin és (el significat|el contrari|la traducció) +quina és (l'antònim|la traducció|la pronunciació) +per què (no|fas|fa|és) diff --git a/ovos_utterance_plugin_cancel/locale/da-DK/cancel.blacklist b/ovos_utterance_plugin_cancel/locale/da-DK/cancel.blacklist new file mode 100644 index 0000000..2d63170 --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/da-DK/cancel.blacklist @@ -0,0 +1,17 @@ +# OVOS-INTENT-2 §4.3 blacklist for the cancel transformer. +# +# Utterance prefixes that veto the cancel suffix match. +# +# When an utterance starts with one of these phrases, the cancel +# suffix match is skipped — even if the utterance happens to end with +# a word from cancel.voc. The utterance is *about* the cancel word +# (define it, spell it, pronounce it, ask after it) rather than a +# command to cancel. +(afspil|sig|stav|udtal|læs|gentag|skriv) +(definer|forklar|oversæt) +fortæl mig (om|hvad|hvordan) +lær mig +hvordan (gør du|skal man|er|udtaler du|staves) +hvad (er|betyder|hedder) +hvad er (betydningen|det modsatte|udtalen|oversættelsen) +hvorfor (gør du|gør du ikke|virker) diff --git a/ovos_utterance_plugin_cancel/locale/de-DE/cancel.blacklist b/ovos_utterance_plugin_cancel/locale/de-DE/cancel.blacklist new file mode 100644 index 0000000..dd4e39c --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/de-DE/cancel.blacklist @@ -0,0 +1,17 @@ +# OVOS-INTENT-2 §4.3 blacklist for the cancel transformer. +# +# Utterance prefixes that veto the cancel suffix match. +# +# When an utterance starts with one of these phrases, the cancel +# suffix match is skipped — even if the utterance happens to end with +# a word from cancel.voc. The utterance is *about* the cancel word +# (define it, spell it, pronounce it, ask after it) rather than a +# command to cancel. +(spiele|sage|sag|buchstabiere|sprich|lies|wiederhole|schreib|tippe) +(definiere|erkläre|übersetze) +(sag|erzähl) mir (was|wie|über|das|die|den) +(bring mir bei|lehre mich) +wie (sagt man|spricht man|schreibt man|buchstabiert man|funktioniert) +was (ist|sind|bedeutet|heißt) +was ist (die bedeutung|das gegenteil|die übersetzung|die aussprache) +warum (machst du|tust du nicht|funktioniert) diff --git a/ovos_utterance_plugin_cancel/locale/en-US/cancel.blacklist b/ovos_utterance_plugin_cancel/locale/en-US/cancel.blacklist new file mode 100644 index 0000000..14a0c35 --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/en-US/cancel.blacklist @@ -0,0 +1,18 @@ +# OVOS-INTENT-2 §4.3 blacklist for the cancel transformer. +# +# Utterance prefixes that veto the cancel suffix match. +# +# When an utterance starts with one of these phrases, the cancel +# suffix match is skipped — even if the utterance happens to end with +# a word from cancel.voc. The utterance is *about* the cancel word +# (define it, spell it, pronounce it, ask after it) rather than a +# command to cancel. +(play|say|spell|pronounce|read|repeat|write|type) +(define|explain|translate) +tell me (the|about|what) +teach me +how (do you|to|does|is) +what (is|are|does|do) +what's the (meaning|opposite|phonemes|translation|french|german|spanish|portuguese|italian|catalan) +what is (another|the meaning|the opposite|the phonemes|the french|the german|the spanish|the portuguese|the italian|the catalan|the translation) +why (do you|don't you|does not|doesn't) diff --git a/ovos_utterance_plugin_cancel/locale/es-ES/cancel.blacklist b/ovos_utterance_plugin_cancel/locale/es-ES/cancel.blacklist new file mode 100644 index 0000000..11fb003 --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/es-ES/cancel.blacklist @@ -0,0 +1,17 @@ +# OVOS-INTENT-2 §4.3 blacklist for the cancel transformer. +# +# Utterance prefixes that veto the cancel suffix match. +# +# When an utterance starts with one of these phrases, the cancel +# suffix match is skipped — even if the utterance happens to end with +# a word from cancel.voc. The utterance is *about* the cancel word +# (define it, spell it, pronounce it, ask after it) rather than a +# command to cancel. +(reproduce|di|deletrea|pronuncia|lee|repite|escribe) +(define|explica|traduce) +dime (el|la|sobre|qué|lo) +enséñame +cómo (se|haces|funciona|se dice|se escribe|se pronuncia|se deletrea) +qué (es|son|hace|hacen|significa|quiere decir) +cuál es (el significado|el opuesto|el contrario|la traducción|la pronunciación) +por qué (no|haces|hace|es|funciona) diff --git a/ovos_utterance_plugin_cancel/locale/fr-FR/cancel.blacklist b/ovos_utterance_plugin_cancel/locale/fr-FR/cancel.blacklist new file mode 100644 index 0000000..7d2db57 --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/fr-FR/cancel.blacklist @@ -0,0 +1,18 @@ +# OVOS-INTENT-2 §4.3 blacklist for the cancel transformer. +# +# Utterance prefixes that veto the cancel suffix match. +# +# When an utterance starts with one of these phrases, the cancel +# suffix match is skipped — even if the utterance happens to end with +# a word from cancel.voc. The utterance is *about* the cancel word +# (define it, spell it, pronounce it, ask after it) rather than a +# command to cancel. +(joue|dis|épelle|prononce|lis|répète|écris|tape) +(définis|explique|traduis) +(dis-moi|raconte-moi) (le|la|ce|sur|quoi|comment) +apprends-moi +comment (dit-on|écrit-on|prononce-t-on|épelle-t-on|fais-tu|fonctionne) +(qu'est-ce que|quel est|quelle est) +quel est (le sens|le contraire|l'opposé|la traduction|la prononciation) +quelle est (la signification|la traduction|la prononciation) +pourquoi (tu|ne|ça ne|ça) diff --git a/ovos_utterance_plugin_cancel/locale/gl-ES/cancel.blacklist b/ovos_utterance_plugin_cancel/locale/gl-ES/cancel.blacklist new file mode 100644 index 0000000..551fe0c --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/gl-ES/cancel.blacklist @@ -0,0 +1,17 @@ +# OVOS-INTENT-2 §4.3 blacklist for the cancel transformer. +# +# Utterance prefixes that veto the cancel suffix match. +# +# When an utterance starts with one of these phrases, the cancel +# suffix match is skipped — even if the utterance happens to end with +# a word from cancel.voc. The utterance is *about* the cancel word +# (define it, spell it, pronounce it, ask after it) rather than a +# command to cancel. +(reproduce|di|soletra|pronuncia|le|repite|escribe) +(define|explica|traduce) +dime (o|a|sobre|que|iso) +ensíname +como (se|fas|funciona|se di|se escribe|se pronuncia|se soletra) +que (é|son|fai|fan|significa|quere dicir) +cal é (o significado|o oposto|o contrario|a tradución|a pronuncia) +por que (non|fas|fai|é|funciona) diff --git a/ovos_utterance_plugin_cancel/locale/it-IT/cancel.blacklist b/ovos_utterance_plugin_cancel/locale/it-IT/cancel.blacklist new file mode 100644 index 0000000..7c2f92a --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/it-IT/cancel.blacklist @@ -0,0 +1,17 @@ +# OVOS-INTENT-2 §4.3 blacklist for the cancel transformer. +# +# Utterance prefixes that veto the cancel suffix match. +# +# When an utterance starts with one of these phrases, the cancel +# suffix match is skipped — even if the utterance happens to end with +# a word from cancel.voc. The utterance is *about* the cancel word +# (define it, spell it, pronounce it, ask after it) rather than a +# command to cancel. +(riproduci|suona|di'|compita|pronuncia|leggi|ripeti|scrivi|digita) +(definisci|spiega|traduci) +dimmi (il|la|cosa|che cosa|su|riguardo) +insegnami +come (si|fai|funziona|si dice|si scrive|si pronuncia|si compita) +(cosa|che cosa) (è|sono|fa|fanno|significa|vuol dire) +qual è (il significato|l'opposto|il contrario|la traduzione|la pronuncia) +perché (non|fai|fa|è|funziona) diff --git a/ovos_utterance_plugin_cancel/locale/nl-NL/cancel.blacklist b/ovos_utterance_plugin_cancel/locale/nl-NL/cancel.blacklist new file mode 100644 index 0000000..8dfe53e --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/nl-NL/cancel.blacklist @@ -0,0 +1,17 @@ +# OVOS-INTENT-2 §4.3 blacklist for the cancel transformer. +# +# Utterance prefixes that veto the cancel suffix match. +# +# When an utterance starts with one of these phrases, the cancel +# suffix match is skipped — even if the utterance happens to end with +# a word from cancel.voc. The utterance is *about* the cancel word +# (define it, spell it, pronounce it, ask after it) rather than a +# command to cancel. +(speel|zeg|spel|spreek|lees|herhaal|schrijf|type|typ) +(definieer|leg uit|vertaal) +vertel me (over|wat|hoe|de|het) +leer me +hoe (zeg je|schrijf je|spreek je|spel je|werkt) +wat (is|zijn|betekent|doet) +wat is (de betekenis|het tegenovergestelde|de vertaling|de uitspraak) +waarom (doe je|doe je niet|werkt) diff --git a/ovos_utterance_plugin_cancel/locale/pt-BR/cancel.blacklist b/ovos_utterance_plugin_cancel/locale/pt-BR/cancel.blacklist new file mode 100644 index 0000000..1e1b37d --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/pt-BR/cancel.blacklist @@ -0,0 +1,17 @@ +# OVOS-INTENT-2 §4.3 blacklist for the cancel transformer. +# +# Utterance prefixes that veto the cancel suffix match. +# +# When an utterance starts with one of these phrases, the cancel +# suffix match is skipped — even if the utterance happens to end with +# a word from cancel.voc. The utterance is *about* the cancel word +# (define it, spell it, pronounce it, ask after it) rather than a +# command to cancel. +(toque|reproduza|diga|soletre|pronuncie|leia|repita|escreva|digite) +(defina|explique|traduza) +(me diga|diga-me) (o|a|sobre|o que|qual) +me ensine +como (se diz|se escreve|se pronuncia|se soletra|você faz|funciona) +(o que|qual) (é|são|faz|fazem|significa|quer dizer) +qual é (o significado|o oposto|o contrário|a tradução|a pronúncia) +por que (você|você não|não|é|funciona) diff --git a/ovos_utterance_plugin_cancel/locale/pt-PT/cancel.blacklist b/ovos_utterance_plugin_cancel/locale/pt-PT/cancel.blacklist new file mode 100644 index 0000000..ffcdccd --- /dev/null +++ b/ovos_utterance_plugin_cancel/locale/pt-PT/cancel.blacklist @@ -0,0 +1,17 @@ +# OVOS-INTENT-2 §4.3 blacklist for the cancel transformer. +# +# Utterance prefixes that veto the cancel suffix match. +# +# When an utterance starts with one of these phrases, the cancel +# suffix match is skipped — even if the utterance happens to end with +# a word from cancel.voc. The utterance is *about* the cancel word +# (define it, spell it, pronounce it, ask after it) rather than a +# command to cancel. +(toca|reproduz|diz|soletra|pronuncia|lê|repete|escreve) +(define|explica|traduz) +diz-me (o|a|sobre|o que|qual) +ensina-me +como (se diz|se escreve|se pronuncia|se soletra|fazes|funciona) +(o que|qual) (é|são|faz|fazem|significa|quer dizer) +qual é (o significado|o oposto|o contrário|a tradução|a pronúncia) +porque (não|fazes|faz|é|funciona) diff --git a/test/end2end/test_cancel_plugin.py b/test/end2end/test_cancel_plugin.py index 21405bd..18e2c25 100644 --- a/test/end2end/test_cancel_plugin.py +++ b/test/end2end/test_cancel_plugin.py @@ -132,6 +132,38 @@ def test_cancel_suffix_on_arbitrary_utterance(self): ], ).execute(timeout=10) + # --- skip-prefix veto (issue #7 partial fix) --------------------------- + + def test_blacklist_prefix_vetoes_cancel(self): + """An utterance starting with a phrase from ``cancel.blacklist`` + bypasses the cancel suffix match even when it ends in a cancel + word (OVOS-INTENT-2 §4.3 blacklist role). + + Partial fix for issue #7: utterances *about* the cancel word + (define / spell / pronounce / what-is / how-do-you / ...) are + not commands to cancel.""" + session = Session("123") + session.lang = "en-US" + # ``nevermind that`` IS in en-US/cancel.voc — without the + # blacklist prefix, ``hello world nevermind that`` fires the + # cancel sequence (see test_cancel_suffix_on_arbitrary_utterance). + # ``spell`` as a blacklist prefix vetoes it. + message = self._utterance("spell nevermind that", session) + + End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + # No cancel sequence — intent failure (hello-world + # doesn't register a "spell" intent in this rig). + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}), + ], + ).execute(timeout=10) + # --- smoke: passthrough -------------------------------------------------- def test_passthrough_without_cancel_word(self): From f60d4cdb1c6ad785b342d24958a6d15b82426000 Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 25 May 2026 15:19:35 +0000 Subject: [PATCH 06/10] Increment Version to 0.3.0a1 --- ovos_utterance_plugin_cancel/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_utterance_plugin_cancel/version.py b/ovos_utterance_plugin_cancel/version.py index 9650c07..59ee186 100644 --- a/ovos_utterance_plugin_cancel/version.py +++ b/ovos_utterance_plugin_cancel/version.py @@ -1,7 +1,7 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 -VERSION_MINOR = 2 -VERSION_BUILD = 9 +VERSION_MINOR = 3 +VERSION_BUILD = 0 VERSION_ALPHA = 1 # END_VERSION_BLOCK From 83488ea96654e070b807f819b6c8279aa3a787ce Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 25 May 2026 15:19:55 +0000 Subject: [PATCH 07/10] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98f459b..a939e1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.3.0a1](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/tree/0.3.0a1) (2026-05-25) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/compare/0.2.9a1...0.3.0a1) + +**Merged pull requests:** + +- feat: cancel.blacklist veto for utterances \*about\* a cancel word \(partial \#7\) [\#35](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/pull/35) ([JarbasAl](https://github.com/JarbasAl)) + ## [0.2.9a1](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/tree/0.2.9a1) (2026-05-25) [Full Changelog](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/compare/0.2.8...0.2.9a1) From cf5c5f37906b5bdcdedbc4672d982009b0b0e6fb Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 16 Jun 2026 05:28:33 +0100 Subject: [PATCH 08/10] chore: add missing shared CI workflows (#38) Add the opm-check reusable-workflow caller to complete the canonical OVOS plugin CI set. All other shared callers were already present and using OpenVoiceOS/gh-automations@dev. --- .github/workflows/opm-check.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/opm-check.yml diff --git a/.github/workflows/opm-check.yml b/.github/workflows/opm-check.yml new file mode 100644 index 0000000..7545741 --- /dev/null +++ b/.github/workflows/opm-check.yml @@ -0,0 +1,14 @@ +name: OPM Plugin Check + +on: + pull_request: + branches: [dev, master, main] + workflow_dispatch: + +jobs: + opm_check: + uses: OpenVoiceOS/gh-automations/.github/workflows/opm-check.yml@dev + secrets: inherit + with: + python_version: '3.11' + plugin_type: 'auto' From 372bd6db35fb31ed424646253f896b751410b007 Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 16 Jun 2026 04:51:54 +0000 Subject: [PATCH 09/10] Increment Version to 0.3.0a2 --- ovos_utterance_plugin_cancel/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utterance_plugin_cancel/version.py b/ovos_utterance_plugin_cancel/version.py index 59ee186..af8c8b5 100644 --- a/ovos_utterance_plugin_cancel/version.py +++ b/ovos_utterance_plugin_cancel/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 0 VERSION_MINOR = 3 VERSION_BUILD = 0 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK __version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}" + (f"a{VERSION_ALPHA}" if VERSION_ALPHA else "") From 2f7249e36f2638f82791af4c47f2c8e97592b084 Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 16 Jun 2026 05:01:53 +0000 Subject: [PATCH 10/10] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a939e1f..d0a8fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.3.0a2](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/tree/0.3.0a2) (2026-06-16) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/compare/0.3.0a1...0.3.0a2) + +**Merged pull requests:** + +- chore: add missing shared CI workflows [\#38](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/pull/38) ([JarbasAl](https://github.com/JarbasAl)) + ## [0.3.0a1](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/tree/0.3.0a1) (2026-05-25) [Full Changelog](https://github.com/OpenVoiceOS/ovos-utterance-plugin-cancel/compare/0.2.9a1...0.3.0a1)