Skip to content

Conversation

@Nash0x7E2
Copy link
Member

@Nash0x7E2 Nash0x7E2 commented Nov 17, 2025

  • Add a new decart plugin
  • Add Decart Restyling processor

Summary by CodeRabbit

  • New Features

    • Decart plugin for real-time video transformation with dynamic prompt adjustments and mirror mode
    • Video frame resizing utility with aspect ratio preservation
  • Documentation

    • Added comprehensive plugin documentation and example implementations
  • Tests

    • Added integration and unit test coverage for new functionality

@coderabbitai
Copy link

coderabbitai bot commented Nov 17, 2025

Walkthrough

Introduces a new Decart plugin for vision-agents that enables live video style restyling through a realtime API integration. Includes the RestylingProcessor class, DecartVideoTrack handler, example agent setup, tests, and workspace configuration updates. Adds a frame resize utility to core video utilities.

Changes

Cohort / File(s) Summary
Plugin Configuration & Setup
pyproject.toml, plugins/decart/pyproject.toml, plugins/decart/example/pyproject.toml, plugins/decart/README.md
Adds Decart plugin workspace to main pyproject.toml; introduces plugin packaging config using Hatch with vision-agents and decart dependencies; includes example project config with Poetry-like UVS sources and editable plugin references; documents plugin purpose
Decart Core Functionality
plugins/decart/vision_agents/plugins/decart/__init__.py, plugins/decart/vision_agents/plugins/decart/decart_restyling_processor.py, plugins/decart/vision_agents/plugins/decart/decart_video_track.py
Exports RestylingProcessor public API; implements RestylingProcessor class managing Decart Realtime API connection, video transformation, prompt/mirror updates, and lifecycle; implements DecartVideoTrack class wrapping Decart API frames into VideoStreamTrack interface with frame queuing and recv at 30 FPS
Tests
plugins/decart/tests/test_decart_restyling.py, plugins/decart/tests/test_decart_video_track.py
Tests RestylingProcessor initialization and API key handling (with dotenv integration); comprehensive tests for DecartVideoTrack covering initialization, frame addition with resizing, recv behavior, stopped state, and placeholder frames
Example Agent Integration
plugins/decart/example/decart_example.py
Demonstrates agent setup with RestylingProcessor, LLM-callable function for runtime prompt updates, and call lifecycle management via create_agent() and join_call() entry points
Core Utilities
agents-core/vision_agents/core/utils/video_utils.py
Adds resize_frame() method for aspect-ratio-preserving frame resizing using PIL with black letterboxing and av.VideoFrame conversion

Sequence Diagram(s)

sequenceDiagram
    participant Agent
    participant RestylingProcessor
    participant DecartAPI as Decart Realtime API
    participant DecartVideoTrack
    participant Publisher

    Agent->>RestylingProcessor: process_video(incoming_track)
    RestylingProcessor->>DecartAPI: connect (first call only)
    RestylingProcessor->>DecartAPI: send initial_state(prompt, mirror)
    
    loop Processing Loop
        RestylingProcessor->>DecartAPI: recv transformed_frames
        DecartAPI-->>RestylingProcessor: styled frames
        RestylingProcessor->>DecartVideoTrack: add_frame(restyled_frame)
    end
    
    Agent->>RestylingProcessor: update_prompt(new_prompt)
    RestylingProcessor->>DecartAPI: update prompt
    
    Agent->>RestylingProcessor: publish_video_track()
    RestylingProcessor-->>Publisher: VideoStreamTrack
    
    Publisher->>DecartVideoTrack: recv()
    DecartVideoTrack-->>Publisher: av.VideoFrame @ 30 FPS
Loading
sequenceDiagram
    participant CLI
    participant AgentLauncher
    participant createAgent as create_agent()
    participant Agent
    participant LLM
    participant joinCall as join_call()
    
    CLI->>AgentLauncher: launch
    AgentLauncher->>createAgent: call
    createAgent->>Agent: new Agent(RestylingProcessor, LLM, STT/TTS)
    createAgent->>LLM: register_callable(change_prompt)
    createAgent-->>AgentLauncher: Agent
    
    AgentLauncher->>joinCall: call (call_type, call_id)
    joinCall->>Agent: create User
    joinCall->>Agent: create Call
    joinCall->>Agent: join Call
    joinCall->>Agent: wait for call end
    joinCall->>Agent: finish()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • RestylingProcessor implementation: Non-trivial async lifecycle management, Decart API integration, state tracking, and reconnection logic warrant careful review of connection flow and error handling
  • DecartVideoTrack frame handling: Verify async recv timing (30 FPS), frame queuing with VideoLatestNQueue, placeholder frame logic, and stop condition handling
  • Integration complexity: Cross-cutting concerns between processor, video track, and example agent—ensure proper ownership of responsibilities and cleanup
  • Test coverage adequacy: Verify tests cover edge cases like failed connections, frame drops, and prompt update failures

Suggested labels

agents-core, examples, plugins, new-feature

Suggested reviewers

  • tschellenbach

Poem

The frames arrive, restyled and new,
Through Decart's neural vein they brew—
A cyberpunk dream, prompt-spun and bright,
Queue-held and whispered to the night,
While mirrors toggle, video flows,
The API sighs where closures go.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.58% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main addition: a new Decart restyling processor plugin. It is concise, clear, and directly reflects the primary change.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/decart

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.

Copy link

@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: 2

🧹 Nitpick comments (6)
plugins/decart/README.md (1)

1-4: Consider expanding README with basic usage details

Right now this is a minimal stub; consider adding a short “Getting started” section (required env vars, how to enable the plugin, and a link to the example) so users can discover the new processor more easily.

plugins/decart/tests/test_decart_plugin.py (1)

1-16: Clarify async test execution and make the examples exercise real behavior

  • test_regular and test_simple currently just assert True, so they don’t validate anything about the Decart plugin. Either flesh them out (even simple smoke checks on import/wiring) or drop them to avoid a false sense of coverage.
  • test_simple is async def with only @pytest.mark.integration. Unless you’ve configured pytest-asyncio for automatic coroutine handling (asyncio_mode = "auto" or similar), this coroutine might not be awaited, and its body may never run.

Consider either making test_simple a synchronous test for now, or adding the appropriate asyncio marker/configuration so it’s actually executed as async.

plugins/decart/tests/test_decart_restyling.py (1)

10-35: Tighten tests and align comments with actual behavior

  • test_regular only does assert True while the comment claims it will fail if DECART_API_KEY is not set. Either wire it to actually exercise RestylingProcessor (or skip when the key is missing), or relax the comment so it matches reality.
  • test_processor_initialization is declared async def with @pytest.mark.integration, but the body is fully synchronous. It could just be a regular def, or—if you plan to await async APIs later—ensure your pytest/pytest-asyncio configuration is set up so this coroutine is actually awaited (or mark it with @pytest.mark.asyncio as appropriate).
  • The skip logic on ValueError is fine for now, but if you ever change the error message, matching on "API key" may become brittle; checking for DECART_API_KEY in the message or using a dedicated exception type would be more robust.

None of these are blockers, but cleaning them up will make the tests more truthful and maintainable.

plugins/decart/example/decart_example.py (1)

21-67: Double-check join context-manager usage and minor example polish

  • In join_call (Lines [50-64]), you’re doing with await agent.join(call): inside an async function. This is correct only if Agent.join is an async function that returns a synchronous context manager. If instead Agent.join is implemented as an async context manager (common pattern), this should be async with agent.join(call): instead. Worth confirming against the Agent.join signature to avoid subtle lifecycle bugs.
  • create_agent(**kwargs) (Line [21]) doesn’t use kwargs. If the launcher never passes anything through, you could drop **kwargs for clarity, or forward them into Agent/components if that’s the intent.

The rest of the example wiring (processor, LLM, TTS/STT, and change_prompt registration) reads clearly for showcasing the plugin.

plugins/decart/vision_agents/plugins/decart/restyling.py (2)

165-210: Connection / reconnection loop looks reasonable, with minor nit on duplicate reconnect paths

The combination of _on_error scheduling _connect_to_decart and _processing_loop periodically attempting reconnects when not self._connected and not self._connecting is a reasonable resilience strategy. The _connecting flag should prevent concurrent connection attempts.

If you ever see overlapping reconnect attempts in logs, consider centralizing reconnection into _processing_loop and having _on_error only flip flags, but this is optional and not a blocker.

Also applies to: 245-260


269-290: Close/cleanup logic is pragmatic; be aware of get_event_loop behavior

The close() implementation that cancels internal tasks and then either schedules _async_close() on a running loop or run_until_complete() on a non‑running loop is pragmatic and should work in the typical agent lifecycle.

Longer‑term, you might want to:

  • Use asyncio.get_running_loop() inside branches where you know you’re in an async context.
  • Expose an explicit async aclose() API that callers can await, to avoid synchronous run_until_complete calls from arbitrary threads.

Not required for this PR but worth considering as the processor gets more usage.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2368813 and 5b1eca0.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • plugins/decart/README.md (1 hunks)
  • plugins/decart/example/decart_example.py (1 hunks)
  • plugins/decart/example/pyproject.toml (1 hunks)
  • plugins/decart/pyproject.toml (1 hunks)
  • plugins/decart/tests/test_decart_plugin.py (1 hunks)
  • plugins/decart/tests/test_decart_restyling.py (1 hunks)
  • plugins/decart/vision_agents/plugins/decart/__init__.py (1 hunks)
  • plugins/decart/vision_agents/plugins/decart/decart_video_track.py (1 hunks)
  • plugins/decart/vision_agents/plugins/decart/restyling.py (1 hunks)
  • pyproject.toml (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
plugins/decart/vision_agents/plugins/decart/restyling.py (2)
agents-core/vision_agents/core/processors/base_processor.py (3)
  • AudioVideoProcessor (117-146)
  • VideoProcessorMixin (66-73)
  • VideoPublisherMixin (84-86)
plugins/decart/vision_agents/plugins/decart/decart_video_track.py (4)
  • DecartVideoTrack (14-104)
  • recv (80-100)
  • add_frame (44-49)
  • stop (102-104)
plugins/decart/example/decart_example.py (4)
agents-core/vision_agents/core/agents/agents.py (3)
  • Agent (74-1316)
  • create_call (774-779)
  • finish (588-621)
agents-core/vision_agents/core/agents/agent_launcher.py (1)
  • AgentLauncher (18-125)
plugins/decart/vision_agents/plugins/decart/restyling.py (1)
  • set_prompt (146-154)
agents-core/vision_agents/core/llm/llm.py (2)
  • LLM (49-418)
  • register_function (212-225)
plugins/decart/vision_agents/plugins/decart/decart_video_track.py (1)
agents-core/vision_agents/core/utils/video_queue.py (2)
  • VideoLatestNQueue (6-28)
  • put_latest_nowait (22-28)
plugins/decart/vision_agents/plugins/decart/__init__.py (1)
plugins/decart/vision_agents/plugins/decart/restyling.py (1)
  • RestylingProcessor (41-295)
plugins/decart/tests/test_decart_restyling.py (1)
plugins/decart/vision_agents/plugins/decart/restyling.py (1)
  • RestylingProcessor (41-295)
🪛 GitHub Actions: CI (unit)
plugins/decart/vision_agents/plugins/decart/restyling.py

[error] 109-109: Mypy type error: Argument 1 to 'realtime' of 'Models' has incompatible type 'str'; expected Literal['mirage', 'mirage_v2', 'lucy_v2v_720p_rt'].


[error] 227-227: Mypy type error: Argument 1 to 'add_frame' of 'DecartVideoTrack' has incompatible type 'Frame | Packet'; expected 'VideoFrame'.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: unit / Test "not integration"
  • GitHub Check: unit / Ruff
  • GitHub Check: unit / Mypy
🔇 Additional comments (7)
pyproject.toml (1)

28-28: Decart workspace wiring looks consistent

The new vision-agents-plugins-decart source and "plugins/decart" workspace member align with how other plugins are registered; I don’t see issues here.

Also applies to: 59-60

plugins/decart/pyproject.toml (1)

1-41: Decart plugin packaging looks reasonable

Build-system, project metadata, dependency-groups, and Hatch/UV configuration look consistent for a standalone plugin package; no blocking issues from my side.

plugins/decart/vision_agents/plugins/decart/__init__.py (1)

1-3: Public export of RestylingProcessor looks good

Importing RestylingProcessor here and exposing it via __all__ matches how the tests and example import the processor; this is a clean export point.

plugins/decart/vision_agents/plugins/decart/decart_video_track.py (1)

14-103: Overall DecartVideoTrack design looks solid

The track’s placeholder frame, latest‑N queue with capacity 2, and ~30 FPS recv() loop with last‑frame fallback form a clean and predictable WebRTC source. The _resize_frame helper is well‑factored for potential future use.

plugins/decart/vision_agents/plugins/decart/restyling.py (3)

221-228: Frame receive loop is fine; mypy error here is addressed via DecartVideoTrack.add_frame

The while not self._video_track._stopped loop that calls frame = await transformed_stream.recv() and forwards to self._video_track.add_frame(frame) is structurally sound. The mypy error at Line 227 (Frame | PacketVideoFrame) is best handled by relaxing the parameter type of DecartVideoTrack.add_frame and guarding for av.VideoFrame there (see comment in decart_video_track.py).

After updating DecartVideoTrack.add_frame as suggested, please re‑run mypy to confirm that the CI error reported for Line 227 here disappears.


41-145: RestylingProcessor overall structure integrates cleanly with the existing processor mixins

The processor wires Decart Realtime into the existing AudioVideoProcessor / VideoProcessorMixin / VideoPublisherMixin stack in a clear way: process_video handles incoming tracks, publish_video_track exposes the transformed track, and prompt/mirror setters guard on connection state. Logging around connection lifecycle is also helpful for debugging.


5-5: Verify the proposed Literal type alias resolves the mypy error at Line 109

The proposed fix is syntactically sound and follows best practices. The code currently passes self.model_name (a plain str from the constructor parameter) to models.realtime(), and the fix correctly narrows this to a Literal type alias. The model names you've included match the documented supported models.

However, I cannot independently confirm the mypy error at Line 109 exists—I lack access to the Decart SDK type stubs to verify what models.realtime() actually expects. The fix itself is correct and will resolve the issue if the SDK stubs define models.realtime() to accept a Literal type.

Before merging, verify:

  • Run mypy plugins/decart/vision_agents/plugins/decart/restyling.py locally to confirm the error is resolved after applying this change
  • If the SDK adds new models, update DecartRealtimeModel accordingly

Comment on lines +6 to +23
dependencies = [
"vision-agents",
"python-dotenv",
"vision-agents-plugins-openai",
"vision-agents-plugins-decart",
"vision-agents-plugins-elvenlabs",
"vision-agents-plugins-getstream",
"vision-agents-plugins-deepgram",

]

[tool.uv.sources]
vision-agents = { workspace = true }
vision-agents-plugins-getstream = { editable=true }
vision-agents-plugins-openai = { editable=true }
vision-agents-plugins-elevenlabs = { editable=true }
vision-agents-plugins-deepgram = { editable=true }
vision-agents-plugins-decart = { editable=true }
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Fix ElevenLabs plugin dependency name typo

Line [11] declares:

  • vision-agents-plugins-elvenlabs

but [tool.uv.sources] (Line [21]) and the rest of the repo use vision-agents-plugins-elevenlabs. This mismatch will prevent the example project from resolving the dependency correctly.

Recommend updating the dependency name:

 dependencies = [
   "vision-agents",
   "python-dotenv",
   "vision-agents-plugins-openai",
   "vision-agents-plugins-decart",
-  "vision-agents-plugins-elvenlabs",
+  "vision-agents-plugins-elevenlabs",
   "vision-agents-plugins-getstream",
   "vision-agents-plugins-deepgram",
 ]

I'll verify this dependency name mismatch by searching the repository for package definitions and other references.


🏁 Script executed:

#!/bin/bash
# Search for all occurrences of elevenlabs or elvenlabs (case-insensitive)
echo "=== Searching for 'elevenlabs' references ==="
rg -i "elevenlabs" -n --type toml --type py --type txt

echo ""
echo "=== Searching for 'elvenlabs' references ==="
rg -i "elvenlabs" -n --type toml --type py --type txt

echo ""
echo "=== Looking for elevenlabs package directory ==="
fd -i "elevenlabs|elvenlabs" -t d

echo ""
echo "=== Checking pyproject.toml files for package name patterns ==="
fd "pyproject.toml" -x grep -H "vision-agents-plugins-elven" {}

Length of output: 14667


Fix ElevenLabs plugin dependency name typo

Line 11 declares "vision-agents-plugins-elvenlabs" but [tool.uv.sources] (Line 21) and the rest of the repository use vision-agents-plugins-elevenlabs. This mismatch will prevent the example project from resolving the dependency correctly.

Recommend updating the dependency name:

 dependencies = [
   "vision-agents",
   "python-dotenv",
   "vision-agents-plugins-openai",
   "vision-agents-plugins-decart",
-  "vision-agents-plugins-elvenlabs",
+  "vision-agents-plugins-elevenlabs",
   "vision-agents-plugins-getstream",
   "vision-agents-plugins-deepgram",
 ]
📝 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
dependencies = [
"vision-agents",
"python-dotenv",
"vision-agents-plugins-openai",
"vision-agents-plugins-decart",
"vision-agents-plugins-elvenlabs",
"vision-agents-plugins-getstream",
"vision-agents-plugins-deepgram",
]
[tool.uv.sources]
vision-agents = { workspace = true }
vision-agents-plugins-getstream = { editable=true }
vision-agents-plugins-openai = { editable=true }
vision-agents-plugins-elevenlabs = { editable=true }
vision-agents-plugins-deepgram = { editable=true }
vision-agents-plugins-decart = { editable=true }
dependencies = [
"vision-agents",
"python-dotenv",
"vision-agents-plugins-openai",
"vision-agents-plugins-decart",
"vision-agents-plugins-elevenlabs",
"vision-agents-plugins-getstream",
"vision-agents-plugins-deepgram",
]
[tool.uv.sources]
vision-agents = { workspace = true }
vision-agents-plugins-getstream = { editable=true }
vision-agents-plugins-openai = { editable=true }
vision-agents-plugins-elevenlabs = { editable=true }
vision-agents-plugins-deepgram = { editable=true }
vision-agents-plugins-decart = { editable=true }
🤖 Prompt for AI Agents
plugins/decart/example/pyproject.toml lines 6-23: the dependency list contains a
typo "vision-agents-plugins-elvenlabs" (line 11) which doesn't match the correct
package name used elsewhere ("vision-agents-plugins-elevenlabs"); update the
dependency entry to "vision-agents-plugins-elevenlabs" so it matches the
[tool.uv.sources] and the rest of the repo, and ensure there are no other
mismatched names or duplicate entries in the dependencies array.

@@ -0,0 +1,104 @@
import asyncio
import logging
from typing import Optional
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Broaden add_frame parameter type to match aiortc/av typings and fix mypy error

CI reports a type mismatch at the call site where frame = await transformed_stream.recv() is passed into DecartVideoTrack.add_frame: recv() is typed as returning Frame | Packet, but add_frame currently accepts only av.VideoFrame. To keep things type‑safe and robust while still only processing video frames, you can relax the parameter type and add a runtime guard:

-import logging
-from typing import Optional
+import logging
+from typing import Any, Optional
@@
-    async def add_frame(self, frame: av.VideoFrame) -> None:
-        if self._stopped:
-            return
-        # if frame.width != self.width or frame.height != self.height:
-        #     frame = await asyncio.to_thread(self._resize_frame, frame)
-        self.frame_queue.put_latest_nowait(frame)
+    async def add_frame(self, frame: Any) -> None:
+        if self._stopped:
+            return
+
+        # Only process video frames; ignore any other payload types (e.g., packets).
+        if not isinstance(frame, av.VideoFrame):
+            logger.debug("Ignoring non-video frame in DecartVideoTrack.add_frame: %r", frame)
+            return
+
+        # if frame.width != self.width or frame.height != self.height:
+        #     frame = await asyncio.to_thread(self._resize_frame, frame)
+        self.frame_queue.put_latest_nowait(frame)

This should satisfy mypy at the call site in restyling._receive_frames_from_decart while ensuring non‑video data won’t accidentally enter the track. Please re‑run mypy to confirm the CI error at Line 227 in restyling.py disappears.

Also applies to: 44-50


I'll verify the review comment by examining the relevant code and type signatures.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find and examine the DecartVideoTrack implementation
fd "decart_video_track\.py" -x cat {}

echo "---"
echo "Checking restyling.py for the call site around line 227:"
echo "---"

# Search for restyling.py and show context around recv() call
fd "restyling\.py" -x cat {} | head -250 | tail -50

Length of output: 5309


🏁 Script executed:

#!/bin/bash
# Search for where add_frame is called to understand the type mismatch
rg -n "add_frame" -B 3 -A 3 --type py

Length of output: 32368


🏁 Script executed:

#!/bin/bash
# Check what aiortc.MediaStreamTrack is and what recv returns
rg -n "MediaStreamTrack\|recv" --type py -B 2 -A 2 | head -100

Length of output: 0


I need to verify the type signature of aiortc's MediaStreamTrack.recv() to confirm the review comment's claims.


---

🌐 Web query:

```web
aiortc MediaStreamTrack recv return type Frame Packet

💡 Result:

Short answer: MediaStreamTrack.recv() returns either a decoded AV Frame (av.audio.frame.AudioFrame or av.video.frame.VideoFrame) or an av.packet.Packet (raw/encoded), i.e. Union[Frame, Packet]. Use isinstance to branch on which you received.

Why: aiortc tracks can deliver decoded frames (common for VideoStreamTrack/AudioStreamTrack or when a player/decoder is used) or raw encoded packets (when the source/MediaPlayer is configured not to decode). Example check:

  • if isinstance(obj, av.video.frame.VideoFrame) / av.audio.frame.AudioFrame: handle decoded frame
  • elif isinstance(obj, av.packet.Packet): handle encoded packet

References: aiortc API (MediaStreamTrack.recv docs) and implementation. [1][2][3]

Sources
[1] aiortc API — MediaStreamTrack.recv return type.
[2] aiortc mediastreams.py (recv docstring & signature).
[3] aiortc helpers/MediaPlayer notes on decoded frames vs packets.


Broaden add_frame parameter type to resolve type mismatch with aiortc.MediaStreamTrack.recv()

The type mismatch is real: aiortc's MediaStreamTrack.recv() is documented to return Union[Frame, Packet] (which includes av.VideoFrame, av.AudioFrame, or av.Packet), but add_frame currently accepts only av.VideoFrame. This causes mypy to flag the call site in restyling.py:227. Relax the parameter type and guard with an isinstance check:

-import logging
-from typing import Optional
+import logging
+from typing import Any, Optional
@@
-    async def add_frame(self, frame: av.VideoFrame) -> None:
-        if self._stopped:
-            return
-        # if frame.width != self.width or frame.height != self.height:
-        #     frame = await asyncio.to_thread(self._resize_frame, frame)
-        self.frame_queue.put_latest_nowait(frame)
+    async def add_frame(self, frame: Any) -> None:
+        if self._stopped:
+            return
+
+        # Only process video frames; ignore any other payload types (e.g., packets).
+        if not isinstance(frame, av.VideoFrame):
+            logger.debug("Ignoring non-video frame in DecartVideoTrack.add_frame: %r", frame)
+            return
+
+        # if frame.width != self.width or frame.height != self.height:
+        #     frame = await asyncio.to_thread(self._resize_frame, frame)
+        self.frame_queue.put_latest_nowait(frame)

This resolves the mypy error at restyling.py:227 while safely filtering non-video payloads.

Also applies to: 44-50

🤖 Prompt for AI Agents
In plugins/decart/vision_agents/plugins/decart/decart_video_track.py around
lines 3 and also affecting lines 44-50, the add_frame parameter is typed too
narrowly as av.VideoFrame while aiortc.MediaStreamTrack.recv() can return
av.VideoFrame, av.AudioFrame or av.Packet; change the add_frame signature to
accept the broader union type (e.g., av.VideoFrame | av.AudioFrame | av.Packet
or typing.Union[...] ) and inside the function add an isinstance guard that only
processes av.VideoFrame values (ignore or return early for non-video frames) so
mypy is satisfied and non-video payloads are safely filtered out.

Copy link

@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: 1

🧹 Nitpick comments (3)
plugins/decart/vision_agents/plugins/decart/restyling.py (3)

215-234: Frame forwarding loop is straightforward but could log non-cancellation errors

The _receive_frames_from_decart loop correctly bails on CancelledError and forwards only av.VideoFrame instances into DecartVideoTrack. If you ever need to diagnose dropped frames or upstream failures, you may want to also log unexpected exceptions here instead of letting them silently kill the task.

     async def _receive_frames_from_decart(
         self, transformed_stream: MediaStreamTrack
     ) -> None:
-        try:
-            while not self._video_track._stopped:
-                frame = await transformed_stream.recv()
-                if frame and isinstance(frame, av.VideoFrame):
-                    await self._video_track.add_frame(frame)
-        except asyncio.CancelledError:
-            logger.debug("Frame receiving from Decart cancelled")
+        try:
+            while not self._video_track._stopped:
+                frame = await transformed_stream.recv()
+                if frame and isinstance(frame, av.VideoFrame):
+                    await self._video_track.add_frame(frame)
+        except asyncio.CancelledError:
+            logger.debug("Frame receiving from Decart cancelled")
+        except Exception as e:
+            logger.error("Error while receiving frames from Decart: %s", e)

---

`273-298`: **close() behavior is mostly safe but async cleanup is fire-and-forget when loop is running**

The `close()` method correctly stops the local video track and cancels background tasks, then runs `_async_close()` either via `run_until_complete` (no running loop) or `asyncio.create_task` (running loop), swallowing `RuntimeError` if no loop is present.

This is generally fine, but note that in the “loop is running” case cleanup of `_realtime_client` / `_decart_client` is fire‑and‑forget; callers can’t know when the underlying connections are actually closed. If you need stronger lifecycle guarantees (e.g., tests waiting for full shutdown), consider exposing an async `async_close()` public method that awaits `_async_close()` directly, and have `close()` remain the best‑effort sync wrapper.

---

`66-116`: **Literal is currently accurate; optional refactor suggestion remains valid**

The verification confirms that the current `Literal["mirage", "mirage_v2", "lucy_v2v_720p_rt"]` matches the currently documented valid realtime model names in the Decart Python SDK. The code is functionally correct.

However, the original suggestion to either drop the `Literal` or derive it from Decart's type hints remains sound from a maintainability perspective, since the Literal will need manual updates if Decart adds new models in the future. The refactor is optional but recommended to reduce future maintenance burden.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: Path: .coderabbit.yaml

**Review profile**: CHILL

**Plan**: Pro

**Disabled knowledge base sources:**

- Linear integration is disabled by default for public repositories

> You can enable these sources in your CodeRabbit configuration.

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5b1eca0acc19b223c093fa1b5e05a7662eeb448b and 9edb8988b0457c1ebdf68afed07bd36c0d0ec3d9.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `plugins/decart/example/decart_example.py` (1 hunks)
* `plugins/decart/vision_agents/plugins/decart/restyling.py` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* plugins/decart/example/decart_example.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code graph analysis (1)</summary>

<details>
<summary>plugins/decart/vision_agents/plugins/decart/restyling.py (2)</summary><blockquote>

<details>
<summary>agents-core/vision_agents/core/processors/base_processor.py (3)</summary>

* `AudioVideoProcessor` (117-146)
* `VideoProcessorMixin` (66-73)
* `VideoPublisherMixin` (84-86)

</details>
<details>
<summary>plugins/decart/vision_agents/plugins/decart/decart_video_track.py (4)</summary>

* `DecartVideoTrack` (14-104)
* `recv` (80-100)
* `add_frame` (44-49)
* `stop` (102-104)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)</summary>

* GitHub Check: unit / Mypy
* GitHub Check: unit / Test "not integration"
* GitHub Check: unit / Ruff
* GitHub Check: unit / Test "not integration"
* GitHub Check: unit / Ruff & mypy

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>plugins/decart/vision_agents/plugins/decart/restyling.py (3)</summary><blockquote>

`26-39`: **Reconnect heuristic is clear and focused on transient failures**

The `_should_reconnect` helper cleanly separates retry decisions and focuses on connection/timeout‑style failures, which is a good default for avoiding pointless retries on auth/config errors.

---

`129-147`: **Video processing + publishing flow looks consistent with core mixins**

`process_video` wiring (`_current_track` + `_connect_to_decart`) and `publish_video_track` returning the dedicated `DecartVideoTrack` align with the `AudioVideoProcessor` / `VideoPublisherMixin` patterns and keep state in one place (`_current_track`, `_video_track`).

---

`149-167`: **Prompt/mirror setters behave defensively when not connected**

Guarding `set_prompt` and `set_mirror` on `_realtime_client` presence avoids accidental calls before connection and gives clear logging, which is a nice, low‑surprise behavior for callers.

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

@GetStream GetStream deleted a comment from coderabbitai bot Nov 18, 2025
Copy link

@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: 1

🧹 Nitpick comments (3)
agents-core/vision_agents/core/utils/video_utils.py (1)

53-61: Consider extracting common aspect-ratio scaling logic.

The aspect ratio calculation logic (lines 53-61) is duplicated in resize_frame (lines 96-106). Consider extracting this into a helper function to improve maintainability.

Example helper function:

def _calculate_scaled_dimensions(
    src_width: int, src_height: int, target_width: int, target_height: int
) -> tuple[int, int]:
    """Calculate scaled dimensions maintaining aspect ratio."""
    scale = min(target_width / src_width, target_height / src_height)
    return int(src_width * scale), int(src_height * scale)

Also applies to: 96-106

plugins/decart/vision_agents/plugins/decart/decart_restyling_processor.py (2)

110-112: Consider validating model name before casting.

The cast to a Literal type doesn't provide runtime validation. If an invalid model name is passed, the error will occur later when the Decart SDK validates it, potentially making debugging harder.

Consider adding explicit validation or at least documenting the valid model names in the docstring.

+        # Valid model names
+        valid_models = ["mirage", "mirage_v2", "lucy_v2v_720p_rt"]
+        if model not in valid_models:
+            raise ValueError(
+                f"Invalid model '{model}'. Valid options: {', '.join(valid_models)}"
+            )
+
         self.model = models.realtime(
             cast(Literal["mirage", "mirage_v2", "lucy_v2v_720p_rt"], self.model_name)
         )

298-304: Event loop handling in close() could be simplified.

The conditional logic for handling running vs non-running event loops is necessary but somewhat complex. The duplicate check for client existence on lines 300 and 303 could be simplified.

Consider this refactoring:

     loop = asyncio.get_event_loop()
+    has_clients = self._realtime_client or self._decart_client
+    if not has_clients:
+        return
+
     if loop.is_running():
-        if self._realtime_client or self._decart_client:
-            asyncio.create_task(self._async_close())
+        asyncio.create_task(self._async_close())
     else:
-        if self._realtime_client or self._decart_client:
-            loop.run_until_complete(self._async_close())
+        loop.run_until_complete(self._async_close())
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9edb898 and ccc2dfe.

📒 Files selected for processing (7)
  • agents-core/vision_agents/core/utils/video_utils.py (2 hunks)
  • plugins/decart/example/decart_example.py (1 hunks)
  • plugins/decart/tests/test_decart_restyling.py (1 hunks)
  • plugins/decart/tests/test_decart_video_track.py (1 hunks)
  • plugins/decart/vision_agents/plugins/decart/__init__.py (1 hunks)
  • plugins/decart/vision_agents/plugins/decart/decart_restyling_processor.py (1 hunks)
  • plugins/decart/vision_agents/plugins/decart/decart_video_track.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • plugins/decart/vision_agents/plugins/decart/init.py
  • plugins/decart/example/decart_example.py
  • plugins/decart/vision_agents/plugins/decart/decart_video_track.py
🧰 Additional context used
🧬 Code graph analysis (3)
plugins/decart/tests/test_decart_restyling.py (3)
plugins/decart/vision_agents/plugins/decart/decart_restyling_processor.py (13)
  • RestylingProcessor (42-311)
  • close (288-304)
  • publish_video_track (140-141)
  • process_video (129-138)
  • update_prompt (143-172)
  • set_mirror (174-181)
  • _connect_to_decart (183-228)
  • _on_error (263-267)
  • _on_connection_change (249-261)
  • _disconnect_from_decart (281-286)
  • _processing_loop (270-279)
  • _receive_frames_from_decart (239-247)
  • _on_remote_stream (230-237)
plugins/decart/vision_agents/plugins/decart/decart_video_track.py (3)
  • DecartVideoTrack (15-76)
  • recv (52-72)
  • stop (74-76)
plugins/decart/tests/test_decart_video_track.py (1)
  • sample_frame (17-19)
plugins/decart/vision_agents/plugins/decart/decart_restyling_processor.py (2)
agents-core/vision_agents/core/processors/base_processor.py (3)
  • AudioVideoProcessor (117-146)
  • VideoProcessorMixin (66-73)
  • VideoPublisherMixin (84-86)
plugins/decart/vision_agents/plugins/decart/decart_video_track.py (1)
  • DecartVideoTrack (15-76)
plugins/decart/tests/test_decart_video_track.py (2)
plugins/decart/vision_agents/plugins/decart/decart_video_track.py (1)
  • DecartVideoTrack (15-76)
agents-core/vision_agents/core/utils/audio_queue.py (2)
  • qsize (40-42)
  • get (119-136)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: unit / Test "not integration"
  • GitHub Check: unit / Mypy
  • GitHub Check: unit / Ruff & mypy
  • GitHub Check: unit / Test "not integration"
🔇 Additional comments (4)
plugins/decart/tests/test_decart_restyling.py (1)

1-446: Well-structured test suite with comprehensive coverage.

The test suite effectively covers the RestylingProcessor functionality:

  • Connection lifecycle and state management
  • Prompt and mirror updates with connection state checks
  • Frame handling and forwarding
  • Reconnection logic for various error types
  • Proper use of mocks to isolate unit behavior

The consistent use of processor.close() for cleanup and appropriate async patterns demonstrate good testing practices.

plugins/decart/tests/test_decart_video_track.py (1)

1-124: Thorough test coverage for DecartVideoTrack.

The test suite comprehensively validates DecartVideoTrack behavior:

  • Initialization with default and custom dimensions
  • Frame adding with and without resize requirements
  • Frame queue management (2-frame capacity)
  • Placeholder frame behavior when queue is empty
  • Stop state preventing further operations
  • Proper pts and time_base assignment on received frames

The tests effectively exercise the public API and verify expected behavior.

plugins/decart/vision_agents/plugins/decart/decart_restyling_processor.py (2)

183-228: Robust connection management with proper guards.

The connection logic correctly:

  • Prevents duplicate connection attempts with the _connecting flag
  • Cleans up existing connections before reconnecting
  • Initializes the realtime client with proper state (prompt, mirror)
  • Registers necessary event handlers
  • Handles errors by resetting state and re-raising

The pattern ensures connection integrity and prevents resource leaks.


230-248: Clean frame handling with proper task lifecycle management.

The frame receiving logic:

  • Correctly cancels previous frame receiving tasks before starting new ones (preventing duplicates)
  • Continuously forwards frames from Decart's transformed stream to the output video track
  • Handles cancellation gracefully with try/except for CancelledError
  • Respects the video track's stopped state

Comment on lines +90 to +116
def resize_frame(self, frame: av.VideoFrame) -> av.VideoFrame:
logger.debug(
f"Resizing frame from {frame.width}x{frame.height} to {self.width}x{self.height}"
)
img = frame.to_image()

# Calculate scaling to maintain aspect ratio
src_width, src_height = img.size
target_width, target_height = self.width, self.height

# Calculate scale factor (fit within target dimensions)
scale = min(target_width / src_width, target_height / src_height)
new_width = int(src_width * scale)
new_height = int(src_height * scale)

# Resize with aspect ratio maintained
resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS)

# Create black background at target resolution
result = Image.new("RGB", (target_width, target_height), (0, 0, 0))

# Paste resized image centered
x_offset = (target_width - new_width) // 2
y_offset = (target_height - new_height) // 2
result.paste(resized, (x_offset, y_offset))

return av.VideoFrame.from_image(result)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Fix misleading function signature for module-level function.

This function is defined at module level with a self parameter, which is misleading since self conventionally indicates a class method. The function is called from DecartVideoTrack as asyncio.to_thread(resize_frame, self, frame), passing the track instance as the first argument.

The parameter should be renamed and properly typed to clarify that it expects an object with width and height attributes.

Apply this diff to fix the signature:

-def resize_frame(self, frame: av.VideoFrame) -> av.VideoFrame:
+def resize_frame(target_dimensions: object, frame: av.VideoFrame) -> av.VideoFrame:
+    """
+    Resize a video frame to target dimensions while preserving aspect ratio.
+    
+    Args:
+        target_dimensions: Object with width and height attributes.
+        frame: Input video frame.
+    
+    Returns:
+        Resized video frame centered on black background.
+    """
     logger.debug(
-        f"Resizing frame from {frame.width}x{frame.height} to {self.width}x{self.height}"
+        f"Resizing frame from {frame.width}x{frame.height} to {target_dimensions.width}x{target_dimensions.height}"
     )
     img = frame.to_image()

     # Calculate scaling to maintain aspect ratio
     src_width, src_height = img.size
-    target_width, target_height = self.width, self.height
+    target_width, target_height = target_dimensions.width, target_dimensions.height

     # Calculate scale factor (fit within target dimensions)
     scale = min(target_width / src_width, target_height / src_height)
     new_width = int(src_width * scale)
     new_height = int(src_height * scale)

     # Resize with aspect ratio maintained
-    resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
+    resized = img.resize((new_width, new_height), Resampling.LANCZOS)

     # Create black background at target resolution
     result = Image.new("RGB", (target_width, target_height), (0, 0, 0))

     # Paste resized image centered
     x_offset = (target_width - new_width) // 2
     y_offset = (target_height - new_height) // 2
     result.paste(resized, (x_offset, y_offset))

     return av.VideoFrame.from_image(result)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In agents-core/vision_agents/core/utils/video_utils.py around lines 90-116, the
function is defined at module level but uses the misleading parameter name
"self"; rename that parameter to something like "track" and add a proper type
annotation (e.g., track: Any or define a small Protocol with width: int and
height: int) to clarify it expects an object with width and height attributes,
then update the function signature and any internal "self" references to use
"track" while leaving call sites (asyncio.to_thread(resize_frame, self, frame))
unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants