Build your own voice assistant and run it locally: Whisper + Ollama + Bark

After my latest post about how to build your own RAG and run it locally. Today, we're taking it a step further by not only implementing the conversational abilities of large language models but also adding listening and speaking capabilities. The idea is straightforward: we are going to create a voice assistant reminiscent of Jarvis or Friday from the iconic Iron Man movies, which can operate offline on your computer. Since this is an introductory tutorial, I will implement it in Python and keep it simple enough for beginners. Lastly, I will provide some guidance on how to scale the application.

Techstack

First, you should set up a virtual Python environment. You have several options for this, including pyenv, virtualenv, poetry, and others that serve a similar purpose. Personally, I'll use Poetry for this tutorial due to my personal preferences. Here are several crucial libraries you'll need to install:

  • rich: For a visually appealing console output.
  • openai-whisper: A robust tool for speech-to-text conversion.
  • suno-bark: A cutting-edge library for text-to-speech synthesis, ensuring high-quality audio output.
  • langchain: A straightforward library for interfacing with Large Language Models (LLMs).
  • sounddevice, pyaudio, and speechrecognition: Essential for audio recording and playback.

For a detailed list of dependencies, refer to the link here.

The most critical component here is the Large Language Model (LLM) backend, for which we will use Ollama. Ollama is widely recognized as a popular tool for running and serving LLMs offline. If Ollama is new to you, I recommend checking out my previous article on offline RAG: "Build Your Own RAG and Run It Locally: Langchain + Ollama + Streamlit". Basically, you just need to download the Ollama application, pull your preferred model, and run it.

Architecture

Okay, if everything has been set up, let's proceed to the next step. Below is the overall architecture of our application, which fundamentally comprises 3 main components:

  • Speech Recognition: Utilizing OpenAI's Whisper, we convert spoken language into text. Whisper's training on diverse datasets ensures its proficiency across various languages and dialects.
  • Conversational Chain: For the conversational capabilities, we'll employ the Langchain interface for the Llama-2 model, which is served using Ollama. This setup promises a seamless and engaging conversational flow.
  • Speech Synthesizer: The transformation of text to speech is achieved through Bark, a state-of-the-art model from Suno AI, renowned for its lifelike speech production.

The workflow is straightforward: record speech, transcribe to text, generate a response using an LLM, and vocalize the response using Bark.

Sequence diagram for voice assistant with Whisper, Ollama, and Bark.

Implementation

The implementation begins with crafting a TextToSpeechService based on Bark, incorporating methods for synthesizing speech from text and handling longer text inputs seamlessly as follow:

  • Initialization (__init__): The class takes an optional device parameter, which specifies the device to be used for the model (either cuda if a GPU is available, or cpu). It loads the Bark model and the corresponding processor from the suno/bark-small pre-trained model. You can also use the large version by specifying suno/bark for the model loader.
  • Synthesize (synthesize): This method takes a text input and a voice_preset parameter, which specifies the voice to be used for the synthesis. You can check out other voice_preset value here. It uses the processor to prepare the input text and the voice preset, and then generates the audio array using the model.generate() method. The generated audio array is converted to a NumPy array and the sample rate is returned along with the audio array.
  • Long-form Synthesize (long_form_synthesize): This method is used for synthesizing longer text inputs. It first tokenizes the input text into sentences using the nltk.sent_tokenize function. For each sentence, it calls the synthesize method to generate the audio array. It then concatenates the generated audio arrays, with a short silence (0.25 seconds) added between each sentence.

Now that we have the TextToSpeechService set up, we need to prepare the Ollama server for the large language model (LLM) serving. To do this, you'll need to follow these steps:

  • Pull the latest Llama-2 model: Run the following command to download the latest Llama-2 model from the Ollama repository: ollama pull llama2.
  • Start the Ollama server: If the server is not yet started, execute the following command to start it: ollama serve.

Once you've completed these steps, your application will be able to use the Ollama server and the Llama-2 model to generate responses to user input.

Next, we'll move to the main application logic. First, we need to initialize the following components:

  • Rich Console: We'll use the Rich library to create a better interactive console for the user within the terminal.
  • Whisper Speech-to-Text: We'll initialize a Whisper speech recognition model, which is a state-of-the-art open-source speech recognition system developed by OpenAI. We'll use the base English model (base.en) for transcribing user input.
  • Bark Text-to-Speech: We'll initialize a Bark text-to-speech synthesizer instance, which was implemented above.
  • Conversational Chain: We'll use the built-in ConversationalChain from the Langchain library, which provides a template for managing the conversational flow. We'll configure it to use the Llama-2 language model with the Ollama backend.

Now, let's define the necessary functions:

  • record_audio: This function runs in a separate thread to capture audio data from the user's microphone using the sounddevice.RawInputStream. The callback function is called whenever new audio data is available, and it puts the data into a data_queue for further processing.
  • transcribe: This function utilizes the Whisper instance to transcribe the audio data from the data_queue into text.
  • get_llm_response: This function feeds the current conversation context to the Llama-2 language model (via the Langchain ConversationalChain) and retrieves the generated text response.
  • play_audio: This function takes the audio waveform generated by the Bark text-to-speech engine and plays it back to the user using a sound playback library (e.g., sounddevice).

Then, we define the main application loop. The main application loop guides the user through the conversational interaction as follow:

  1. The user is prompted to press Enter to start recording their input.
  2. Once the user presses Enter, the record_audio function is called in a separate thread to capture the user's audio input.
  3. When the user presses Enter again to stop the recording, the audio data is transcribed using the transcribe function.
  4. The transcribed text is then passed to the get_llm_response function, which generates a response using the Llama-2 language model.
  5. The generated response is printed to the console and played back to the user using the play_audio function.

Result

Once everything is put down together, we can run the application as shown in the video above. The application runs quite slowly on my MacBook because the Bark model is large, even in its smaller version. Therefore, I have slightly sped up the video. For those with a CUDA-enabled computer, it might run faster. Here are the key features of our application:

  • Voice-based interaction: Users can start and stop recording their voice input, and the assistant responds by playing back the generated audio.
  • Conversational context: The assistant maintains the context of the conversation, enabling more coherent and relevant responses. The use of the Llama-2 language model allows the assistant to provide concise and focused responses.

For those aiming to elevate this application to a production-ready status, the following enhancements are recommended:

  • Performance Optimization: Incorporate optimized versions of the models, such as whisper.cpp, llama.cpp, and bark.cpp, which are designed to boost performance, especially on lower-end computers.
  • Customizable Bot Prompts: Implement a system that allows users to customize the bot's persona and prompt, enabling the creation of different types of assistants (e.g., personal, professional, or domain-specific).
  • Graphical User Interface (GUI): Develop a user-friendly GUI to enhance the overall user experience, making the application more accessible and visually appealing.
  • Multimodal Capabilities: Expand the application to support multimodal interactions, such as the ability to generate and display images, diagrams, or other visual content in addition to the voice-based responses.

Finally, we have completed our simple voice assistant application, full code can be found at: https://github.com/vndee/local-talking-llm. This combination of speech recognition, language modeling, and text-to-speech technologies demonstrates how we can build something that sounds difficult but can actually run on your computer. Let's enjoy coding, and don't forget to subscribe to my blog so you don't miss the latest in AI and programming articles.