paint-brush
So erstellen Sie Ihren eigenen Sprachassistenten und führen ihn lokal mit Whisper + Ollama + Bark ausvon@vndee
999 Lesungen
999 Lesungen

So erstellen Sie Ihren eigenen Sprachassistenten und führen ihn lokal mit Whisper + Ollama + Bark aus

von Duy Huynh13m2024/04/02
Read on Terminal Reader

Zu lang; Lesen

Sprachbasierte Interaktion: Benutzer können die Aufzeichnung ihrer Spracheingabe starten und stoppen, und der Assistent reagiert mit der Wiedergabe des generierten Audios. Konversationskontext: Der Assistent behält den Kontext des Gesprächs bei und ermöglicht so kohärentere und relevantere Antworten. Die Verwendung des Llama-2-Sprachmodells ermöglicht es dem Assistenten, prägnante und zielgerichtete Antworten zu geben.
featured image - So erstellen Sie Ihren eigenen Sprachassistenten und führen ihn lokal mit Whisper + Ollama + Bark aus
Duy Huynh HackerNoon profile picture

Nach meinem letzten Beitrag darüber, wie Sie Ihre eigene RAG erstellen und lokal ausführen, gehen wir heute noch einen Schritt weiter, indem wir nicht nur die Konversationsfähigkeiten großer Sprachmodelle implementieren, sondern auch Hör- und Sprechfunktionen hinzufügen. Die Idee ist einfach: Wir werden einen Sprachassistenten entwickeln, der an Jarvis oder Friday aus den legendären Iron Man-Filmen erinnert und offline auf Ihrem Computer arbeiten kann.


Da es sich um ein Einführungstutorial handelt, werde ich es in Python implementieren und es für Anfänger einfach genug halten. Abschließend werde ich einige Hinweise zur Skalierung der Anwendung geben.

Techstack

Zunächst sollten Sie eine virtuelle Python-Umgebung einrichten. Hierfür stehen Ihnen mehrere Optionen zur Verfügung, darunter Pyenv, Virtualenv, Poetry und andere, die einem ähnlichen Zweck dienen. Aufgrund meiner persönlichen Vorlieben werde ich für dieses Tutorial Poesie verwenden. Hier sind einige wichtige Bibliotheken, die Sie installieren müssen:


  • rich : Für eine optisch ansprechende Konsolenausgabe.
  • openai-whisper : Ein robustes Tool für die Konvertierung von Sprache in Text.
  • suno-bark : Eine hochmoderne Bibliothek für die Text-zu-Sprache-Synthese, die eine hochwertige Audioausgabe gewährleistet.
  • langchain : Eine unkomplizierte Bibliothek für die Anbindung an Large Language Models (LLMs).
  • Soundgerät , Pyaudio und Spracherkennung : Unverzichtbar für die Audioaufnahme und -wiedergabe.


Eine detaillierte Liste der Abhängigkeiten finden Sie unter dem Link hier .


Die wichtigste Komponente hierbei ist das Large Language Model (LLM)-Backend, für das wir Ollama verwenden werden. Ollama ist weithin als beliebtes Tool für die Offline-Ausführung und Bereitstellung von LLMs anerkannt. Wenn Ollama für Sie neu ist, empfehle ich Ihnen, meinen vorherigen Artikel über Offline-RAG zu lesen: „Erstellen Sie Ihr eigenes RAG und führen Sie es lokal aus: Langchain + Ollama + Streamlit.“ Im Grunde müssen Sie nur die Ollama-Anwendung herunterladen, Ihr bevorzugtes Modell abrufen und ausführen.

Die Architektur

Okay, wenn alles eingerichtet ist, fahren wir mit dem nächsten Schritt fort. Nachfolgend finden Sie die Gesamtarchitektur unserer Anwendung, die im Wesentlichen aus drei Hauptkomponenten besteht:


  • Spracherkennung : Mithilfe von Whisper von OpenAI wandeln wir gesprochene Sprache in Text um. Die Schulung von Whisper anhand verschiedener Datensätze gewährleistet die Beherrschung verschiedener Sprachen und Dialekte.


  • Konversationskette : Für die Konversationsfunktionen verwenden wir die Langchain-Schnittstelle für das Llama-2- Modell, das mit Ollama bedient wird. Dieses Setup verspricht einen nahtlosen und ansprechenden Gesprächsfluss.


  • Sprachsynthesizer : Die Umwandlung von Text in Sprache wird durch Bark erreicht, ein hochmodernes Modell von Suno AI, das für seine lebensechte Sprachproduktion bekannt ist.


Der Arbeitsablauf ist unkompliziert: Sprache aufzeichnen, in Text transkribieren, mit einem LLM eine Antwort generieren und die Antwort mit Bark vokalisieren.

Sequenzdiagramm für Sprachassistenten mit Whisper, Ollama und Bark.

Implementierung

Die Implementierung beginnt mit der Erstellung eines TextToSpeechService auf Basis von Bark, der Methoden zum Synthetisieren von Sprache aus Text und zur nahtlosen Verarbeitung längerer Texteingaben wie folgt enthält:

 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)
  • Initialisierung ( __init__ ) : Die Klasse akzeptiert einen optionalen device , der das für das Modell zu verwendende Gerät angibt (entweder cuda , wenn eine GPU verfügbar ist, oder cpu ). Es lädt das Bark-Modell und den entsprechenden Prozessor aus dem vorab trainierten suno/bark-small Modell. Sie können auch die große Version verwenden, indem Sie suno/bark für den Modelllader angeben.


  • Synthetisieren ( synthesize ) : Diese Methode benötigt eine text und einen voice_preset Parameter, der die für die Synthese zu verwendende Stimme angibt. Sie können sich hier andere voice_preset Werte ansehen. Es verwendet den processor um den Eingabetext und die Sprachvoreinstellung vorzubereiten, und generiert dann das Audio-Array mithilfe der Methode model.generate() . Das generierte Audio-Array wird in ein NumPy-Array konvertiert und die Abtastrate wird zusammen mit dem Audio-Array zurückgegeben.


  • Long-form Synthesize ( long_form_synthesize ) : Diese Methode wird zum Synthetisieren längerer Texteingaben verwendet. Zunächst wird der Eingabetext mit der Funktion nltk.sent_tokenize in Sätze tokenisiert. Für jeden Satz wird die synthesize aufgerufen, um das Audio-Array zu generieren. Anschließend werden die generierten Audio-Arrays verkettet, wobei zwischen jedem Satz eine kurze Pause (0,25 Sekunden) eingefügt wird.


Nachdem wir nun den TextToSpeechService eingerichtet haben, müssen wir den Ollama-Server für die Bereitstellung des Large Language Model (LLM) vorbereiten. Dazu müssen Sie die folgenden Schritte ausführen:


  • Rufen Sie das neueste Llama-2-Modell ab : Führen Sie den folgenden Befehl aus, um das neueste Llama-2-Modell aus dem Ollama-Repository herunterzuladen: ollama pull llama2 .


  • Starten Sie den Ollama-Server : Wenn der Server noch nicht gestartet ist, führen Sie den folgenden Befehl aus, um ihn zu starten: ollama serve .


Sobald Sie diese Schritte abgeschlossen haben, kann Ihre Anwendung den Ollama-Server und das Llama-2-Modell verwenden, um Antworten auf Benutzereingaben zu generieren.


Als Nächstes wenden wir uns der Hauptanwendungslogik zu. Zuerst müssen wir die folgenden Komponenten initialisieren:

  • Rich Console : Wir verwenden die Rich-Bibliothek, um eine bessere interaktive Konsole für den Benutzer im Terminal zu erstellen.


  • Whisper Speech-to-Text : Wir initialisieren ein Whisper-Spracherkennungsmodell, ein hochmodernes Open-Source-Spracherkennungssystem, das von OpenAI entwickelt wurde. Wir verwenden das englische Basismodell ( base.en ) zum Transkribieren von Benutzereingaben.


  • Bark Text-to-Speech : Wir werden eine Bark-Text-to-Speech-Synthesizer-Instanz initialisieren, die oben implementiert wurde.


  • Konversationskette : Wir verwenden die integrierte ConversationalChain aus der Langchain-Bibliothek, die eine Vorlage für die Verwaltung des Konversationsflusses bereitstellt. Wir konfigurieren es für die Verwendung des Llama-2-Sprachmodells mit dem Ollama-Backend.
 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(), )

