Files
vllm-omni/vllm_omni/experimental/fullduplex/joyvl/adapter.py
T
e241005a71 [Feature] JoyAI-VL-Interaction streaming interaction serving layer (#4575)
Signed-off-by: lishunyang12 <125541396+lishunyang12@users.noreply.github.com>
Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
Signed-off-by: lishunyang <lishunyang12@163.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: Yueqian Lin <linyueqian@outlook.com>
2026-06-21 07:42:51 +00:00

73 lines
2.8 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
from collections.abc import AsyncIterator, Awaitable, Callable
from typing import Any
from vllm_omni.experimental.fullduplex.core.adapter import DuplexAdapter, DuplexCapability, OutputChunk
from vllm_omni.experimental.fullduplex.core.session import DuplexSession
from vllm_omni.experimental.fullduplex.joyvl.bridges.delegation import DelegationBridge, StubDelegationBridge
from vllm_omni.experimental.fullduplex.joyvl.decision.output_parser import Action
from vllm_omni.experimental.fullduplex.joyvl.decision.policy import JoyVLPolicy, sample_frames
GenerateFn = Callable[[list[dict[str, Any]]], Awaitable[str]]
class JoyVLDuplexAdapter(DuplexAdapter):
def __init__(
self,
generate: GenerateFn,
*,
persona: str = "default",
num_frames: int = 4,
chunk_frames: int = 200,
frame_seconds: float = 1.0,
delegation: DelegationBridge | None = None,
) -> None:
self._generate = generate
self._policy = JoyVLPolicy(
persona=persona,
num_frames=num_frames,
chunk_frames=chunk_frames,
frame_seconds=frame_seconds,
delegation=delegation or StubDelegationBridge(),
)
self._frames: list[str] = []
self._pending_query: str | None = None
def capabilities(self) -> DuplexCapability:
return DuplexCapability(frozenset({"video", "text"}), frozenset({"text"}), proactive=True)
async def on_input(self, session: DuplexSession, modality: str, data: Any) -> None:
if modality == "video":
self._policy.tick()
self._frames.append(data)
elif modality == "text":
self._pending_query = data
def should_respond(self, session: DuplexSession) -> bool:
return bool(self._frames)
async def respond(self, session: DuplexSession) -> AsyncIterator[OutputChunk]:
policy = self._policy
await policy.fold_delegations()
policy.set_query(self._pending_query)
self._pending_query = None
parts = [{"type": "image_url", "image_url": {"url": f}} for f in sample_frames(self._frames, policy.num_frames)]
messages, _ = policy.build_messages(parts)
action = policy.commit(await self._generate(messages))
frames = [(str(i), f) for i, f in enumerate(self._frames)]
await policy.submit_if_delegate(action, frames)
if action.action is not Action.SILENCE and action.text:
yield OutputChunk("text", action.text)
if policy.needs_flush():
await policy.flush(frames)
self._frames.clear()
async def on_barge_in(self, session: DuplexSession) -> None:
self._pending_query = None