paint-brush
Comment créer votre propre assistant vocal et l'exécuter localement à l'aide de Whisper + Ollama + Barkby@vndee
1,408
1,408

Comment créer votre propre assistant vocal et l'exécuter localement à l'aide de Whisper + Ollama + Bark

Duy Huynh13m2024/04/02
Read on Terminal Reader

Interaction vocale : les utilisateurs peuvent démarrer et arrêter l'enregistrement de leur saisie vocale, et l'assistant répond en lisant l'audio généré. Contexte conversationnel : l'assistant maintient le contexte de la conversation, permettant des réponses plus cohérentes et pertinentes. L'utilisation du modèle linguistique Llama-2 permet à l'assistant de fournir des réponses concises et ciblées.
featured image - Comment créer votre propre assistant vocal et l'exécuter localement à l'aide de Whisper + Ollama + Bark
Duy Huynh HackerNoon profile picture

Après mon dernier article sur la façon de créer votre propre RAG et de l'exécuter localement, nous allons aujourd'hui encore plus loin en implémentant non seulement les capacités conversationnelles de grands modèles de langage, mais également en ajoutant des capacités d'écoute et de parole. L'idée est simple : nous allons créer un assistant vocal rappelant Jarvis ou Friday des films emblématiques d'Iron Man, qui pourra fonctionner hors ligne sur votre ordinateur.


Puisqu'il s'agit d'un didacticiel d'introduction, je vais l'implémenter en Python et le garder assez simple pour les débutants. Enfin, je fournirai quelques conseils sur la façon de faire évoluer l'application.

Pile technologique

Tout d’abord, vous devez configurer un environnement Python virtuel. Vous disposez de plusieurs options pour cela, notamment pyenv, virtualenv, poésie et d'autres qui servent un objectif similaire. Personnellement, j'utiliserai Poésie pour ce tutoriel en raison de mes préférences personnelles. Voici plusieurs bibliothèques cruciales que vous devrez installer :


  • riche : Pour une sortie console visuellement attrayante.
  • openai-whisper : Un outil robuste pour la conversion parole-texte.
  • suno-bark : Une bibliothèque de pointe pour la synthèse texte-parole, garantissant une sortie audio de haute qualité.
  • langchain : Une bibliothèque simple pour s'interfacer avec les grands modèles de langage (LLM).
  • sounddevice , pyaudio et voicerecognition : Indispensable pour l'enregistrement et la lecture audio.


Pour une liste détaillée des dépendances, reportez-vous au lien ici .


Le composant le plus critique ici est le backend du Large Language Model (LLM), pour lequel nous utiliserons Ollama. Ollama est largement reconnu comme un outil populaire pour exécuter et servir des LLM hors ligne. Si Ollama est nouveau pour vous, je vous recommande de consulter mon article précédent sur RAG hors ligne : « Construisez votre propre RAG et exécutez-le localement : Langchain + Ollama + Streamlit. Fondamentalement, il vous suffit de télécharger l'application Ollama, d'extraire votre modèle préféré et de l'exécuter.

Architecture

Bon, si tout est configuré, passons à l'étape suivante. Ci-dessous l'architecture globale de notre application, qui comprend fondamentalement 3 composants principaux :


  • Reconnaissance vocale : en utilisant Whisper d'OpenAI , nous convertissons la langue parlée en texte. La formation de Whisper sur divers ensembles de données garantit sa maîtrise de diverses langues et dialectes.


  • Chaîne conversationnelle : pour les capacités conversationnelles, nous utiliserons l'interface Langchain pour le modèle Llama-2 , qui est servie à l'aide d'Ollama. Cette configuration promet un flux conversationnel fluide et engageant.


  • Synthétiseur vocal : La transformation du texte en parole est réalisée grâce à Bark , un modèle de pointe de Suno AI, réputé pour sa production vocale réaliste.


Le flux de travail est simple : enregistrer la parole, transcrire en texte, générer une réponse à l'aide d'un LLM et vocaliser la réponse à l'aide de Bark.

Diagramme de séquence pour assistant vocal avec Whisper, Ollama et Bark.

Mise en œuvre

L'implémentation commence par la création d'un TextToSpeechService basé sur Bark, incorporant des méthodes de synthèse de la parole à partir du texte et gérant de manière transparente les entrées de texte plus longues comme suit :

 import nltk import torch import warnings import numpy as np from transformers import AutoProcessor, BarkModel warnings.filterwarnings( "ignore", message="torch.nn.utils.weight_norm is deprecated in favor of torch.nn.utils.parametrizations.weight_norm.", ) class TextToSpeechService: def __init__(self, device: str = "cuda" if torch.cuda.is_available() else "cpu"): """ Initializes the TextToSpeechService class. Args: device (str, optional): The device to be used for the model, either "cuda" if a GPU is available or "cpu". Defaults to "cuda" if available, otherwise "cpu". """ self.device = device self.processor = AutoProcessor.from_pretrained("suno/bark-small") self.model = BarkModel.from_pretrained("suno/bark-small") self.model.to(self.device) def synthesize(self, text: str, voice_preset: str = "v2/en_speaker_1"): """ Synthesizes audio from the given text using the specified voice preset. Args: text (str): The input text to be synthesized. voice_preset (str, optional): The voice preset to be used for the synthesis. Defaults to "v2/en_speaker_1". Returns: tuple: A tuple containing the sample rate and the generated audio array. """ inputs = self.processor(text, voice_preset=voice_preset, return_tensors="pt") inputs = {k: v.to(self.device) for k, v in inputs.items()} with torch.no_grad(): audio_array = self.model.generate(**inputs, pad_token_id=10000) audio_array = audio_array.cpu().numpy().squeeze() sample_rate = self.model.generation_config.sample_rate return sample_rate, audio_array def long_form_synthesize(self, text: str, voice_preset: str = "v2/en_speaker_1"): """ Synthesizes audio from the given long-form text using the specified voice preset. Args: text (str): The input text to be synthesized. voice_preset (str, optional): The voice preset to be used for the synthesis. Defaults to "v2/en_speaker_1". Returns: tuple: A tuple containing the sample rate and the generated audio array. """ pieces = [] sentences = nltk.sent_tokenize(text) silence = np.zeros(int(0.25 * self.model.generation_config.sample_rate)) for sent in sentences: sample_rate, audio_array = self.synthesize(sent, voice_preset) pieces += [audio_array, silence.copy()] return self.model.generation_config.sample_rate, np.concatenate(pieces)
  • Initialisation ( __init__ ) : La classe prend un paramètre device facultatif, qui spécifie le périphérique à utiliser pour le modèle (soit cuda si un GPU est disponible, soit cpu ). Il charge le modèle Bark et le processeur correspondant à partir du modèle pré-entraîné suno/bark-small . Vous pouvez également utiliser la grande version en spécifiant suno/bark pour le chargeur de modèles.


  • Synthétiser ( synthesize ) : Cette méthode prend une saisie text et un paramètre voice_preset , qui spécifie la voix à utiliser pour la synthèse. Vous pouvez consulter d'autres valeurs voice_preset ici . Il utilise le processor pour préparer le texte d'entrée et le préréglage vocal, puis génère le tableau audio à l'aide de la méthode model.generate() . Le tableau audio généré est converti en tableau NumPy et la fréquence d'échantillonnage est renvoyée avec le tableau audio.


  • Synthèse de forme longue ( long_form_synthesize ) : Cette méthode est utilisée pour synthétiser des entrées de texte plus longues. Il convertit d'abord le texte saisi en phrases à l'aide de la fonction nltk.sent_tokenize . Pour chaque phrase, il appelle la méthode synthesize pour générer le tableau audio. Il concatène ensuite les tableaux audio générés, avec un court silence (0,25 seconde) ajouté entre chaque phrase.


Maintenant que TextToSpeechService est configuré, nous devons préparer le serveur Ollama pour le service LLM (Large Language Model). Pour ce faire, vous devrez suivre ces étapes :


  • Extrayez le dernier modèle Llama-2 : Exécutez la commande suivante pour télécharger le dernier modèle Llama-2 à partir du référentiel Ollama : ollama pull llama2 .


  • Démarrez le serveur Ollama : Si le serveur n'est pas encore démarré, exécutez la commande suivante pour le démarrer : ollama serve .


Une fois ces étapes terminées, votre application pourra utiliser le serveur Ollama et le modèle Llama-2 pour générer des réponses aux entrées de l'utilisateur.


Ensuite, nous passerons à la logique principale de l’application. Tout d’abord, nous devons initialiser les composants suivants :

  • Rich Console : Nous utiliserons la bibliothèque Rich pour créer une meilleure console interactive pour l'utilisateur au sein du terminal.


  • Whisper Speech-to-Text : Nous initialiserons un modèle de reconnaissance vocale Whisper qui est un système de reconnaissance vocale open source de pointe développé par OpenAI. Nous utiliserons le modèle anglais de base ( base.en ) pour transcrire les entrées de l'utilisateur.


  • Bark Text-to-Speech : Nous allons initialiser une instance de synthétiseur de synthèse vocale Bark, qui a été implémentée ci-dessus.


  • Chaîne conversationnelle : nous utiliserons la ConversationalChain intégrée de la bibliothèque Langchain qui fournit un modèle pour gérer le flux conversationnel. Nous allons le configurer pour utiliser le modèle de langage Llama-2 avec le backend Ollama.
 import time import threading import numpy as np import whisper import sounddevice as sd from queue import Queue from rich.console import Console from langchain.memory import ConversationBufferMemory from langchain.chains import ConversationChain from langchain.prompts import PromptTemplate from langchain_community.llms import Ollama from tts import TextToSpeechService console = Console() stt = whisper.load_model("base.en") tts = TextToSpeechService() template = """ You are a helpful and friendly AI assistant. You are polite, respectful, and aim to provide concise responses of less than 20 words. The conversation transcript is as follows: {history} And here is the user's follow-up: {input} Your response: """ PROMPT = PromptTemplate(input_variables=["history", "input"], template=template) chain = ConversationChain( prompt=PROMPT, verbose=False, memory=ConversationBufferMemory(ai_prefix="Assistant:"), llm=Ollama(), )

Maintenant, définissons les fonctions nécessaires :

  • record_audio : Cette fonction s'exécute dans un thread séparé pour capturer les données audio du microphone de l'utilisateur à l'aide de sounddevice.RawInputStream . La fonction de rappel est appelée chaque fois que de nouvelles données audio sont disponibles et place les données dans une data_queue pour un traitement ultérieur.


  • transcribe : Cette fonction utilise l'instance Whisper pour transcrire les données audio de la data_queue en texte.


  • get_llm_response : Cette fonction alimente le contexte de conversation actuel au modèle de langage Llama-2 (via Langchain ConversationalChain ) et récupère la réponse textuelle générée.


  • play_audio : Cette fonction prend la forme d'onde audio générée par le moteur de synthèse vocale Bark et la restitue à l'utilisateur à l'aide d'une bibliothèque de lecture sonore (par exemple, sounddevice ).
 def record_audio(stop_event, data_queue): """ Captures audio data from the user's microphone and adds it to a queue for further processing. Args: stop_event (threading.Event): An event that, when set, signals the function to stop recording. data_queue (queue.Queue): A queue to which the recorded audio data will be added. Returns: None """ def callback(indata, frames, time, status): if status: console.print(status) data_queue.put(bytes(indata)) with sd.RawInputStream( samplerate=16000, dtype="int16", channels=1, callback=callback ): while not stop_event.is_set(): time.sleep(0.1) def transcribe(audio_np: np.ndarray) -> str: """ Transcribes the given audio data using the Whisper speech recognition model. Args: audio_np (numpy.ndarray): The audio data to be transcribed. Returns: str: The transcribed text. """ result = stt.transcribe(audio_np, fp16=False) # Set fp16=True if using a GPU text = result["text"].strip() return text def get_llm_response(text: str) -> str: """ Generates a response to the given text using the Llama-2 language model. Args: text (str): The input text to be processed. Returns: str: The generated response. """ response = chain.predict(input=text) if response.startswith("Assistant:"): response = response[len("Assistant:") :].strip() return response def play_audio(sample_rate, audio_array): """ Plays the given audio data using the sounddevice library. Args: sample_rate (int): The sample rate of the audio data. audio_array (numpy.ndarray): The audio data to be played. Returns: None """ sd.play(audio_array, sample_rate) sd.wait()

