Mittwoch, 19. Juli 2023

Ein selbstgebauter Chat GPT Sprachassistent

Ich möchte mir meinen eigenen Sprachassistenten bauen, ohne Mithörfunktion wie z.B. bei Amazon Alexa und dennoch unterstützt durch künstliche Intelligenz.

Das Funktionsprinzip ist einfach. Der Sprachassistent soll eine Nachricht aufnehmen, diese dann in Text transkribieren, den Text an Chat GPT schicken und die Antwort vorlesen. Chat GPT-4 kann zwar bereits Bildinformationen verarbeiten, aber immer noch keine Sprache, und das in der kostenlosen Testphase verfügbare Model 'turbo 3.5' erwartet ohnehin nur Text basierte Eingaben.

Das Transkribieren der Sprachnachricht soll 'AWS Transcibe' erledigen. Für diesen professionellen Webservice von Amazon fallen allerdings Kosten an. Im Rahmen des kostenlosen AWS Kontingents können 60 Minuten pro Monat für 12 Monate übersetzt werden. Anschließend kostet der Dienst für die ersten 250k Minuten in der Region Europa (Frankfurt) 2,4 USD cent pro Minute. Das Vorlesen der Nachrichten soll ebenfalls mit einem Amazon Dienst namens 'AWS Polly' geschehen. Das kostenlose Kontingent umfasst hier für Standardstimmen 5 Millionen Zeichen pro Monat für 12 Monate. Dann kosten 1000 Zeichen ungefähr 0,4 USD cent. Für die Nutzung beider Dienste muss ein AWS Konto erstellt werden, und  eine 'AWS_ACCESS_KEY_ID' sowie ein 'AWS_SECRET_ACCESS_KEY' generiert werden. Damit die  Schlüssel später im AWS Python API verwendet werden können, empfiehlt es sich diese mit 'export' als Shell Umgebungsvariablen zu definieren oder dauerhaft in '/etc/profile' oder '/etc/bash.bashrc' zu speichern.

Auch für den Zugang zu Chat GPT muss ein Open AI Schlüssel generiert, und ein Konto erstellt werden. OpenAI bietet Entwicklern 3 Monate kostenlosen Zugang zu Chat GPT. Danach kostet eine Eingabe für 'GPT-3.5 Turbo' im '4k context' 0,15 UScent pro 1k Token, und eine Ausgabe 0,2 UScent pro 1k Token. Dabei entsprechen 1000 Token ungefähr 750 Wörtern.

All die Dienste sollen auf einem Kleincomputer mit Raspberry Pi OS laufen. Ich habe  noch einen 'Zero W' in der Bastelkiste gefunden.

Um die OpenAI API zu installieren, genügt ein einfaches 'pip install openai'. Für 'AWS Transcribe' muss das Python API mit 'python3 -m pip install amazon-transcribe' installiert werden, und für 'AWS Polly' Boto3 für Python mit 'pip install boto3'.

Leider verfügt der 'Zero W' aber nicht über Audio Ein- oder Ausgänge. Ich habe daher eine kleine externe USB Soundkarte erworben.

Im Netz finden sich zahlreiche Anleitungen um diese zu installieren.

Als Mikrofon soll eine Elektret Kapsel dienen. Auf die Membran dieses Kondensatormikrofons ist ein Elektret aufgetragen, also ein isolierendes Material, das quasi-permanent gespeicherte elektrische Ladungen enthält. Diese Ladung dient als 'Vorspannung' und schaltet einen Sperrschicht-Feldeffekttransistor. Unten ist die Anordnung in Source Schaltung gezeigt.

Bei der von mir verwendeten Soundkarte ist der Mikrofoneingang als 3 polige Klinkenbuchse ausgelegt. Ein AUX Eingang auf einem 4-ten Ring fehlt. L- und R-Kanal sind verbunden und liegen auf 2,8 V gegen GND. Das Elektret Mikrofon kann daher direkt an den Mikrofoneingang angeschlossen werden.

Unten ist die Schaltung im fliegenden Aufbau zu sehen.

Neben dem Elektret Mikrofon und einem Lautsprecher, sind auch ein Schalter mit Hardware Entprellung sowie zwei Status LEDs zu sehen. Leuchtet die grüne LED ist das System bereit, und es können Anfragen gestellt werden. Leuchtet die rote LED arbeitet das System, und der Nutzer muss warten.

Grundsätzlich wird eine Sprachnachricht aufgenommen solange der grüne Taster gedrückt ist. Da das Transkribieren der Nachricht asynchron erfolgt, die Datenpakete also gesendet werden ohne das transkribiertes Ergebnis des vorhergehenden Pakets abzuwarten, ist ein aufwändiges Eventhandling nötig. Ich habe mich daher gegen zusätzliche Interrupts durch eine softwareseitige Entprellung und softwareseitige Abfrage der Tasterzustands entschieden.

Die Schaltung für die Hardware Entprellung ist unten zu sehen.

Für 3,3V CMOS Logik liegt die 'input LOW' Grenze bei 0,8 V, und die 'input HIGH' Grenze liegt bei 2,0 V.

Wird der Taster gedrückt entlädt sich der 1µ F Kondensator über den 27k Ohm Widerstand. Für die Spannung am Kondensator gilt

Die Zeitkonstante tau ist durch 'Rc x C' definiert. Nach ca. 0.5 tau hier 13 ms ist der Kondensator zu ca. 60% entladen und die 'input HIGH' Grenze wird unterschritten. Kürzere Pulse werden nicht detektiert. Nach ca. 50 ms ist die 'INPUT LOW' Grenze sicher unterschritten. Im Python Code wird der Tasterzustand dann nochmal abgefragt. Hat sich der Zustand des Tasters nicht verändert wird der entsprechende Code ausgeführt.

Wird der Taster losgelassen lädt sich der Kondensator über den 27k Ohm und den 3,3k Ohm Widerstand auf. Für die Spannung am Kondensator gilt

Nach ca. 0.3 tau hier 10ms ist die 'input LOW' Grenze überschritten und nach ca. 1 tau also 30 ms die 'input HIGH' Grenze. Letztlich habe ich die Taste also wie unten zu sehen um zwei Widerstände und einem Kondensator ergänzt.

Der Anschluss der Status LEDs gestaltet sich demgegenüber recht einfach. Um einen Stromfluss von ca. 10 mA zu gewährleisten habe ich lediglich 330 Ohm Widerstände vorgeschaltet. 

Doch nun zum Herzstück des Sprachassistenten. Der Software...

 Im Phyton Skript werden zunächst die 'input' und 'output pins' des Zero W festgelegt.

import RPi.GPIO as GPIO

# Zählweise der Pins festlegen
GPIO.setmode(GPIO.BOARD)
# Warnungen ausschalten
GPIO.setwarnings(False)
# Pin 8 (GPIO 14) als Eingang festlegen, keinen internen pull up/down Widerstand setzen
GPIO.setup(8, GPIO.IN)
# Pin 32 aus Ausgang für grüne LED festlegen
GPIO.setup(32, GPIO.OUT)
GPIO.output(32, GPIO.LOW)
# Pin 36 aus Ausgang für rote LED festlegen
GPIO.setup(36, GPIO.OUT)
GPIO.output(36, GPIO.LOW)
# globale Flanken Variable EDGE definieren
EDGE=False

Dann werden die benötigten Bibliotheken importiert.

import os
import time
import asyncio
# Für AWS Polly ggf. AWS SDK Boto3 für Python mit 'pip install boto3' instalieren
import boto3
# Für ChatGPT ggf. OpenAI API mit 'pip install openai' installieren
import openai
# Für AWS Transcribe ggf. 'aiofile' mit `pip install aiofile` für asysncrone Dateizugriffe installieren
import aiofile

# Für AWS Transcribe ggf. auch Phyton SDK für AWS Transcribe mit 'python3 -m pip install amazon-transcribe' installieren
from amazon_transcribe.client import TranscribeStreamingClient
from amazon_transcribe.handlers import TranscriptResultStreamHandler
from amazon_transcribe.model import TranscriptEvent
from amazon_transcribe.utils import apply_realtime_delay

# Für ChatGPT OpenAI Konto erstellen und Schlüssel unter 'https://platform.openai.com/account/api-keys' generieren
openai.api_key = "MyKeyMyKeyMyKey"

# Für AWS Transcribe und AWS Polly AWS Konto auf https://aws.amazon.com/de/ erstellen und Schlüssel generieren 
# Ggf. mit 'export' AWS_ACCESS_KEY_ID und AWS_SECRET_ACCESS_KEY als Shell Umgebungsvariable definieren oder dauerhaft in /etc/profile oder /etc/bash.bashrc speichern

Anschließend werden Einstellungen für das asynchrone Transkribieren festgelegt, Event Handler definiert und instanziert, und schließlich bevor das Hauptprogramm startet, eine AWS Polly Sitzung gestartet.

# Settings für asynchrone Transkription
SAMPLE_RATE = 44100
BYTES_PER_SAMPLE = 2
CHANNEL_NUMS = 1
AUDIO_PATH = "/home/pi/record.wav"
CHUNK_SIZE = 1024 * 8
REGION = "eu-central-1" #Frankfurt

# Liste für 'Transcribe' Ergebnisse
RESULT=[]

# Event Handler definieren
class MyEventHandler(TranscriptResultStreamHandler):
    async def handle_transcript_event(self, transcript_event: TranscriptEvent):
        results = transcript_event.transcript.results
        # Alle Transkriptionsergebnisse in Liste schreiben
        for result in results:
            for alt in result.alternatives:
                RESULT.append(alt.transcript)


async def basic_transcribe():
    # Client aufsetzen
    client = TranscribeStreamingClient(region=REGION)

    stream = await client.start_stream_transcription(
        language_code="de-DE",
        media_sample_rate_hz=SAMPLE_RATE,
        media_encoding="pcm",
    )

    async def write_chunks():
        async with aiofile.AIOFile(AUDIO_PATH, "rb") as afp:
            reader = aiofile.Reader(afp, chunk_size=CHUNK_SIZE)
            await apply_realtime_delay(
                stream, reader, BYTES_PER_SAMPLE, SAMPLE_RATE, CHANNEL_NUMS
            )
        await stream.input_stream.end_stream()

    # Handler instanziieren
    handler = MyEventHandler(stream.output_stream)
    await asyncio.gather(write_chunks(), handler.handle_events())

# AWS Polly session starten
polly_client = boto3.Session(region_name='eu-central-1').client('polly')

Nun startet das Hauptprogramm in einer Endlosschleife. Zunächst wird der Tasterzustand abgefragt. Wird der Taster gedrückt, und eine HIGH-LOW Flanke detektiert, startet 'arecord' als Hintergrundprozess eine Aufnahme. Die Status LEDs werden von Grün auf Rot gesetzt.

Wird der Taster losgelassen, stoppt die Aufnahme, und der Hintergrundprozess wird gekillt. Die aufgenommene Datei 'record.wav' wird anschließend asynchron an AWS zum Transkribieren gesandt, und ein Wartetext wird angesagt. 

Die zurückerhaltenen Textstücke werden in einer RESULT Liste gespeichert. Der letzte Listeneintrag enthält die gesamte Nachricht. Diese Nachricht wird nun an 'OpenAI' gesandt, und erneut ein Wartetext angesagt. Aus der Antwort von Chat GPT wird dann anschließend bei AWS Polly eine mp3-Datei erzeugt, und diese mit 'play' vorgelesen.

Zu guter Letzt werden die Status LEDs wieder auf Grün gesetzt und eine neue Aufnahme kann beginnen.

# Endlosschleife
while 1:

    # Default Zustand ist HIGH bei offenem Taster
    if GPIO.input(8) == GPIO.HIGH and EDGE==False:
        time.sleep(0.05)
        # nachdem Taste stabil ist nochmal abfragen
        if GPIO.input(8) == GPIO.HIGH and EDGE==False:
        # grüne LED an und rote LED aus, bereit zur Aufnahme
            GPIO.output(32, GPIO.HIGH)
            GPIO.output(36, GPIO.LOW)

    # HIGH-LOW Flanke detektieren
    if GPIO.input(8) == GPIO.LOW and EDGE==False:
        time.sleep(0.05)
        # nachdem Taste stabil ist nochmal abfragen
        if GPIO.input(8) == GPIO.LOW and EDGE==False:
            # grüne LED aus und rote LED an, Verarbeitung läuft
            GPIO.output(32, GPIO.LOW)
            GPIO.output(36, GPIO.HIGH)

            # Dauerhaft im Hintergrund aufnehmen bis der Prozess gekillt wird
            os.system("arecord --device=hw:1,0 --format S16_LE --rate 44100 -c1 -q /home/pi/record.wav &")

            # Flankenflag setzen
            EDGE=True

    # LOW-HIGH Flanke detektieren
    if GPIO.input(8) == GPIO.HIGH and EDGE==True:
        time.sleep(0.05)
        # nachdem Taste stabil ist nochmal abfragen
        if GPIO.input(8) == GPIO.HIGH and EDGE==True:
            # arecord beenden
            os.system("pkill arecord")

            # transkribieren...
            # Wartetext ansagen
            # Ggf. 'sox' mit 'sudo apt-get install sox' installieren
            # Ggf. mp3 library mit 'sudo apt-get install libsox-fmt-mp3' installieren
            os.system("play -q -v 2 '/home/pi/BitteWartenNeural.mp3' -t alsa &")
            #print ("Transcrption ongoing...")
            loop = asyncio.get_event_loop()
            loop.run_until_complete(basic_transcribe())
            #print(RESULT[-1])

            if len(RESULT) > 0:
                # ChatGPT session starten und Frage stellen...
                #print ("Waiting for Chat GPT...")
                # Wartetext ansagen
                os.system("play -q -v 2 '/home/pi/BitteWartenNeural.mp3' -t alsa &")
                
                completion = openai.ChatCompletion.create(
                    model="gpt-3.5-turbo",
                    messages=[
                        {"role": "user", "content": RESULT[-1]}
                    ]   
                )
                GPT_out=completion.choices[0].message.content
                #print(GPT_out)

                # Antwort mit AWS Polly ausgeben...
                #print ("Text to Speach ongoing...")

                # Verfügbare deutsche Stimmen sind: 'Marlene', 'Vicki', 'Hans' und 'Daniel' für die 'standard' engine
                # Für die 'neural' engine mt besserer Sprachqualität ist nur 'Vicki' und 'Daniel' verfügbar
                # Ausgabe Formate sind: 'ogg_vorbis', 'json', 'mp3' und 'pcm'

                response = polly_client.synthesize_speech(VoiceId='Daniel',
                        OutputFormat='mp3',
                        Text = GPT_out,
                        Engine = 'neural')

                file = open('/home/pi/speech.mp3', 'wb')
                file.write(response['AudioStream'].read())
                file.close()

                # Vorlesen der MP3 Datei
                os.system("play -q -v 2 '/home/pi/speech.mp3' -t alsa")

            # FlankenFlag zurücksetzen
            EDGE=False
            # RESULT löschen
            RESULT=[]

Das gesamte Pythonprogramm sowie der 'BitteWarten' Ansagetext steht auf dem öffentlichen github repository 'ChatBox' zur Verfügung.

Damit das Programm nach dem Booten direkt startet, muss mit 'systemd' ein Service gestartet werden. Eine detaillierte Anleitung wie ein solcher 'Service' erzeugt werden kann findet sich z.B. hier.

Mein '/etc/systemd/system/AutoRunChatBox.service' file sieht so aus.

Sobald 'sound' verfügbar ist, wird das shell script 'script.sh' aufgerufen. In dieser Shell werden die notwendigen AWS Schlüssel als Umgebungsvariablen exportiert und das 'ChatBox.py' Programm gestartet.

Nachdem alle Programmierarbeiten abgeschlossen sind, habe ich mir in Free CAD ein ca. 95 mm x 65 mm großes und 40 mm hohes Gehäuse konstruiert. Dieses habe ich anschließend mit weißem PLA ausgedruckt. Im Bild unten ist die offene Box, die später noch nachbearbeitet wird zu sehen.

Die Zeichnung der Bodenplatte sieht wie folgt aus.

Nachdem die Bohrungen für Lautsprecher und Mikrofon angebracht, sowie der Taster und die LEDs eingebaut sind, sieht der Sprachassistent im halb fertigen Zustand nun aus wie im Bild unten.

Die gesamte Elektronik passt zusammen mit den Kabeln gerade so ins Gehäuse.

Zu guter Letzt möchte ich den Sprachassistenten auch noch im Einsatz zeigen.

Auch wenn Chat GPT noch nicht auf alle Fragen die richtige Antwort weiß, kann der Dienst dennoch immer eine gut formulierte und häufig wortreiche Antwort liefern.

Viel Spaß beim selber Ausprobieren und Basteln...

Keine Kommentare:

Kommentar veröffentlichen