Skip to content

Resolve default snd resources from package data#150

Closed
goldyfruit wants to merge 1 commit into
OpenVoiceOS:devfrom
goldyfruit:fix/resolve-packaged-default-sounds
Closed

Resolve default snd resources from package data#150
goldyfruit wants to merge 1 commit into
OpenVoiceOS:devfrom
goldyfruit:fix/resolve-packaged-default-sounds

Conversation

@goldyfruit

@goldyfruit goldyfruit commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • resolve snd/* system sounds from installed OVOS package resources before falling back to generic resource lookup
  • add packaged fallbacks for snd/end_listening.wav -> snd/start_listening.wav and snd/cancel.mp3 -> snd/error.mp3
  • cover packaged-resource and alias behavior with unit tests

Problem

ovos-audio can receive valid URIs like snd/acknowledge.mp3 and still fail with FileNotFoundError: None does not exist when the actual sound files are shipped by another installed package such as ovos_dinkum_listener/res/snd.

Validation

python3.11 -m venv .venv311
.venv311/bin/pip install -e . pytest
.venv311/bin/pytest -q test/unittests/test_playback_service.py test/unittests/test_service_handlers.py

Summary by CodeRabbit

  • New Features

    • The audio service now supports bundled sounds from installed packages, enabling automatic resolution of packaged sound resources when local files aren't found.
    • Added predefined sound aliases to map common audio cues across different package sources.
  • Tests

    • Added tests for packaged sound resolution behavior, including alias mapping and fallback scenarios.

@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This change adds packaged sound resolution to the PlaybackService, enabling the system to locate bundled OVOS sounds from installed packages. It introduces constants for default sound packages and aliases, implements a new resolver method for packaged sounds, and enhances the existing URI resolution to check packaged resources as a fallback.

Changes

Cohort / File(s) Summary
Sound URI Resolution Enhancement
ovos_audio/service.py
Added support for resolving packaged sounds from installed OVOS packages. Introduced _DEFAULT_SOUND_PACKAGES and _DEFAULT_SOUND_ALIASES constants, implemented _resolve_packaged_sound_uri() static method to locate bundled sounds within package resources, and enhanced _resolve_sound_uri() to use the new packaged resolver after checking local paths.
Unit Tests
test/unittests/test_playback_service.py
Added comprehensive tests for packaged sound URI resolution, covering scenarios including existing packaged copies, alias resolution (end_listening.wav to start_listening.wav, cancel.mp3 to error.mp3), and fallback behavior when packaged sounds are missing. Tests use mocks for resource file resolution.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Poem

🐰 Hark! A bundle of sounds now nestles so near,
In packages bundled, with aliases clear.
No hunting through folders or searching in vain—
Packaged melodies ready again and again! 🎵

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding resolution of default sound resources from package data, which is the core objective of the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan for PR comments
  • Generate coding plan

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
test/unittests/test_playback_service.py (1)

137-199: Add one case where a default sound package is missing.

The resolver now depends on skipping packages that cannot be imported, but these tests always make files() return a path. A small regression test where files("ovos_audio") or files("ovos_dinkum_listener") raises ModuleNotFoundError and the other package still resolves would pin the production branch this PR is relying on.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_playback_service.py` around lines 137 - 199, Add a new
unit test that exercises PlaybackService._resolve_sound_uri when one of the
default sound packages cannot be imported: create temp resource(s) for the
remaining package, patch ovos_audio.service.files so that calling
files("ovos_audio") or files("ovos_dinkum_listener") raises ModuleNotFoundError
for one package while returning a Path for the other, patch
resolve_resource_file as needed to return the fallback path, call
PlaybackService._resolve_sound_uri with a packaged sound name and assert it
resolves to the path from the available package and that resolve_resource_file
is not called (or is called only for the expected fallback case); reference the
existing test helpers and patterns used in test_playback_service.py (e.g.,
patches around ovos_audio.service.files,
ovos_audio.service.resolve_resource_file and PlaybackService._resolve_sound_uri)
to add this regression test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ovos_audio/service.py`:
- Around line 486-503: Reject path traversal inputs before joining by validating
the incoming URI in _resolve_packaged_sound_uri: detect and reject any
normalized_uri (and any alias from _DEFAULT_SOUND_ALIASES) that contains '..' or
attempts to walk outside the intended res/ tree (e.g., components like "../" or
leading "/" after normalization) and return None early; apply the same
validation logic for the analogous resolver at lines 511-518 (same function or
sibling function using resource_parts and _DEFAULT_SOUND_PACKAGES) so no
candidate is passed to resource.joinpath or local os.path joins when it can
escape the res/ directory.
- Around line 497-500: The broad except around resolving package resources is
hiding real errors; narrow it to only catch the expected package-import failure
(ModuleNotFoundError and ImportError) for the
files(package).joinpath(*resource_parts) call in service.py so we continue when
the package truly isn't installed, and let other exceptions surface (either
re-raise them or log them with logger.exception before re-raising) so resolver
bugs are not swallowed.

---

Nitpick comments:
In `@test/unittests/test_playback_service.py`:
- Around line 137-199: Add a new unit test that exercises
PlaybackService._resolve_sound_uri when one of the default sound packages cannot
be imported: create temp resource(s) for the remaining package, patch
ovos_audio.service.files so that calling files("ovos_audio") or
files("ovos_dinkum_listener") raises ModuleNotFoundError for one package while
returning a Path for the other, patch resolve_resource_file as needed to return
the fallback path, call PlaybackService._resolve_sound_uri with a packaged sound
name and assert it resolves to the path from the available package and that
resolve_resource_file is not called (or is called only for the expected fallback
case); reference the existing test helpers and patterns used in
test_playback_service.py (e.g., patches around ovos_audio.service.files,
ovos_audio.service.resolve_resource_file and PlaybackService._resolve_sound_uri)
to add this regression test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cd22ca05-12da-4dff-935f-87228beef35d

📥 Commits

Reviewing files that changed from the base of the PR and between 132891a and 464ab62.

📒 Files selected for processing (2)
  • ovos_audio/service.py
  • test/unittests/test_playback_service.py

Comment thread ovos_audio/service.py
Comment on lines +486 to +503
def _resolve_packaged_sound_uri(uri: str) -> Optional[str]:
"""Resolve bundled OVOS sounds from installed package resources."""
normalized_uri = uri.replace("\\", "/")
candidates = [normalized_uri]
alias = _DEFAULT_SOUND_ALIASES.get(normalized_uri)
if alias and alias not in candidates:
candidates.append(alias)

for candidate in candidates:
resource_parts = ("res", *candidate.split("/"))
for package in _DEFAULT_SOUND_PACKAGES:
try:
resource = files(package).joinpath(*resource_parts)
except Exception:
continue
if resource.is_file():
return str(resource)
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject snd/../... before joining any paths.

The new resolver still accepts values like snd/../../../../tmp/x.wav: the prefix check passes, then both the local os.path.join(...) path and the packaged joinpath(...) path can walk out of res/ on filesystem-backed installs. Since these URIs come from bus messages, this turns sound resolution into arbitrary local-path lookup.

Suggested hardening
+from pathlib import PurePosixPath
+
+    `@staticmethod`
+    def _normalize_sound_uri(uri: str) -> str:
+        path = PurePosixPath(uri.replace("\\", "/"))
+        if path.is_absolute() or ".." in path.parts or path.parts[:1] != ("snd",):
+            raise ValueError(f"Invalid sound URI: {uri}")
+        return path.as_posix()
+
     `@staticmethod`
     def _resolve_packaged_sound_uri(uri: str) -> Optional[str]:
         """Resolve bundled OVOS sounds from installed package resources."""
-        normalized_uri = uri.replace("\\", "/")
+        normalized_uri = PlaybackService._normalize_sound_uri(uri)
         candidates = [normalized_uri]
         alias = _DEFAULT_SOUND_ALIASES.get(normalized_uri)
         if alias and alias not in candidates:
             candidates.append(alias)
         if uri.startswith("snd/") or uri.startswith("snd\\"):
-            normalized_uri = uri.replace("\\", "/")
+            normalized_uri = PlaybackService._normalize_sound_uri(uri)
             local_uri = os.path.join(os.path.dirname(__file__), "res",
                                      normalized_uri)

Also applies to: 511-518

🧰 Tools
🪛 Ruff (0.15.5)

[error] 499-500: try-except-continue detected, consider logging the exception

(S112)


[warning] 499-499: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_audio/service.py` around lines 486 - 503, Reject path traversal inputs
before joining by validating the incoming URI in _resolve_packaged_sound_uri:
detect and reject any normalized_uri (and any alias from _DEFAULT_SOUND_ALIASES)
that contains '..' or attempts to walk outside the intended res/ tree (e.g.,
components like "../" or leading "/" after normalization) and return None early;
apply the same validation logic for the analogous resolver at lines 511-518
(same function or sibling function using resource_parts and
_DEFAULT_SOUND_PACKAGES) so no candidate is passed to resource.joinpath or local
os.path joins when it can escape the res/ directory.

Comment thread ovos_audio/service.py
Comment on lines +497 to +500
try:
resource = files(package).joinpath(*resource_parts)
except Exception:
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Only swallow the expected “package not installed” failure here.

Catching every Exception masks real resolver bugs and can fall through to the old None does not exist error instead of surfacing the actual packaging problem. Narrow this to the expected import failure (ModuleNotFoundError/ImportError) and log or re-raise anything else.

Suggested change
             for package in _DEFAULT_SOUND_PACKAGES:
                 try:
                     resource = files(package).joinpath(*resource_parts)
-                except Exception:
+                except ModuleNotFoundError:
                     continue
+                except Exception:
+                    LOG.exception("Failed to inspect packaged sound '%s' in '%s'",
+                                  candidate, package)
+                    raise
                 if resource.is_file():
                     return str(resource)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
resource = files(package).joinpath(*resource_parts)
except Exception:
continue
for package in _DEFAULT_SOUND_PACKAGES:
try:
resource = files(package).joinpath(*resource_parts)
except ModuleNotFoundError:
continue
except Exception:
LOG.exception("Failed to inspect packaged sound '%s' in '%s'",
candidate, package)
raise
if resource.is_file():
return str(resource)
🧰 Tools
🪛 Ruff (0.15.5)

[error] 499-500: try-except-continue detected, consider logging the exception

(S112)


[warning] 499-499: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_audio/service.py` around lines 497 - 500, The broad except around
resolving package resources is hiding real errors; narrow it to only catch the
expected package-import failure (ModuleNotFoundError and ImportError) for the
files(package).joinpath(*resource_parts) call in service.py so we continue when
the package truly isn't installed, and let other exceptions surface (either
re-raise them or log them with logger.exception before re-raising) so resolver
bugs are not swallowed.

@goldyfruit goldyfruit closed this Mar 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant