Während OpenAI die Veröffentlichung der erweiterten Sprachmodi für ChatGPT verzögert , möchte ich mitteilen, wie wir unsere LLM-Sprachanwendung erstellt und in einen interaktiven Stand integriert haben.
Ende Februar fand auf Bali das Lampu- Festival statt, das nach den Prinzipien des berühmten Burning Man veranstaltet wurde. Der Tradition entsprechend erstellen die Teilnehmer ihre eigenen Installationen und Kunstobjekte.
Inspiriert von der Idee katholischer Beichtstühle und den Möglichkeiten der aktuellen LLMs kamen meine Freunde vom Camp 19:19 und ich auf die Idee, unseren eigenen KI-Beichtstuhl zu bauen, in dem jeder mit einer künstlichen Intelligenz sprechen kann.
So haben wir uns das am Anfang vorgestellt:
Um das Konzept zu testen und mit einer Eingabeaufforderung für das LLM zu experimentieren, habe ich an einem Abend eine naive Implementierung erstellt:
Um diese Demo zu implementieren, habe ich mich vollständig auf die Cloud-Modelle von OpenAI verlassen: Whisper , GPT-4 und TTS . Dank der hervorragenden Bibliothek speech_recognition konnte ich die Demo in nur wenigen Dutzend Codezeilen erstellen.
import os import asyncio from dotenv import load_dotenv from io import BytesIO from openai import AsyncOpenAI from soundfile import SoundFile import sounddevice as sd import speech_recognition as sr load_dotenv() aiclient = AsyncOpenAI( api_key=os.environ.get("OPENAI_API_KEY") ) SYSTEM_PROMPT = """ You are helpfull assistant. """ async def listen_mic(recognizer: sr.Recognizer, microphone: sr.Microphone): audio_data = recognizer.listen(microphone) wav_data = BytesIO(audio_data.get_wav_data()) wav_data.name = "SpeechRecognition_audio.wav" return wav_data async def say(text: str): res = await aiclient.audio.speech.create( model="tts-1", voice="alloy", response_format="opus", input=text ) buffer = BytesIO() for chunk in res.iter_bytes(chunk_size=4096): buffer.write(chunk) buffer.seek(0) with SoundFile(buffer, 'r') as sound_file: data = sound_file.read(dtype='int16') sd.play(data, sound_file.samplerate) sd.wait() async def respond(text: str, history): history.append({"role": "user", "content": text}) completion = await aiclient.chat.completions.create( model="gpt-4", temperature=0.5, messages=history, ) response = completion.choices[0].message.content await say(response) history.append({"role": "assistant", "content": response}) async def main() -> None: m = sr.Microphone() r = sr.Recognizer() messages = [{"role": "system", "content": SYSTEM_PROMPT}] with m as source: r.adjust_for_ambient_noise(source) while True: wav_data = await listen_mic(r, source) transcript = await aiclient.audio.transcriptions.create( model="whisper-1", temperature=0.5, file=wav_data, response_format="verbose_json", ) if transcript.text == '' or transcript.text is None: continue await respond(transcript.text, messages) if __name__ == '__main__': asyncio.run(main())
Die Probleme, die wir lösen mussten, wurden bereits nach den ersten Tests dieser Demo deutlich:
Wir hatten die Wahl, diese Probleme zu lösen: durch die Suche nach einer geeigneten technischen oder Produktlösung.
Bevor wir überhaupt mit dem Code begannen, mussten wir entscheiden, wie der Benutzer mit dem Stand interagieren würde:
Um einen neuen Benutzer in der Kabine zu erkennen, haben wir mehrere Optionen in Betracht gezogen: Türöffnungssensoren, Bodengewichtssensoren, Abstandssensoren und ein Kamera-+YOLO-Modell. Der Abstandssensor hinter der Rückseite erschien uns am zuverlässigsten, da er versehentliche Auslöser ausschloss, etwa wenn die Tür nicht fest genug geschlossen ist, und im Gegensatz zum Gewichtssensor keine komplizierte Installation erforderte.
Um die Herausforderung zu vermeiden, den Anfang und das Ende eines Dialogs zu erkennen, haben wir beschlossen, einen großen roten Knopf zur Steuerung des Mikrofons hinzuzufügen. Diese Lösung ermöglicht es dem Benutzer auch, die KI jederzeit zu unterbrechen.
Zur Implementierung einer Rückmeldung bei der Bearbeitung einer Anfrage hatten wir viele verschiedene Ideen. Wir entschieden uns für eine Variante mit einem Bildschirm, der anzeigt, was das System gerade macht: das Abhören des Mikrofons, die Bearbeitung einer Frage oder eine Antwort.
Wir haben auch eine ziemlich clevere Option mit einem alten Festnetztelefon in Betracht gezogen. Die Sitzung würde beginnen, wenn der Benutzer den Hörer abnimmt, und das System würde dem Benutzer zuhören, bis er auflegt. Wir haben jedoch entschieden, dass es authentischer ist, wenn der Benutzer von der Kabine „beantwortet“ wird, als von einer Stimme aus dem Telefon.
Am Ende sah der endgültige Benutzerfluss wie folgt aus:
Arduino überwacht den Zustand des Abstandssensors und des roten Knopfs. Es sendet alle Änderungen über die HTTP-API an unser Backend, wodurch das System feststellen kann, ob der Benutzer die Kabine betreten oder verlassen hat und ob es erforderlich ist, das Abhören des Mikrofons zu aktivieren oder mit der Generierung einer Antwort zu beginnen.
Die Web-Benutzeroberfläche ist lediglich eine in einem Browser geöffnete Webseite, die kontinuierlich den aktuellen Status des Systems vom Backend empfängt und dem Benutzer anzeigt.
Das Backend steuert das Mikrofon, interagiert mit allen erforderlichen KI-Modellen und spricht die LLM-Antworten aus. Es enthält die Kernlogik der App.
Wie man einen Sketch für Arduino codiert, den Abstandssensor und den Knopf richtig anschließt und alles in der Kabine zusammenbaut, ist ein Thema für einen separaten Artikel. Lassen Sie uns kurz zusammenfassen, was wir bekommen haben, ohne auf technische Details einzugehen.
Wir verwendeten einen Arduino, genauer gesagt das Modell ESP32 mit eingebautem WLAN-Modul. Der Mikrocontroller war mit demselben WLAN-Netzwerk verbunden wie der Laptop, auf dem das Backend lief.
Vollständige Liste der von uns verwendeten Hardware:
Die Hauptkomponenten der Pipeline sind Speech-To-Text (STT), LLM und Text-To-Speech (TTS). Für jede Aufgabe stehen viele verschiedene Modelle sowohl lokal als auch über die Cloud zur Verfügung.
Da wir keinen leistungsstarken Grafikprozessor zur Verfügung hatten, entschieden wir uns für Cloud-basierte Versionen der Modelle. Die Schwäche dieses Ansatzes ist die Notwendigkeit einer guten Internetverbindung. Dennoch war die Interaktionsgeschwindigkeit nach allen Optimierungen akzeptabel, selbst mit dem mobilen Internet, das wir auf dem Festival hatten.
Sehen wir uns nun die einzelnen Komponenten der Pipeline genauer an.
Viele moderne Geräte unterstützen die Spracherkennung schon seit langem. Beispielsweise ist die Apple Speech API für iOS und macOS verfügbar, die Web Speech API für Browser.
Leider sind sie qualitativ viel schlechter als Whisper oder Deepgram und können die Sprache nicht automatisch erkennen.
Um die Verarbeitungszeit zu verkürzen, ist die beste Option die Spracherkennung in Echtzeit, während der Benutzer spricht. Hier sind einige Projekte mit Beispielen für die Implementierung:
Bei unserem Laptop erwies sich die Geschwindigkeit der Spracherkennung mit diesem Ansatz als weit von Echtzeit entfernt. Nach mehreren Experimenten entschieden wir uns für das cloudbasierte Whisper-Modell von OpenAI.
Das Ergebnis des Speech-to-Text-Modells aus dem vorherigen Schritt ist der Text, den wir mit dem Dialogverlauf an das LLM senden.
Bei der Auswahl eines LLM verglichen wir GPT-3.5, GPT-4 und Claude. Es stellte sich heraus, dass der entscheidende Faktor nicht so sehr das spezifische Modell war, sondern vielmehr seine Konfiguration. Letztendlich entschieden wir uns für GPT-4, dessen Antworten uns besser gefielen als die der anderen.
Die Anpassung der Eingabeaufforderung für LLM-Modelle ist zu einer eigenen Kunstform geworden. Im Internet gibt es viele Anleitungen, wie Sie Ihr Modell nach Bedarf anpassen können:
Wir mussten ausgiebig mit den Eingabeaufforderungs- und Temperatureinstellungen experimentieren, um eine ansprechende, präzise und humorvolle Reaktion des Modells zu erzielen.
Wir sprechen die vom LLM erhaltene Antwort mithilfe des Text-to-Speech-Modells aus und spielen sie dem Benutzer vor. Dieser Schritt war die Hauptursache für Verzögerungen in unserer Demo.
LLMs brauchen ziemlich lange, um zu antworten. Sie unterstützen jedoch die Antwortgenerierung im Streaming-Modus – Token für Token. Wir können diese Funktion nutzen, um die Wartezeit zu optimieren, indem wir einzelne Phrasen aussprechen, sobald sie empfangen werden, ohne auf eine vollständige Antwort vom LLM zu warten.
Wir nutzen die Zeit, während der Benutzer das erste Fragment abhört, um die Verzögerung bei der Verarbeitung der restlichen Teile der Antwort vor dem LLM zu verbergen. Dank dieses Ansatzes tritt die Antwortverzögerung nur am Anfang auf und beträgt ~3 Sekunden.
async generateResponse(history) { const completion = await this.ai.completion(history); const chunks = new DialogChunks(); for await (const chunk of completion) { const delta = chunk.choices[0]?.delta?.content; if (delta) { chunks.push(delta); if (chunks.hasCompleteSentence()) { const sentence = chunks.popSentence(); this.voice.ttsAndPlay(sentence); } } } const sentence = chunks.popSentence(); if (sentence) { this.voice.say(sentence); } return chunks.text; }
Selbst mit all unseren Optimierungen ist eine Verzögerung von 3-4 Sekunden immer noch erheblich. Wir haben uns entschieden, die Benutzeroberfläche mit Feedback zu versehen, um dem Benutzer das Gefühl zu ersparen, dass die Antwort hängen bleibt. Wir haben uns mehrere Ansätze angesehen:
Wir haben uns für die letzte Option mit einer einfachen Webseite entschieden, die das Backend abfragt und entsprechend dem aktuellen Status Animationen anzeigt.
Unser KI-Beichtraum lief vier Tage lang und zog Hunderte von Teilnehmern an. Wir haben nur etwa 50 US-Dollar für OpenAI-APIs ausgegeben. Im Gegenzug erhielten wir viel positives Feedback und wertvolle Eindrücke.
Dieses kleine Experiment hat gezeigt, dass es auch mit begrenzten Ressourcen und herausfordernden äußeren Bedingungen möglich ist, einem LLM eine intuitive und effiziente Sprachschnittstelle hinzuzufügen.
Übrigens, die Backend-Quellen sind auf GitHub verfügbar