Definieren wir nun die notwendigen Funktionen:

  • record_audio : Diese Funktion wird in einem separaten Thread ausgeführt, um Audiodaten vom Mikrofon des Benutzers mithilfe von sounddevice.RawInputStream zu erfassen. Die Callback-Funktion wird immer dann aufgerufen, wenn neue Audiodaten verfügbar sind, und stellt die Daten zur weiteren Verarbeitung in eine data_queue .


  • transcribe : Diese Funktion nutzt die Whisper-Instanz, um die Audiodaten aus der data_queue in Text zu transkribieren.


  • get_llm_response : Diese Funktion speist den aktuellen Konversationskontext in das Llama-2-Sprachmodell ein (über die Langchain ConversationalChain ) und ruft die generierte Textantwort ab.


  • play_audio : Diese Funktion nimmt die von der Bark-Text-zu-Sprache-Engine generierte Audiowellenform und spielt sie mithilfe einer Soundwiedergabebibliothek (z. B. sounddevice ) für den Benutzer ab.
 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()

Dann definieren wir die Hauptanwendungsschleife. Die Hauptanwendungsschleife führt den Benutzer wie folgt durch die Konversationsinteraktion:


  1. Der Benutzer wird aufgefordert, die Eingabetaste zu drücken, um mit der Aufzeichnung seiner Eingabe zu beginnen.


  2. Sobald der Benutzer die Eingabetaste drückt, wird die Funktion record_audio in einem separaten Thread aufgerufen, um die Audioeingabe des Benutzers zu erfassen.


  3. Wenn der Benutzer erneut die Eingabetaste drückt, um die Aufnahme zu stoppen, werden die Audiodaten mithilfe der transcribe transkribiert.


  4. Der transkribierte Text wird dann an die Funktion get_llm_response übergeben, die mithilfe des Llama-2-Sprachmodells eine Antwort generiert.


  5. Die generierte Antwort wird auf der Konsole gedruckt und dem Benutzer mithilfe der Funktion play_audio vorgespielt.

 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.")

Ergebnis

Sobald alles zusammengestellt ist, können wir die Anwendung wie im Video oben gezeigt ausführen. Die Anwendung läuft auf meinem MacBook recht langsam, da das Bark-Modell selbst in seiner kleineren Version groß ist. Deshalb habe ich das Video etwas beschleunigt. Für diejenigen mit einem CUDA-fähigen Computer läuft es möglicherweise schneller. Hier sind die Hauptmerkmale unserer Anwendung:


  • Sprachbasierte Interaktion : Benutzer können die Aufzeichnung ihrer Spracheingabe starten und stoppen, und der Assistent reagiert mit der Wiedergabe des generierten Audios.


  • Konversationskontext: Der Assistent behält den Kontext des Gesprächs bei und ermöglicht so kohärentere und relevantere Antworten. Die Verwendung des Llama-2-Sprachmodells ermöglicht es dem Assistenten, prägnante und zielgerichtete Antworten zu geben.


Für diejenigen, die diese Anwendung auf einen produktionsbereiten Status bringen möchten, werden die folgenden Verbesserungen empfohlen:

  • Leistungsoptimierung : Integrieren Sie optimierte Versionen der Modelle wie whisper.cpp, llama.cpp und bark.cpp, die die Leistung insbesondere auf Computern der unteren Preisklasse steigern sollen.


  • Anpassbare Bot-Eingabeaufforderungen : Implementieren Sie ein System, das es Benutzern ermöglicht, die Persona und Eingabeaufforderung des Bots anzupassen und so die Erstellung verschiedener Arten von Assistenten (z. B. persönlich, beruflich oder domänenspezifisch) zu ermöglichen.


  • Grafische Benutzeroberfläche (GUI) : Entwickeln Sie eine benutzerfreundliche GUI, um das gesamte Benutzererlebnis zu verbessern und die Anwendung zugänglicher und optisch ansprechender zu machen.


  • Multimodale Funktionen : Erweitern Sie die Anwendung, um multimodale Interaktionen zu unterstützen, z. B. die Möglichkeit, zusätzlich zu den sprachbasierten Antworten Bilder, Diagramme oder andere visuelle Inhalte zu generieren und anzuzeigen.


Endlich haben wir unsere einfache Sprachassistentenanwendung fertiggestellt. Den vollständigen Code finden Sie unter: https://github.com/vndee/local-talking-llm . Diese Kombination aus Spracherkennung, Sprachmodellierung und Text-to-Speech-Technologien zeigt, wie wir etwas erstellen können, das schwierig klingt, aber tatsächlich auf Ihrem Computer ausgeführt werden kann. Viel Spaß beim Programmieren und vergessen Sie nicht , meinen Blog zu abonnieren, damit Sie die neuesten Artikel zu KI und Programmierung nicht verpassen.


Auch hier veröffentlicht