How the Realtime Voice Agent Works in AgentScope with TTS Integration

The realtime voice agent in AgentScope uses the RealtimeAgent class to bridge WebSocket connections between a frontend and a realtime model, streaming audio through bidirectional queues that handle speech-to-text input and text-to-speech output via the DashScopeRealtimeModel integration.

The realtime voice agent architecture in AgentScope enables low-latency voice conversations by combining a WebSocket-based frontend connection with streaming TTS (Text-to-Speech) capabilities. This implementation centers on the RealtimeAgent class in the agentscope-ai/agentscope repository, which orchestrates audio streaming between client microphones and realtime AI models like DashScope's qwen3-omni-flash-realtime.

Architecture Overview of the Realtime Voice Agent

The realtime voice agent operates as a bidirectional streaming pipeline. At its core, the RealtimeAgent manages two primary data flows:

  • Inbound flow: Client audio (microphone input) → RealtimeAgent → Realtime Model (STT processing)
  • Outbound flow: Realtime Model (TTS generation) → RealtimeAgent → Frontend (speaker output)

This architecture relies on asyncio queues to handle concurrent streaming without blocking, enabling real-time conversation with sub-second latency.

Initializing the RealtimeAgent Class

Constructor and Internal Queues

The RealtimeAgent initialization establishes the core infrastructure for voice streaming. Located in src/agentscope/agent/_realtime_agent.py, the constructor creates two critical asyncio queues:


# From _realtime_agent.py lines 65-92

def __init__(
    self,
    name: str,
    model: RealtimeModel,
    sys_prompt: str = "You are a helpful assistant.",
    toolkit: Optional[Toolkit] = None,
) -> None:
    self.name = name
    self.model = model
    self.sys_prompt = sys_prompt
    self.toolkit = toolkit
    
    # Internal queues for event handling

    self._incoming_queue = asyncio.Queue()  # From model to agent

    self._model_response_queue = asyncio.Queue()  # From agent to frontend

    
    self.id = str(uuid.uuid4())
    self._tasks = []

The _incoming_queue receives raw events from the realtime model, while _model_response_queue holds processed events ready for frontend transmission. This dual-queue design separates concerns between model communication and client delivery.

Establishing the WebSocket Connection

The start() Method and Async Tasks

The start() method in _realtime_agent.py (lines 102-124) activates the realtime voice agent by establishing the model connection and launching concurrent processing loops:

async def start(self, outgoing_queue: asyncio.Queue) -> None:
    # Connect to the realtime model's WebSocket

    await self.model.connect()
    
    # Launch the forward loop (client → model)

    self._tasks.append(
        asyncio.create_task(self._forward_loop())
    )
    
    # Launch the response loop (model → client)

    self._tasks.append(
        asyncio.create_task(self._model_response_loop(outgoing_queue))
    )

This method creates two persistent asyncio tasks that run for the duration of the session. The _forward_loop handles incoming client audio, while _model_response_loop manages TTS output streaming.

Processing Client Input and Audio Streaming

Forwarding Events with _forward_loop()

The _forward_loop method processes client input from the frontend and forwards it to the realtime model. When the agent receives audio from the client, it packages the raw PCM data into an AudioBlock and transmits it via the model's send() method.

The implementation handles audio resampling when necessary. In _realtime_agent.py, the code processes ServerEvents from the client:


# Conceptual flow based on _realtime_agent.py implementation

async def _forward_loop(self):
    while True:
        event = await self._incoming_queue.get()
        
        if event.type == "client_audio":
            # Package into AudioBlock

            audio_block = AudioBlock(
                source=Base64Source(
                    data=event.audio_data,
                    media_type="audio/pcm"
                ),
                format={"type": "audio/pcm", "rate": 16000}
            )
            # Send to realtime model

            await self.model.send(audio_block)

This loop maintains the real-time stream from microphone to model, ensuring minimal latency for voice input processing.

TTS Integration and Audio Generation

Realtime Model Processing (DashScopeRealtimeModel)

The TTS integration occurs within the realtime model implementation. For DashScope integration, the DashScopeRealtimeModel class in src/agentscope/realtime/_dashscope_realtime_model.py handles the WebSocket communication with the remote service.

The send() method (lines 31-88) formats client input into the DashScope protocol:

async def send(self, block: Union[AudioBlock, TextBlock, ImageBlock]) -> None:
    if isinstance(block, AudioBlock):
        # Convert to DashScope audio format

        message = {
            "type": "input_audio",
            "audio": block.source.data,  # base64 encoded PCM

            "sample_rate": block.format["rate"]
        }
    elif isinstance(block, TextBlock):
        # Handle text input

        message = {"type": "input_text", "text": block.content}
    
    # Send via WebSocket

    await self.websocket.send(json.dumps(message))

Parsing Audio Delta Events

When the remote model generates speech, it streams audio chunks as response.audio.delta events. The parse_api_message method in _dashscope_realtime_model.py (lines 62-73) converts these into standardized ModelResponseAudioDeltaEvent objects:

def parse_api_message(self, data: str) -> ModelEvents:
    msg = json.loads(data)
    
    if msg.get("type") == "response.audio.delta":
        return ModelEvents.ModelResponseAudioDeltaEvent(
            type="audio_delta",
            data=msg["delta"],  # base64 PCM data

            sample_rate=msg.get("sample_rate", 24000)
        )
    elif msg.get("type") == "response.audio.done":
        return ModelEvents.ModelResponseAudioDoneEvent(type="audio_done")