Ensuite, nous définissons la boucle applicative principale. La boucle d'application principale guide l'utilisateur tout au long de l'interaction conversationnelle comme suit :


  1. L'utilisateur est invité à appuyer sur Entrée pour commencer à enregistrer sa saisie.


  2. Une fois que l'utilisateur appuie sur Entrée, la fonction record_audio est appelée dans un thread séparé pour capturer l'entrée audio de l'utilisateur.


  3. Lorsque l'utilisateur appuie à nouveau sur Entrée pour arrêter l'enregistrement, les données audio sont transcrites à l'aide de la fonction transcribe .


  4. Le texte transcrit est ensuite transmis à la fonction get_llm_response , qui génère une réponse en utilisant le modèle de langage Llama-2.


  5. La réponse générée est imprimée sur la console et restituée à l'utilisateur à l'aide de la fonction play_audio .

 if __name__ == "__main__": console.print("[cyan]Assistant started! Press Ctrl+C to exit.") try: while True: console.input( "Press Enter to start recording, then press Enter again to stop." ) data_queue = Queue() # type: ignore[var-annotated] stop_event = threading.Event() recording_thread = threading.Thread( target=record_audio, args=(stop_event, data_queue), ) recording_thread.start() input() stop_event.set() recording_thread.join() audio_data = b"".join(list(data_queue.queue)) audio_np = ( np.frombuffer(audio_data, dtype=np.int16).astype(np.float32) / 32768.0 ) if audio_np.size > 0: with console.status("Transcribing...", spinner="earth"): text = transcribe(audio_np) console.print(f"[yellow]You: {text}") with console.status("Generating response...", spinner="earth"): response = get_llm_response(text) sample_rate, audio_array = tts.long_form_synthesize(response) console.print(f"[cyan]Assistant: {response}") play_audio(sample_rate, audio_array) else: console.print( "[red]No audio recorded. Please ensure your microphone is working." ) except KeyboardInterrupt: console.print("\n[red]Exiting...") console.print("[blue]Session ended.")

Résultat

Une fois que tout est mis en place, nous pouvons exécuter l'application comme indiqué dans la vidéo ci-dessus. L'application s'exécute assez lentement sur mon MacBook car le modèle Bark est grand, même dans sa version plus petite. J'ai donc légèrement accéléré la vidéo. Pour ceux qui disposent d’un ordinateur compatible CUDA, il peut fonctionner plus rapidement. Voici les principales fonctionnalités de notre application :


  • Interaction vocale : les utilisateurs peuvent démarrer et arrêter l'enregistrement de leur saisie vocale, et l'assistant répond en lisant l'audio généré.


  • Contexte conversationnel : l'assistant maintient le contexte de la conversation, permettant des réponses plus cohérentes et pertinentes. L'utilisation du modèle linguistique Llama-2 permet à l'assistant de fournir des réponses concises et ciblées.


Pour ceux qui souhaitent élever cette application à un statut prêt pour la production, les améliorations suivantes sont recommandées :

  • Optimisation des performances : intégrez des versions optimisées des modèles, telles que murmure.cpp, llama.cpp et bark.cpp, qui sont conçues pour améliorer les performances, en particulier sur les ordinateurs bas de gamme.


  • Invites de robot personnalisables : implémentez un système qui permet aux utilisateurs de personnaliser le personnage et l'invite du robot, permettant la création de différents types d'assistants (par exemple, personnels, professionnels ou spécifiques à un domaine).


  • Interface utilisateur graphique (GUI) : Développer une interface graphique conviviale pour améliorer l'expérience utilisateur globale, rendant l'application plus accessible et visuellement attrayante.


  • Capacités multimodales : développez l'application pour prendre en charge les interactions multimodales, telles que la possibilité de générer et d'afficher des images, des diagrammes ou d'autres contenus visuels en plus des réponses vocales.


Enfin, nous avons terminé notre application d'assistant vocal simple, le code complet peut être trouvé sur : https://github.com/vndee/local-talking-llm . Cette combinaison de technologies de reconnaissance vocale, de modélisation du langage et de synthèse vocale démontre comment nous pouvons créer quelque chose qui semble difficile mais qui peut réellement fonctionner sur votre ordinateur. Amusons-nous à coder et n'oubliez pas de vous abonner à mon blog pour ne pas manquer les derniers articles sur l'IA et la programmation.


Également publié ici