Build an AI Voice Assistant with Twilio Voice and Media Streams, Rime's Prism, and Node.js Twilio published a tutorial for building an AI voice assistant that connects Rime's newly released Prism speech-to-speech model to Twilio Voice and Media Streams using Node.js 22+ and Rime's @rimelabs/sdk. The walkthrough covers interruption handling, a tool call, and persisting customer details with Twilio Conversation Memory and analyzing transcripts with Conversation Intelligence, requiring a Twilio Voice number, a Rime API key, and ngrok for tunneling. Build an AI Voice Assistant with Twilio Voice and Media Streams, Rime's Prism, and Node.js Time to read: Written by Reviewed by We're so excited for our friends at Rime https://www.rime.ai/prism for their release of Prism https://www.rime.ai/prism , their new speech-to-speech model for voice AI agents. In this tutorial, we'll show you how to connect Prism to Twilio's voice infrastructure using Twilio Media Streams https://www.twilio.com/docs/voice/media-streams , Node.js, and Rime's Node SDK. When you're done, you'll be able to call a phone number and talk to an AI voice assistant – complete with interruption handling and a tool call. After that, we’ll show you how to persist customer details with Twilio’s Conversation Memory https://www.twilio.com/docs/conversations/memory , and analyze transcripts with Conversation Intelligence https://www.twilio.com/docs/conversations/intelligence . So who you gonna call 👻? Buckle up… let's get started Prerequisites toc-heading-39e86b0d-da8a-48f9-96df-351bbd8c386e Prerequisites Every good tutorial has a few prerequisites – to follow along with this tutorial, you will first need: - Node.js 22+ We used 22.12.0 for testing this tutorial; you can download it from here https://nodejs.org/ - A Twilio account. If you don't have one yet, sign up for a free trial here https://www.twilio.com/try-twilio . - A Twilio number with Voice capabilities. Here are instructions https://help.twilio.com/articles/223135247-How-to-Search-for-and-Buy-a-Twilio-Phone-Number-from-Console if you don't have one yet. - A Rime account and a Rime API key. You can create a key in the Rime dashboard under API Tokens https://app.rime.ai/tokens . - ngrok or another tunneling solution to expose your local server to the internet for testing. You can download ngrok here https://ngrok.com/ . - A cell phone or landline that can make an outgoing phone call, and a willingness to talk to the agent that picks up Got all that? Then you're ready… let's build. Set up the Prism speech-to-speech Node project toc-heading-ce8f9caf-fc8d-4680-b29a-8d4caa86da24 Set up the Prism speech-to-speech Node project Although you just ticked through the prerequisites, we still need to cover a few scaffolding and setup steps before we can build in earnest. In these next steps, we'll walk you through setting up your project, installing dependencies, and the code you'll need to connect Twilio and Prism. Alright, let's really get started Step 1: Initialize the Project toc-heading-359ce7d2-7fe6-40d7-84b2-1722a028481b Step 1: Initialize the Project First, set up a new Node.js project. From the command line, run: Step 2: Install dependencies toc-heading-8d56401d-a622-4432-b587-db7793da0dc0 Step 2: Install dependencies Next, you need to install the required dependencies for the project. We'll be using the web framework Fastify https://fastify.dev/ , a great option for a Node framework with WebSocket support. And we will store our sensitive environment variable in a .env file You can find more information on this strategy here https://www.twilio.com/en-us/blog/working-with-environment-variables-in-node-js-html . We'll also install Rime's SDK, @rimelabs/sdk . Step 3: Create the project files toc-heading-af1ecf06-e27c-4c0e-ae78-6fad00551dd5 Step 3: Create the project files Next, we will create three files: .env for our secrets, index.js for the server, audio.js for converting audio between Twilio and Prism. There's a bit more on why we need that in a moment. Step 3.1: Create the .env File toc-heading-fea29a8a-d8f3-49e7-97ad-aaa1f8ef5fbb Step 3.1: Create the .env File First, create the .env file: Then – in your favorite text editor ed 's our favorite https://en.wikipedia.org/wiki/Ed %28text editor%29 – add your RIME API KEY on the first line: The Rime SDK reads RIME API KEY on its own, so you won't see us pass it anywhere in the following code. Step 3.2: Create the index.js and audio.js Files toc-heading-539582ec-4cd3-4cde-96ed-bd493bc17a0b Step 3.2: Create the index.js and audio.js Files Next, create the two code files in your project directory: Step 4: Write the audio conversion code toc-heading-143263f2-5a79-4222-9a26-5382dde66d1e Step 4: Write the audio conversion code There's one wrinkle when connecting a phone call between Prism and Twilio's Media Streams: they don't speak the same audio format. - Twilio Media Streams sends and receives https://www.twilio.com/docs/voice/media-streams/websocket-messages start-message 8 kHz G.711 μ-law https://en.wikipedia.org/wiki/G.711 . - Prism takes 16 kHz PCM16 in and sends 24 kHz PCM16 out https://docs.rime.ai/docs/prism/overview . It doesn't accept or send μ-law. So on the way in, we need to decode μ-law to PCM16, while Rime's SDK resamples it for Prism. On the way out, we need to drop from 24 kHz down to 8 kHz for Media Streams. Our algorithm is relatively simple: run the audio through a low-pass filter with a 3.6 kHz cutoff – which removes nearly everything at 4 kHz and above – then keep every third sample. Paste the following code in your audio.js : Here's what the functions do: - mulawToPcm16 decodes μ-law to PCM16. This decoder comes from Rime's docs https://docs.rime.ai/docs/prism/live-conversations phone-audio . - encodeMulaw converts one PCM16 sample into a μ-law byte. - FILTER is a low-pass filter. It keeps the 300–3,400 Hz telephone voice band from ITU-T G.712 https://www.itu.int/rec/T-REC-G.712 , cuts off at 3.6 kHz, and removes nearly everything at 4 kHz and above. - createReplyEncoder keeps every third sample, leaving us with 8 kHz μ-law that Twilio can play. Step 5: Write the code toc-heading-d828c4c2-f648-4637-a622-60d595908d23 Step 5: Write the code Here is a brief overview of the main components that will make up the AI voice assistant system we’ll be building. - Twilio Voice and Media Streams : Handles the phone call and streams audio via WebSockets. - Node.js Server : Your code and orchestration: manages the webhook, handles media conversion, connects to Rime, and includes any needed business logic. - Rime Prism : Provides the speech-to-speech AI responses. Now, let's walk through building each part of the application step by step. Step 5.1: Import dependencies, load our Environment Variable, and initialize Fastify toc-heading-0cafb0f4-d2d3-4078-933b-882cbdcda995 We start by importing required modules and loading the environment variables from our .env file. Paste the following code in your index.js : Step 5.2: Define some constants toc-heading-70e4df45-57b0-4c3d-b9d2-c6ccd8ef83c7 Step 5.2: Define some constants Next, we define constants for the voice, the server port, the assistant's first line, and its instructions. Here's what you should paste next in your file: VOICE controls how the assistant will sound. Prism has four voices https://docs.rime.ai/docs/prism/sessions choose-a-voice : astra the default , aurora , brando , and wawona . PORT controls which port your application will open. We'll discuss this a little more in the ngrok section below. MARK INTERVAL MS controls how often we ask Twilio to confirm what the caller has heard. We'll explain why that matters in Step 5.5. OPENING is your assistant's first spoken line. INSTRUCTIONS is the system prompt. You might think a speech-to-speech model wouldn't need rules about markdown – but Prism speaks every word it writes, and without guidance, it can answer like a chat assistant, with lists and math notation. Rime recommends starting your instructions with rules for speech, so we do. Song lyrics without a song name? Wonder if you can find those anywhere else. And Alexander Graham Bell wanted everyone to say "ahoy" https://www.twilio.com/docs/glossary/what-is-ahoy – so does your voice agent. Step 5.3: Define a tool toc-heading-f0fb2f9b-5fb7-49d3-9740-ee81ddb82404 Step 5.3: Define a tool Prism can ask your backend to run a tool , sometimes known as function calling . To show a full tool round, we'll give the assistant one simple tool: the current time. Paste this next: TIME TOOL is the definition for Prism: a name, a description, and a JSON Schema for the arguments. The description is how Prism decides whether to call the tool – you need to write it the way you'd write documentation for a colleague who can't read your code. getCurrentTime is our tool. It's a mock, of course, and straightforward to demo – swap in your own business logic here later Step 5.4: Define two routes toc-heading-002b9f50-f747-4f5c-b126-5cd3255dff08 Step 5.4: Define two routes Now, let's get to the heart of the code. We create our Rime client, then define a root route more of a health check… and a route to handle incoming calls. The /incoming-call route will return TwiML, Twilio's Markup Language https://www.twilio.com/docs/voice/twiml , to direct Twilio how to handle an incoming call. Paste this into index.js after the tool: Our TwiML is short: a