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: add basic implementation of the Connector interface and a StreamingAudioInputDevice Connector #350

Merged
merged 17 commits into from
Jan 3, 2025

Conversation

rachwalk
Copy link
Contributor

@rachwalk rachwalk commented Dec 20, 2024

Purpose

The purpose of the connector interface is to provide an abstraction over different devices and communication protocols RAI agents may use. This connector will be used in the ASR refactor, to provide an easily configurable abstraction for sound devices.

Proposed Changes

Adds the Connector interface and implementation of it in the for of an StreamingAudioInputDevice, and its required tests.

Issues

#309

Testing

Unit tests for the StreamingAudioInputDevice were added.

Summary by CodeRabbit

  • New Features

    • Added communication module for audio device management
    • Introduced base classes for message handling and audio streaming
    • Implemented streaming audio input device functionality
  • Tests

    • Added unit tests for sound device connector
  • Chores

    • Updated GitHub Actions workflow to support audio testing
    • Modified pytest configuration to manage test markers

@rachwalk rachwalk marked this pull request as ready for review December 20, 2024 15:35
@boczekbartek boczekbartek changed the title [feat] Add basic implementation of the Connector interface and a StreamingAu… feat: add basic implementation of the Connector interface and a StreamingAudioInputDevice Connector Dec 23, 2024
@rachwalk rachwalk requested a review from maciejmajek January 2, 2025 12:38
@maciejmajek
Copy link
Member

@coderabbitai full review

Copy link
Contributor

coderabbitai bot commented Jan 2, 2025

Walkthrough

This pull request introduces a comprehensive audio communication module for the RAI project. The changes include setting up a GitHub Actions workflow with audio device support, updating pytest configuration, and implementing a new communication framework. The core addition is a StreamingAudioInputDevice class that manages audio input streams, with abstract base classes for message handling and device configuration. The implementation supports flexible audio device setup, resampling, and error handling.

Changes

File Change Summary
.github/workflows/poetry-test.yml Added step to create virtual audio device by installing jackd and starting a dummy audio server
pyproject.toml Added ci_only marker and updated pytest options to exclude CI-only tests
src/rai/rai/communication/__init__.py Created module initialization with imports and __all__ definition
src/rai/rai/communication/base_connector.py Introduced abstract base classes BaseMessage and BaseConnector for communication framework
src/rai/rai/communication/sound_device_connector.py Implemented StreamingAudioInputDevice for managing audio input streams with configuration and error handling
tests/communication/test_sound_device_connector.py Added unit tests for StreamingAudioInputDevice class functionality

Sequence Diagram

sequenceDiagram
    participant User
    participant StreamingAudioInputDevice
    participant SoundDevice
    
    User->>StreamingAudioInputDevice: configure_device()
    StreamingAudioInputDevice-->>User: Device configured
    
    User->>StreamingAudioInputDevice: start_action()
    StreamingAudioInputDevice->>SoundDevice: Initialize input stream
    SoundDevice-->>StreamingAudioInputDevice: Stream callback
    StreamingAudioInputDevice->>User: Return action handle
    
    User->>StreamingAudioInputDevice: terminate_action()
    StreamingAudioInputDevice->>SoundDevice: Stop input stream
Loading

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/rai/rai/communication/base_connector.py (2)

20-22: Consider removing or justifying ABC inheritance in BaseMessage
BaseMessage is declared as an abstract base class but lacks abstract methods or properties. If you don't plan to add any, you can remove the ABC inheritance to avoid confusion. Alternatively, you can introduce abstract methods or properties if you foresee extension.

🧰 Tools
🪛 Ruff (0.8.2)

20-20: BaseMessage is an abstract base class, but it has no abstract methods or properties

(B024)


24-49: Add docstrings for public abstract methods
While the abstract methods in BaseConnector are clear from their names and signatures, adding docstrings or usage examples would improve readability and clarity. Explaining the expected parameters, return types, and usage scenarios encourages consistent implementations in subclasses.

src/rai/rai/communication/sound_device_connector.py (2)

66-83: Typo in configred_devices
self.configred_devices appears to be a misspelling. Consider renaming it to self.configured_devices for clarity and consistency.

-        self.configred_devices: dict[str, ConfiguredDevice] = {}
+        self.configured_devices: dict[str, ConfiguredDevice] = {}

84-98: Typo in error messages: "suport"
The error messages raised in send_message, receive_message, and send_and_wait have a small spelling mistake ("suport" instead of "support"). Fixing it will improve readability.

-    "StreamingAudioInputDevice does not suport sending messages"
+    "StreamingAudioInputDevice does not support sending messages"
pyproject.toml (1)

90-90: Document CI-specific test requirements.

The addopts configuration now excludes both billable and CI-only tests by default. Consider adding documentation about which tests require CI environment and why, to help developers understand when to use the ci_only marker.

Add a comment in the README.md or testing documentation:

+ ## Test Markers
+ 
+ - `billable`: Tests that incur costs and are excluded by default
+ - `ci_only`: Tests that require CI environment (e.g., specific audio device setup)
+
+ To run CI-only tests locally: `pytest -m "ci_only"`
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f441e0 and 7601b1f.

📒 Files selected for processing (6)
  • .github/workflows/poetry-test.yml (2 hunks)
  • pyproject.toml (1 hunks)
  • src/rai/rai/communication/__init__.py (1 hunks)
  • src/rai/rai/communication/base_connector.py (1 hunks)
  • src/rai/rai/communication/sound_device_connector.py (1 hunks)
  • tests/communication/test_sound_device_connector.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
tests/communication/test_sound_device_connector.py

55-55: Do not assert False (python -O removes these calls), raise AssertionError()

Replace assert False

(B011)


103-103: Do not assert False (python -O removes these calls), raise AssertionError()

Replace assert False

(B011)

src/rai/rai/communication/base_connector.py

20-20: BaseMessage is an abstract base class, but it has no abstract methods or properties

(B024)

src/rai/rai/communication/sound_device_connector.py

136-136: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🔇 Additional comments (7)
src/rai/rai/communication/base_connector.py (1)

1-14: License header check
The Apache License header is present and correctly formatted.

tests/communication/test_sound_device_connector.py (3)

40-63: Device configuration tests
The test verifies that configure_device applies configuration attributes correctly, including consumer sampling rate, window size, target sampling rate, and data type. Looks good for coverage.

🧰 Tools
🪛 Ruff (0.8.2)

55-55: Do not assert False (python -O removes these calls), raise AssertionError()

Replace assert False

(B011)


65-82: Unconfigured device failure test
The test test_start_action_failed_init correctly asserts that a SoundDeviceError is raised when trying to start an action on an unconfigured device. This ensures robust error handling.


84-115: Action start test coverage
The test validates successful stream creation and callback handling. The usage of fixtures and mocks ensures reliable coverage of the start_action flow. Looks good.

🧰 Tools
🪛 Ruff (0.8.2)

103-103: Do not assert False (python -O removes these calls), raise AssertionError()

Replace assert False

(B011)

src/rai/rai/communication/__init__.py (1)

1-23: Re-exporting classes and errors
Importing and enumerating BaseMessage, BaseConnector, StreamingAudioInputDevice, and SoundDeviceError in __all__ helps define a clear public API. Looks good.

.github/workflows/poetry-test.yml (1)

41-46: Virtual audio device setup
Installing and running jackd in dummy mode is a good approach for enabling audio-related tests in CI. This step looks properly integrated.

pyproject.toml (1)

88-88: LGTM! Verify test markers in the codebase.

The new ci_only marker is correctly defined and follows the existing pattern.

Let's verify the usage of this marker in test files:

✅ Verification successful

ci_only marker is correctly used in sound device tests

The new ci_only marker is properly used in tests/communication/test_sound_device_connector.py for three test functions that verify sound device configuration and initialization. This makes sense as these tests likely require specific audio hardware setup that's only available in the CI environment.

  • Found in test_sound_device_connector.py:
    • test_configure: Tests device configuration
    • test_start_action_failed_init: Tests failure handling
    • test_start_action: Tests start action
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for tests marked with ci_only to understand the testing requirements
# Expected: Find test files using the new marker

# Search for @pytest.mark.ci_only decorators
rg "@pytest\.mark\.ci_only" -t python

# Also check for alternative syntax
rg "pytest\.mark\.ci_only" -t python

Length of output: 140


Script:

#!/bin/bash
# Description: Check for tests marked with ci_only to understand the testing requirements
# Let's try without the file type flag since it's not supported

# Search for @pytest.mark.ci_only decorators
rg "@pytest\.mark\.ci_only"

# Also check for alternative syntax
rg "pytest\.mark\.ci_only"

# Let's also check for files that might contain pytest tests
fd "test_.*\.py$"

Length of output: 712


Script:

#!/bin/bash
# Let's check the context of the ci_only markers to understand their usage
rg "@pytest\.mark\.ci_only" -B 2 -A 5 tests/communication/test_sound_device_connector.py

Length of output: 558

src/rai/rai/communication/sound_device_connector.py Outdated Show resolved Hide resolved
Comment on lines 99 to 142
def start_action(
self,
target: str,
on_feedback: Callable[[np.ndarray, dict[str, Any]], None],
on_finish: Callable = lambda _: None,
) -> str:

target_device = self.configred_devices.get(target)
if target_device is None:
raise SoundDeviceError(f"Device {target} has not been configured")

def callback(indata: np.ndarray, frames: int, _, status: CallbackFlags):
indata = indata.flatten()
sample_time_length = len(indata) / target_device.target_sampling_rate
if target_device.sample_rate != target_device.target_sampling_rate:
indata = resample(indata, int(sample_time_length * target_device.target_samping_rate)) # type: ignore
flag_dict = {
"input_overflow": status.input_overflow,
"input_underflow": status.input_underflow,
"output_overflow": status.output_overflow,
"output_underflow": status.output_underflow,
"priming_output": status.priming_output,
}
on_feedback(indata, flag_dict)

handle = self._generate_handle()
try:
stream = sd.InputStream(
samplerate=target_device.sample_rate,
channels=1,
device=int(target),
dtype=target_device.dtype,
blocksize=target_device.window_size_samples,
callback=callback,
finished_callback=on_finish,
)
except AttributeError:
raise SoundDeviceError(f"Device {target} has not been correctly configured")
stream.start()
self.streams[handle] = stream
return handle

def terminate_action(self, action_handle: str):
self.streams[action_handle].stop()
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 2, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix typos and enhance exception chaining

  1. The resample call references the incorrect key target_samping_rate; it should read target_sampling_rate.
  2. In the except AttributeError block, you can leverage exception chaining by using raise <Exception> from exc or raise SoundDeviceError(...) from None to distinguish the new exception from the original cause.
-indata = resample(indata, int(sample_time_length * target_device.target_samping_rate)) 
+indata = resample(indata, int(sample_time_length * target_device.target_sampling_rate)) 

...
except AttributeError as exc:
-    raise SoundDeviceError(f"Device {target} has not been correctly configured")
+    raise SoundDeviceError(f"Device {target} has not been correctly configured") from exc
📝 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
def start_action(
self,
target: str,
on_feedback: Callable[[np.ndarray, dict[str, Any]], None],
on_finish: Callable = lambda _: None,
) -> str:
target_device = self.configred_devices.get(target)
if target_device is None:
raise SoundDeviceError(f"Device {target} has not been configured")
def callback(indata: np.ndarray, frames: int, _, status: CallbackFlags):
indata = indata.flatten()
sample_time_length = len(indata) / target_device.target_sampling_rate
if target_device.sample_rate != target_device.target_sampling_rate:
indata = resample(indata, int(sample_time_length * target_device.target_samping_rate)) # type: ignore
flag_dict = {
"input_overflow": status.input_overflow,
"input_underflow": status.input_underflow,
"output_overflow": status.output_overflow,
"output_underflow": status.output_underflow,
"priming_output": status.priming_output,
}
on_feedback(indata, flag_dict)
handle = self._generate_handle()
try:
stream = sd.InputStream(
samplerate=target_device.sample_rate,
channels=1,
device=int(target),
dtype=target_device.dtype,
blocksize=target_device.window_size_samples,
callback=callback,
finished_callback=on_finish,
)
except AttributeError:
raise SoundDeviceError(f"Device {target} has not been correctly configured")
stream.start()
self.streams[handle] = stream
return handle
def terminate_action(self, action_handle: str):
self.streams[action_handle].stop()
def start_action(
self,
target: str,
on_feedback: Callable[[np.ndarray, dict[str, Any]], None],
on_finish: Callable = lambda _: None,
) -> str:
target_device = self.configred_devices.get(target)
if target_device is None:
raise SoundDeviceError(f"Device {target} has not been configured")
def callback(indata: np.ndarray, frames: int, _, status: CallbackFlags):
indata = indata.flatten()
sample_time_length = len(indata) / target_device.target_sampling_rate
if target_device.sample_rate != target_device.target_sampling_rate:
indata = resample(indata, int(sample_time_length * target_device.target_sampling_rate)) # type: ignore
flag_dict = {
"input_overflow": status.input_overflow,
"input_underflow": status.input_underflow,
"output_overflow": status.output_overflow,
"output_underflow": status.output_underflow,
"priming_output": status.priming_output,
}
on_feedback(indata, flag_dict)
handle = self._generate_handle()
try:
stream = sd.InputStream(
samplerate=target_device.sample_rate,
channels=1,
device=int(target),
dtype=target_device.dtype,
blocksize=target_device.window_size_samples,
callback=callback,
finished_callback=on_finish,
)
except AttributeError as exc:
raise SoundDeviceError(f"Device {target} has not been correctly configured") from exc
stream.start()
self.streams[handle] = stream
return handle
def terminate_action(self, action_handle: str):
self.streams[action_handle].stop()
🧰 Tools
🪛 Ruff (0.8.2)

136-136: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

Copy link
Member

Choose a reason for hiding this comment

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

Check now

Copy link
Contributor

Choose a reason for hiding this comment

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

@maciejmajek, here’s what we have from the latest script output:

  1. Typo correction:

    • The string "target_samping_rate" no longer appears in the repository, confirming the mismatch is presumably corrected to "target_sampling_rate".
  2. Exception chaining:

    • We still don’t see "from exc" usage or except AttributeError as exc in the file. The script found an except AttributeError: but not with exception chaining (from exc).

Thus, it appears that the typo fix has been integrated, but the recommended exception chaining is not in place yet. If you wish to apply exception chaining, please modify that block to use, for example:

except AttributeError as exc:
    raise SoundDeviceError(f"Device {target} has not been correctly configured") from exc

That should address both points from your earlier feedback. Let me know if you have any other questions!

🔗 Analysis chain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Let's verify that the typo "target_samping_rate" is removed and replaced by "target_sampling_rate".
echo "Searching for 'target_samping_rate' occurrences..."
rg "target_samping_rate"

# Let's also verify that we're now using exception chaining with 'from exc' in the except block.
echo ""
echo "Searching for 'except AttributeError as exc' lines and the following lines..."
rg -A 3 "except AttributeError as exc" src/rai/rai/communication/sound_device_connector.py

Length of output: 416


Script:

#!/bin/bash
# Let's broaden our search efforts to see if the fix was applied anywhere, or if "from exc" occurs in the codebase.
# We will search for "from exc" usage, references to raising SoundDeviceError, and references to "AttributeError".

echo "Searching for 'from exc' usage..."
rg "from exc"

echo ""
echo "Searching for 'raise SoundDeviceError' lines to see if chaining might have been manually inserted..."
rg -A 3 "raise SoundDeviceError"

echo ""
echo "Searching for 'AttributeError' references in entire codebase..."
rg "AttributeError" -A 5

Length of output: 3554

Copy link
Member

Choose a reason for hiding this comment

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

@rachwalk please apply exception chaining suggestion

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

tests/communication/test_sound_device_connector.py Outdated Show resolved Hide resolved
Copy link
Member

@maciejmajek maciejmajek left a comment

Choose a reason for hiding this comment

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

Looks good overall. Few things:

  • Naming: Please adjust the class names to better reflect audio/sound nature of the devices
  • Typos
  • CI hangs

@rachwalk rachwalk requested a review from maciejmajek January 2, 2025 16:03
Copy link
Member

@maciejmajek maciejmajek left a comment

Choose a reason for hiding this comment

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

One thing, otherwise LGTM

@rachwalk rachwalk merged commit 2cc2176 into development Jan 3, 2025
5 checks passed
@rachwalk rachwalk deleted the refactor/rai_asr branch January 3, 2025 10:47
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.

2 participants