This parsing layer abstracts the vendor-specific protocol (DashScope) into generic events that the RealtimeAgent can process uniformly.

Delivering Synthesized Speech to the Frontend

Mapping Model Events to Server Events

The _model_response_loop in _realtime_agent.py bridges the model's output to the client-facing server. It translates internal ModelEvents into ServerEvents using the from_model_event factory method:

async def _model_response_loop(self, outgoing_queue: asyncio.Queue):
    while True:
        # Get event from model

        model_event = await self._model_response_queue.get()
        
        # Convert to server event for frontend

        server_event = ServerEvents.from_model_event(model_event)
        
        # Queue for WebSocket transmission

        await outgoing_queue.put(server_event)

When the event is an AgentResponseAudioDeltaEvent, it contains the base64-encoded PCM audio chunk that the browser will decode and play. The AgentResponseAudioDoneEvent signals the end of the utterance.

Complete Implementation Example

Here is a complete server implementation showing how to wire the realtime voice agent with TTS integration:

from agentscope.agent import RealtimeAgent
from agentscope.realtime import DashScopeRealtimeModel
from fastapi import FastAPI, WebSocket
import asyncio
import os

app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    
    # Initialize the realtime model with TTS capability

    model = DashScopeRealtimeModel(
        model_name="qwen3-omni-flash-realtime",
        api_key=os.getenv("DASHSCOPE_API_KEY"),
    )
    
    # Create the realtime voice agent

    agent = RealtimeAgent(
        name="VoiceAssistant",
        sys_prompt="You are a helpful voice assistant.",
        model=model,
    )
    
    # Queue for frontend-bound events

    outgoing = asyncio.Queue()
    
    # Start the agent (opens WebSocket to model, starts loops)

    await agent.start(outgoing)
    
    # Task: Forward agent events to frontend WebSocket

    async def send_to_frontend():
        while True:
            event = await outgoing.get()
            await websocket.send_json(event.model_dump())
    
    # Task: Receive from frontend and send to agent

    async def receive_from_frontend():
        while True:
            data = await websocket.receive_json()
            await agent.handle_input(data)
    
    # Run both directions concurrently

    await asyncio.gather(
        send_to_frontend(),
        receive_from_frontend()
    )

Summary

  • The realtime voice agent in AgentScope centers on the RealtimeAgent class in src/agentscope/agent/_realtime_agent.py, which manages bidirectional audio streaming through asyncio queues.
  • TTS integration is handled by realtime model implementations like DashScopeRealtimeModel in src/agentscope/realtime/_dashscope_realtime_model.py, which stream base64-encoded PCM audio chunks via WebSocket.
  • The agent uses two primary loops: _forward_loop() to send client audio to the model, and _model_response_loop() to receive synthesized speech and forward it to the frontend.
  • Audio events flow through a standardized pipeline: ModelResponseAudioDeltaEventServerEvents.AgentResponseAudioDeltaEvent → WebSocket → Browser playback.

Frequently Asked Questions

How does the RealtimeAgent handle audio format conversion?

The RealtimeAgent processes raw PCM audio through the _forward_loop() method in src/agentscope/agent/_realtime_agent.py. When receiving audio from the frontend, it packages the data into AudioBlock objects with base64 encoding. If resampling is required, the agent handles PCM delta adjustments before forwarding to the model's send() method. The DashScope realtime model expects specific sample rates (typically 16kHz for input), and the agent ensures compatibility before transmission.

What is the difference between RealtimeAgent and standard TTS models in AgentScope?

The RealtimeAgent provides a unified, stateful conversation interface that combines both STT (Speech-to-Text) and TTS (Text-to-Speech) in a single WebSocket connection to a realtime model like DashScopeRealtimeModel. In contrast, standard TTS models in src/agentscope/tts/ (such as _dashscope_realtime_tts_model.py) provide only text-to-speech conversion without the conversational state management or STT capabilities. The RealtimeAgent handles the full duplex communication, queue management, and event translation required for interactive voice applications.

How does the TTS audio stream from the model to the browser?

The TTS audio stream flows through a three-stage pipeline. First, the DashScopeRealtimeModel receives response.audio.delta events from the remote API and parses them into ModelResponseAudioDeltaEvent objects containing base64-encoded PCM chunks. Second, the RealtimeAgent._model_response_loop() translates these into ServerEvents.AgentResponseAudioDeltaEvent objects and places them on the outgoing queue. Finally, the server (as shown in examples/agent/realtime_voice_agent/run_server.py) retrieves these events from the queue and transmits them via WebSocket to the browser, where JavaScript decodes the base64 PCM and plays it through the Web Audio API.

Can I use a different realtime model provider instead of DashScope?

Yes, the AgentScope architecture supports pluggable realtime models through the abstract base class in src/agentscope/realtime/_base.py. To integrate a different provider (such as OpenAI's Realtime API or Gemini), you would create a new class inheriting from the base realtime model class and implement the connect(), send(), and parse_api_message() methods. The RealtimeAgent in src/agentscope/agent/_realtime_agent.py interacts with these models through the standardized ModelEvents protocol, so switching providers requires no changes to the agent logic—only the model implementation needs to be swapped.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →