Files
ai-video-fullstack/backend/services/pipecat/pipeline_events.py
Xin Wang d069e5282e Enhance greeting context management in Brain classes
- Introduce greeting context handling in BaseBrain and WorkflowBrain to manage assistant greetings effectively.
- Implement prepare_greeting_context method to add greeting messages to the local context while preserving playback order.
- Update pipeline event handling to ensure greeting timestamps are maintained until the client is ready.
- Enhance tests to verify the correct behavior of greeting context management in various scenarios.
2026-07-14 13:26:47 +08:00

209 lines
7.2 KiB
Python

"""Event registration for cascade and realtime conversation pipelines."""
from loguru import logger
from pipecat.frames.frames import (
EndFrame,
LLMMessagesAppendFrame,
OutputTransportMessageUrgentFrame,
TTSSpeakFrame,
)
from pipecat.runner.utils import (
get_transport_client_id,
maybe_capture_participant_camera,
)
from pipecat.utils.time import time_now_iso8601
def bind_cascade_pipeline_events(
*,
transport,
worker,
brain,
context,
text_input,
user_aggregator,
assistant_aggregator,
greeting: str,
vision_enabled: bool,
vision_state: dict[str, str | None],
) -> None:
"""Connect processors to transport events without owning pipeline assembly."""
pending_text_inputs: list[str] = []
greeting_transcript_sent = False
greeting_timestamp = ""
async def queue_transcript(role: str, content: str, timestamp: str) -> None:
if not content:
return
await worker.queue_frame(
OutputTransportMessageUrgentFrame(
message={
"type": "transcript",
"role": role,
"content": content,
"timestamp": timestamp,
}
)
)
async def append_user_text_to_context(text: str, *, run_llm: bool) -> None:
await worker.queue_frame(
LLMMessagesAppendFrame(
messages=[{"role": "user", "content": text}],
run_llm=run_llm,
)
)
@user_aggregator.event_handler("on_user_turn_stopped")
async def on_user_turn_stopped(_aggregator, _strategy, message):
await queue_transcript("user", message.content, message.timestamp)
@assistant_aggregator.event_handler("on_assistant_text_start")
async def on_assistant_text_start(_aggregator, turn_id, timestamp):
await brain.on_assistant_text_start(turn_id)
await worker.queue_frame(
OutputTransportMessageUrgentFrame(
message={
"type": "assistant-text-start",
"turn_id": turn_id,
"timestamp": timestamp,
}
)
)
@assistant_aggregator.event_handler("on_assistant_text_delta")
async def on_assistant_text_delta(_aggregator, turn_id, delta):
await worker.queue_frame(
OutputTransportMessageUrgentFrame(
message={
"type": "assistant-text-delta",
"turn_id": turn_id,
"delta": delta,
}
)
)
@assistant_aggregator.event_handler("on_assistant_text_end")
async def on_assistant_text_end(_aggregator, turn_id, content, interrupted):
await worker.queue_frame(
OutputTransportMessageUrgentFrame(
message={
"type": "assistant-text-end",
"turn_id": turn_id,
"content": content,
"interrupted": interrupted,
}
)
)
await brain.on_assistant_text_end(turn_id, content, interrupted)
@text_input.event_handler("on_text_input")
async def on_text_input(_processor, text):
pending_text_inputs.append(text)
# The transcript must be queued before the interruption is broadcast.
await queue_transcript("user", text, time_now_iso8601())
@assistant_aggregator.event_handler("on_interruption_processed")
async def on_interruption_processed(_aggregator):
if not pending_text_inputs:
return
text = pending_text_inputs.pop(0)
await append_user_text_to_context(text, run_llm=True)
@text_input.event_handler("on_text_append")
async def on_text_append(_processor, text):
brain.record_user_message(text)
await queue_transcript("user", text, time_now_iso8601())
await append_user_text_to_context(text, run_llm=False)
@text_input.event_handler("on_client_ready")
async def on_client_ready(_processor):
nonlocal greeting_transcript_sent
if greeting and not greeting_transcript_sent:
greeting_transcript_sent = True
await queue_transcript(
"assistant",
greeting,
greeting_timestamp or time_now_iso8601(),
)
await brain.on_client_ready()
@transport.event_handler("on_client_connected")
async def on_client_connected(_transport, _client):
nonlocal greeting_timestamp
if vision_enabled:
try:
vision_state["client_id"] = get_transport_client_id(
_transport,
_client,
)
await maybe_capture_participant_camera(_transport, _client)
logger.info(
f"视觉理解已接入视频客户端: {vision_state['client_id']}"
)
except Exception as exc: # noqa: BLE001 - media availability is optional
logger.warning(f"视觉理解摄像头捕获初始化失败: {exc}")
if greeting:
# Preserve the actual playback order. The transcript is delivered
# later on client-ready, but the preview sorts by this timestamp.
greeting_timestamp = greeting_timestamp or time_now_iso8601()
if brain.spec.owns_context:
brain.prepare_greeting_context(greeting, context)
await worker.queue_frame(
TTSSpeakFrame(greeting, append_to_context=False)
)
await brain.on_connected()
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(_transport, _client):
logger.info("对端断开,结束管线")
await worker.queue_frame(EndFrame())
def bind_realtime_pipeline_events(
*,
transport,
worker,
realtime,
text_input,
greeting: str,
) -> None:
"""Connect text and lifecycle events for a realtime model pipeline."""
async def queue_transcript(role: str, content: str) -> None:
if not content:
return
await worker.queue_frame(
OutputTransportMessageUrgentFrame(
message={
"type": "transcript",
"role": role,
"content": content,
"timestamp": time_now_iso8601(),
}
)
)
@text_input.event_handler("on_text_input")
async def on_text_input(_processor, text):
await queue_transcript("user", text)
await realtime.interrupt()
await realtime.send_text(text, run_immediately=True)
@text_input.event_handler("on_text_append")
async def on_text_append(_processor, text):
await queue_transcript("user", text)
await realtime.send_text(text, run_immediately=False)
@transport.event_handler("on_client_connected")
async def on_client_connected(_transport, _client):
if greeting:
await realtime.speak(greeting)
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(_transport, _client):
logger.info("Realtime 对端断开,结束管线")
await worker.queue_frame(EndFrame())