Skip to Content

Sprachassistent auf dem UNIHIKER M10 mit OpenAI

Sprachassistent auf dem UNIHIKER M10 mit OpenAI

Der UNIHIKER M10 ist ein kompaktes Einplatinen-System, das ein Linux-basiertes System, Sensoren und Hardware-Schnittstellen auf einer Platine integriert. Er verfügt über einen Quad-Core Arm Cortex-A35 Prozessor mit 512 MB RAM und 16 GB eMMC-Speicher.

Das Board ist außerdem mit einem 2,8-Zoll-Touchscreen mit 240 × 320 Pixel Auflösung sowie Wi-Fi- und Bluetooth-Konnektivität ausgestattet. Eingebaute Sensoren umfassen einen Lichtsensor, Beschleunigungssensor, Gyroskop und Mikrofon.

Da das Board Linux ausführt und Python-Programmierung unterstützt, kann es für viele Anwendungen genutzt werden. In diesem Tutorial lernst du, wie du einen Sprachassistenten auf dem UNIHIKER M10 implementierst. Der folgende kurze Videoclip zeigt den Sprachassistenten in Aktion:

Möglicherweise musst du die Lautstärke an deinem Computer erhöhen, um meine Stimme zu hören. Beachte auch, dass dieser Assistent keine Sprachausgabe hat, obwohl sich diese leicht hinzufügen ließe.

Benötigte Teile

Du benötigst ein UNIHIKER M10 Board. Du kannst es zum Beispiel bei DFRobot oder Amazon erwerben:

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

Hardware des Unihiker M10

Der UNIHIKER M10 basiert auf dem Rockchip RK3308 Chip. Dieser Chip verwendet vier Arm Cortex-A35 Kerne und arbeitet mit einer Taktfrequenz von bis zu 1,2 GHz. Der Prozessor läuft mit einem vollständigen Debian Linux Betriebssystem, auf dem Python 3.7 vorinstalliert ist. Ebenfalls vorinstalliert sind viele wissenschaftliche Python-Bibliotheken wie Numpy, Matplotlib, Jupyter, Pandas, Seaborn, Tensorflow und weitere.

Das Board verfügt über 512 MB DDR3 Systemspeicher, der vom Linux-Betriebssystem und Benutzeranwendungen genutzt wird. Der Programmspeicher wird durch 16 GB eMMC Flash-Speicher auf dem Board bereitgestellt.

Zusätzlich zum Hauptprozessor integriert das Board einen GD32VF103C8T6 Mikrocontroller als Co-Prozessor. Dieser verfügt über 64 KB Flash-Speicher und 32 KB SRAM. Der Mikrocontroller übernimmt viele hardwarebezogene Aufgaben wie Sensorverwaltung, GPIO-Steuerung und Aktuatorbetrieb.

Display und Benutzeroberfläche

Der UNIHIKER M10 ist mit einem 2,8-Zoll-Farb-Touchscreen ausgestattet. Das Display hat eine Auflösung von 240 × 320 Pixeln. Das Board verfügt über eine Home-Taste und zwei programmierbare Benutzertasten mit den Bezeichnungen A und B, wie unten gezeigt:

Buttons on UNIHIKER M10
Tasten am UNIHIKER M10

Integrierte Sensoren

Eingebaute Sensoren sind ein Lichtsensor, Beschleunigungssensor, Gyroskop und Mikrofon. Hardware-Erweiterungen sind über USB-Ports, Gravity-Sensoranschlüsse und einen micro:bit-kompatiblen Edge Connector möglich, der GPIO, I2C, SPI und UART Schnittstellen bereitstellt.

Front and back of UNIHIKER M10
Vorder- und Rückseite des UNIHIKER M10 (source)

Außerdem bieten ein passiver Summer und eine Status-LED einfache Audio- und visuelle Ausgaben für Benachrichtigungen und Debugging. Ein externer Lautsprecher kann über den USB-Port angeschlossen werden.

Konnektivität und Netzwerk

Drahtlose Kommunikation erfolgt über ein kombiniertes Wi-Fi- und Bluetooth-Modul. Das Board unterstützt 2,4 GHz Wi-Fi und Bluetooth 4.0. Wi-Fi kann über eine Weboberfläche unter 10.1.2.3 konfiguriert werden.

Schnittstellen und Erweiterungsoptionen

Das Board bietet mehrere physische Schnittstellen zum Anschluss externer Hardware. Ein USB Type-C Anschluss dient zur Stromversorgung und Kommunikation mit einem Computer. Das Board kann über diesen Port mit 5 V versorgt werden.

Ein USB Type-A Host-Port ermöglicht den Anschluss von USB-Peripheriegeräten wie Speichermedien, Tastaturen oder Adaptern. Das Board verfügt außerdem über einen microSD-Kartensteckplatz zur Speichererweiterung oder zum Dateitransfer.

Darüber hinaus stehen mehrere Erweiterungsanschlüsse für Sensoren und Aktuatoren zur Verfügung. Gravity-kompatible 3-Pin-Anschlüsse bieten Zugriff auf PWM- und analoge Eingänge. Dedizierte 4-Pin-Anschlüsse ermöglichen I2C-Kommunikation für digitale Sensoren und Module.

IO Connectors of UNIHIKER M10
IO-Anschlüsse des UNIHIKER M10 (source)

Edge Connector

Das Board verfügt außerdem über einen micro:bit-kompatiblen Edge Connector. Dieser Anschluss stellt bis zu 19 GPIO-Pins bereit und unterstützt Schnittstellen wie I2C, UART und SPI. Zudem bietet er mehrere ADC-Eingänge und PWM-Ausgänge zur Steuerung externer Hardware. Das folgende Bild zeigt die Pinbelegung des Edge Connectors:

Pinout of Edge Connector
Pinbelegung des Edge Connectors (source)

Schaltpläne

Für detailliertere technische Informationen siehe die unten verlinkten Schaltpläne des UNIHIKER M10:

Technische Spezifikationen

Die folgende Tabelle fasst die technischen Spezifikationen des UNIHIKER M10 Boards zusammen:

Merkmal Spezifikation
Hauptprozessor Rockchip RK3308
CPU-Architektur Quad-Core Arm Cortex-A35
CPU-Taktfrequenz Bis zu 1,2 GHz
Systemspeicher 512 MB DDR3
Interner Speicher 16 GB eMMC
Co-Prozessor GD32VF103C8T6 RISC-V Mikrocontroller
MCU-Taktfrequenz Bis zu 108 MHz
MCU-Speicher 64 KB Flash, 32 KB SRAM
Betriebssystem Debian Linux
Display 2,8-Zoll-Touchscreen
Display-Auflösung 240 × 320 Pixel
Drahtlose Konnektivität Wi-Fi 802.11 b/g/n (2,4 GHz), Bluetooth 4.0
Sensoren Lichtsensor, 6-Achsen-IMU (Beschleunigungssensor + Gyroskop), Mikrofon
Benutzereingaben Home-Taste, A/B Benutzertasten, Touchscreen
Audio-Ausgabe Passiver Summer
USB-Ports 1 × USB-C (Strom und Daten), 1 × USB-A Host
Speichererweiterung microSD-Kartensteckplatz
Erweiterungsschnittstellen Gravity 3-Pin-Anschlüsse (analog/PWM), I2C-Anschlüsse
Edge Connector micro:bit-kompatibler Edge Connector
Unterstützte Schnittstellen GPIO, ADC, PWM, I2C, SPI, UART
Betriebsspannung 5 V Eingang über USB-C
Logikpegel 3,3 V
Typischer Strom Bis zu ~2 A
Platinengröße Ca. 51,6 mm × 83 mm × 13 mm

Programmierung des UNIHIKER M10

Es gibt verschiedene Anwendungen, mit denen du den UNIHIKER M10 programmieren kannst, wie Jupyter Notebook, Mind+, VSCode, Python IDLE oder Thonny. Für detaillierte Anleitungen zur Einrichtung dieser Anwendungen siehe den Getting Started Abschnitt der UNIHIKER-Dokumentation.

Installation von Thonny

Ich persönlich fand Thonny am einfachsten für ein kleines Projekt wie in diesem Tutorial. Für Installationsanweisungen siehe den Download and Install Thonny-Abschnitt der UNIHIKER-Dokumentation.

Thonny mit UNIHIKER M10 verbinden

Nach der Installation von Thonny verbinde deinen UNIHIKER M10 mit dem USB-Kabel mit deinem Computer. Öffne dann Thonny und klicke auf „Run -> Select interpreter …“:

Es öffnet sich ein Dialog, in dem du „Remote Python 3 (SSH)“ als Interpreter auswählst. Unter Host gibst du „10.1.2.3“ ein, als Benutzername „root“ und als Authentifizierungsmethode „password“:

Nach dem Klick auf „Run -> Stop/Restart backend“ ist Thonny mit deinem UNIHIKER M10 verbunden und du kannst Python-Programme darauf bearbeiten und ausführen.

Installation von Putty und der OpenAI-Bibliothek

Als nächstes benötigen wir ein Tool, mit dem wir Python-Bibliotheken auf dem UNIHIKER M10 installieren können, wie die OpenAI Bibliothek. Ich habe versucht, Bibliotheken über Thonny („Manage Packages“, „Open System Shell…“) zu installieren, aber es hat nicht funktioniert.

Stattdessen habe ich Putty verwendet. Installationsanweisungen findest du im SSH Tools der UNIHIKER-Dokumentation. Nach der Installation öffne Putty und erstelle eine Sitzung mit 10.1.2.3 als HOST:

Klicke dann auf „Open“ und gib im Putty-Terminal als Benutzername „root“ und als Passwort „dfrobot“ ein:

Jetzt kannst du die OpenAI Bibliothek mit dem Befehl „pip install openai“ installieren:

Sollten weitere Python-Bibliotheken fehlen, kannst du diese auf die gleiche Weise installieren. Beachte jedoch, dass der UNIHIKER M10 per USB-Kabel verbunden sein muss.

Wi-Fi einrichten

Schließlich müssen wir noch die Wi-Fi-Verbindung einrichten, damit unser Sprachassistent Internetzugang hat, um die KI-Tools von OpenAI aufzurufen.

Öffne dazu einen Webbrowser und gib „10.1.2.3“ in die Adresszeile ein. Du siehst eine Webseite mit dem Menüpunkt „Network Settings“ in der Seitenleiste. Klicke darauf und gib deine Wi-Fi-Zugangsdaten im rechten Dialogfeld ein:

OpenAI API-Schlüssel erhalten

Unser Sprachassistent nutzt KI-Modelle von OpenAI. Du benötigst daher ein OpenAI-Konto. Gehe zu https://platform.openai.com und melde dich mit einer E-Mail-Adresse oder einem bestehenden Google- oder Microsoft-Konto an.

Nach der Verifizierung deiner E-Mail und dem Abschluss der Ersteinrichtung melde dich im OpenAI-Dashboard an, platform.openai.com/api-keys und finde oder erstelle deinen API-Schlüssel (=SECRET KEY) wie unten gezeigt:

OpenAI API keys
OpenAI API-Schlüssel

Der API-Schlüssel ist eine eindeutige, lange Zeichenkette, die mit „sk-proj-“ beginnt und zur Authentifizierung deiner API-Anfragen benötigt wird (siehe unten). Später musst du diese gesamte Zeichenkette in den Code des Sprachassistenten kopieren.

sk-proj-xcA.......................OtDu0U

Das ist alles, was du wirklich brauchst, aber ich empfehle, auch ein Nutzungslimit für dein Konto festzulegen. So vermeidest du versehentlich hohe Kosten durch Fehler im Code (z. B. durch hunderte Anfragen).

Nutzungslimits kannst du unter dem Abrechnungs-Tab einstellen und dort auch die Preise (Kosten) für die verschiedenen KI-Modelle einsehen (platform.openai.com/settings/organization/billing).

Jetzt sind wir bereit, den Code für den Sprachassistenten zu schreiben.

Code für den Sprachassistenten

Erstelle in Thonny eine neue Datei, ich habe meine „assistant_tk.py“ genannt, und kopiere den folgenden Code hinein.

Dieser Code implementiert einen Sprachassistenten. Er nimmt Audio auf, während die Taste A gedrückt gehalten wird, transkribiert die Sprache mit OpenAIs Whisper-Modell in Text, sendet den Text an ein GPT-basiertes Large Language Model (LLM) zur Antwort und zeigt sowohl Frage als auch Antwort in der GUI des Geräts an.

# www.makerguides.com
# Python 3.7
# openai 1.39.0
# PyAudio 0.2.11
# pinpong 0.6.1
# numpy 1.21.6

import time
import threading
import numpy as np
import pyaudio
import io
import wave
from openai import OpenAI
from pinpong.extension.unihiker import button_a
import tkinter as tk
from tkinter import ttk
from typing import List

API_KEY = "sk-proj-my-api-key"
DEVICE_INDEX = 2
SAMPLE_RATE = 16000
CHANNELS = 1
CHUNK = 1024

client = OpenAI(api_key=API_KEY)

# GUI -----------------------------------------------------

root = tk.Tk()
root.title("Voice Chatbot")
root.geometry("240x320")

status_var = tk.StringVar()
status_var.set("Hold Button A to speak")

status_label = ttk.Label(root, textvariable=status_var,
    font=("Arial", 11), anchor="center")
status_label.pack(fill="x", padx=6, pady=4)

frame = tk.Frame(root)
frame.pack(fill="both", expand=True)

scrollbar = tk.Scrollbar(frame, width=18)
scrollbar.pack(side="right", fill="y")

text_box = tk.Text(frame, wrap="word",
    yscrollcommand=scrollbar.set, font=("Arial", 10))
text_box.bind("<Motion>", lambda e: "break")
text_box.bind("<B1-Motion>", lambda e: "break")
text_box.bind("<Button-1>", lambda e: "break")
text_box.pack(side="left", fill="both", expand=True)

scrollbar.config(command=text_box.yview)


def set_status(text):
    status_var.set(text)
    root.update_idletasks()


def show_answer(text):
    text_box.delete("1.0", tk.END)
    text_box.insert(tk.END, text)
    text_box.see(tk.END)


# Audio helpers -----------------------------------------------

def normalize(pcm_bytes: bytes) -> bytes:
    samples = np.frombuffer(pcm_bytes, dtype=np.int16).astype(np.float32)
    peak = np.max(np.abs(samples))
    if peak == 0:
        return pcm_bytes
    gain = (0.9 * 32767) / peak
    normalized = np.clip(samples * gain, -32768, 32767).astype(np.int16)
    return normalized.tobytes()


def record_while_held() -> bytes:
    frames: List[bytes] = []

    pa = pyaudio.PyAudio()
    stream = pa.open(
        format=pyaudio.paInt16,
        channels=CHANNELS,
        rate=SAMPLE_RATE,
        input=True,
        input_device_index=DEVICE_INDEX,
        frames_per_buffer=CHUNK
    )

    set_status("Hold Button A to speak")

    while not button_a.is_pressed():
        time.sleep(0.01)

    set_status("recording...")

    while button_a.is_pressed():
        data = stream.read(CHUNK, exception_on_overflow=False)
        frames.append(data)

    stream.stop_stream()
    stream.close()
    pa.terminate()

    return b"".join(frames)


def pcm_to_wav(pcm_bytes: bytes) -> bytes:
    buf = io.BytesIO()
    with wave.open(buf, "wb") as wf:
        wf.setnchannels(CHANNELS)
        wf.setsampwidth(2)
        wf.setframerate(SAMPLE_RATE)
        wf.writeframes(pcm_bytes)
    return buf.getvalue()


# OpenAI -----------------------------------------------

def transcribe(wav_bytes: bytes) -> str:
    audio_file = io.BytesIO(wav_bytes)
    audio_file.name = "recording.wav"

    set_status("transcribing...")
    response = client.audio.transcriptions.create(
        model="whisper-1",
        file=audio_file,
        language="en",
        temperature=0
    )
    return response.text


def ask_gpt(question: str) -> str:
    set_status("thinking...")
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": "You are a helpful assistant. Answer clearly and concisely."
            },
            {"role": "user", "content": question}
        ],
    )
    return response.choices[0].message.content


# Main assistant loop -----------------------------------------------

def assistant_loop():
    while True:
        raw_pcm = record_while_held()
        if not raw_pcm:
            continue

        normalized_pcm = normalize(raw_pcm)
        wav_bytes = pcm_to_wav(normalized_pcm)
        question = transcribe(wav_bytes)
        answer = ask_gpt(question)

        set_status("ready")
        show_answer(question + "\n\n" + answer)


threading.Thread(target=assistant_loop, daemon=True).start()
root.mainloop()

Importe

Der Code beginnt mit dem Import mehrerer Module, die für die Funktionalität notwendig sind. Dazu gehören Standardbibliotheken wie time, threading und io für Zeitsteuerung, nebenläufige Ausführung und Byte-Stream-Verarbeitung. Außerdem werden numpy für numerische Operationen auf Audiodaten, pyaudio für Audioaufnahme und wave für die Verarbeitung des WAV-Audioformats importiert.

Der OpenAI Python-Client wird importiert, um mit der OpenAI-API zu kommunizieren. Das UNIHIKER-spezifische button_a wird importiert, um Tastendrücke zu erkennen. Schließlich werden tkinter und ttk importiert, um die GUI zu erstellen, und List aus typing wird für Typ-Hinweise verwendet.

import time
import threading
import numpy as np
import pyaudio
import io
import wave
from openai import OpenAI
from pinpong.extension.unihiker import button_a
import tkinter as tk
from tkinter import ttk
from typing import List

Konstanten und Client-Initialisierung

Mehrere Konstanten definieren die Parameter für die Audioaufnahme, wie den API-Schlüssel für OpenAI, den Index des Audioeingabegeräts, Abtastrate, Anzahl der Kanäle und die Puffergröße für Audio-Chunks. Ein OpenAI-Client-Objekt wird mit dem API-Schlüssel erstellt, um die Kommunikation mit den OpenAI-Diensten zu ermöglichen.

API_KEY = "sk-proj-my-api-key"
DEVICE_INDEX = 2
SAMPLE_RATE = 16000
CHANNELS = 1
CHUNK = 1024

client = OpenAI(api_key=API_KEY)

Beachte, dass du den Platzhalter „sk-proj-my-api-key“ für API_KEY durch deinen eigenen OpenAI API-Schlüssel ersetzen musst!

Grafische Benutzeroberfläche (GUI)

Die GUI wird mit tkinter erstellt. Ein Hauptfenster wird mit Titel und fester Größe erzeugt. Ein Status-Label informiert den Benutzer über den aktuellen Zustand, z. B. die Aufforderung, Taste A zum Sprechen zu halten, oder zeigt Fortschrittsmeldungen an.

Unter dem Status-Label befindet sich ein Textfeld mit Scrollbar, das die transkribierte Frage und die Antwort des Assistenten anzeigt. Das Textfeld ist schreibgeschützt, indem Mausinteraktionen, die den Inhalt verändern könnten, deaktiviert sind.

root = tk.Tk()
root.title("Voice Chatbot")
root.geometry("240x320")

status_var = tk.StringVar()
status_var.set("Hold Button A to speak")

status_label = ttk.Label(root, textvariable=status_var,
    font=("Arial", 11), anchor="center")
status_label.pack(fill="x", padx=6, pady=4)

frame = tk.Frame(root)
frame.pack(fill="both", expand=True)

scrollbar = tk.Scrollbar(frame, width=18)
scrollbar.pack(side="right", fill="y")

text_box = tk.Text(frame, wrap="word",
    yscrollcommand=scrollbar.set, font=("Arial", 10))
text_box.bind("<Motion>", lambda e: "break")
text_box.bind("<B1-Motion>", lambda e: "break")
text_box.bind("<Button-1>", lambda e: "break")
text_box.pack(side="left", fill="both", expand=True)

scrollbar.config(command=text_box.yview)

Status- und Anzeige-Funktionen

Zwei Hilfsfunktionen verwalten die GUI-Aktualisierungen. Die set_status() -Funktion aktualisiert den Text des Status-Labels und erzwingt eine sofortige GUI-Aktualisierung. Die show_answer() -Funktion löscht das Textfeld, fügt neuen Text ein und scrollt ans Ende, damit der neueste Inhalt sichtbar ist.

def set_status(text):
    status_var.set(text)
    root.update_idletasks()


def show_answer(text):
    text_box.delete("1.0", tk.END)
    text_box.insert(tk.END, text)
    text_box.see(tk.END)

Audio-Hilfsfunktionen

Der Code enthält mehrere Funktionen zur Audiobearbeitung. Die normalize() -Funktion wandelt rohe PCM-Audio-Bytes in Fließkomma-Samples um und skaliert sie so, dass die Spitzenamplitude 90 % des maximalen 16-Bit-Integer-Bereichs erreicht. Diese Normalisierung sorgt für eine konsistente Lautstärke bei der Transkription.

def normalize(pcm_bytes: bytes) -> bytes:
    samples = np.frombuffer(pcm_bytes, dtype=np.int16).astype(np.float32)
    peak = np.max(np.abs(samples))
    if peak == 0:
        return pcm_bytes
    gain = (0.9 * 32767) / peak
    normalized = np.clip(samples * gain, -32768, 32767).astype(np.int16)
    return normalized.tobytes()

Die record_while_held() -Funktion nimmt Audio vom angegebenen Eingabegerät auf, solange Taste A gedrückt ist.

Sie initialisiert einen PyAudio-Stream mit den konfigurierten Parametern und wartet, bis Taste A gedrückt wird. Dann liest sie kontinuierlich Audio-Chunks aus und fügt sie einer Liste hinzu, bis die Taste losgelassen wird. Die aufgenommenen Audioframes werden zusammengefügt und als rohe PCM Bytes zurückgegeben.

def record_while_held() -> bytes:
    frames: List[bytes] = []

    pa = pyaudio.PyAudio()
    stream = pa.open(
        format=pyaudio.paInt16,
        channels=CHANNELS,
        rate=SAMPLE_RATE,
        input=True,
        input_device_index=DEVICE_INDEX,
        frames_per_buffer=CHUNK
    )

    set_status("Hold Button A to speak")
    while not button_a.is_pressed():
        time.sleep(0.01)

    set_status("recording...")
    while button_a.is_pressed():
        data = stream.read(CHUNK, exception_on_overflow=False)
        frames.append(data)

    stream.stop_stream()
    stream.close()
    pa.terminate()

    return b"".join(frames)

Die pcm_to_wav() -Funktion wandelt die rohen PCM-Bytes in einen WAV -Format-Byte-Stream um, unter Verwendung des wave -Moduls. Dies ist notwendig, da die OpenAI-Transkriptions-API Audiodateien in Standardformaten wie WAV erwartet.

def pcm_to_wav(pcm_bytes: bytes) -> bytes:
    buf = io.BytesIO()
    with wave.open(buf, "wb") as wf:
        wf.setnchannels(CHANNELS)
        wf.setsampwidth(2)
        wf.setframerate(SAMPLE_RATE)
        wf.writeframes(pcm_bytes)
    return buf.getvalue()

OpenAI API-Interaktion

Zwei Funktionen übernehmen die Kommunikation mit der OpenAI-API. Die transcribe() -Funktion sendet die WAV-Audio-Bytes an das Whisper-Modell, um eine Texttranskription zu erhalten. Sie aktualisiert das Status-Label, um anzuzeigen, dass die Transkription läuft, und gibt den erkannten Text zurück.

def transcribe(wav_bytes: bytes) -> str:
    audio_file = io.BytesIO(wav_bytes)
    audio_file.name = "recording.wav"

    set_status("transcribing...")

    response = client.audio.transcriptions.create(
        model="whisper-1",
        file=audio_file,
        language="en",
        temperature=0
    )

    return response.text

Die ask_gpt() -Funktion sendet die transkribierte Frage an ein GPT-basiertes Chat-Completion-Modell. Während der Antwort wartet, wird der Status auf „thinking…“ gesetzt. Anschließend wird eine Systemnachricht erstellt, die das Modell anweist, ein hilfreicher Assistent zu sein. Schließlich gibt die Funktion die Antwort des Assistenten zurück.

def ask_gpt(question: str) -> str:
    set_status("thinking...")
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": "You are a helpful assistant. Answer clearly and concisely."
            },
            {"role": "user", "content": question}
        ],
    )

    return response.choices[0].message.content

Haupt-Assistenz-Schleife

Die Kernfunktionalität läuft in der assistant_loop() -Funktion, die in einem separaten Thread ausgeführt wird, um die GUI reaktionsfähig zu halten.

Sie wartet kontinuierlich darauf, dass der Benutzer Taste A hält, und nimmt währenddessen Audio auf. Wird kein Audio aufgenommen, startet die Schleife neu. Andernfalls wird das Audio normalisiert, in WAV konvertiert, die Sprache transkribiert und das GPT-Modell nach einer Antwort gefragt.

Nach der Verarbeitung wird der Status auf „ready“ gesetzt und sowohl die Frage als auch die Antwort im Textfeld angezeigt.

def assistant_loop():
    while True:
        raw_pcm = record_while_held()
        if not raw_pcm:
            continue

        normalized_pcm = normalize(raw_pcm)
        wav_bytes = pcm_to_wav(normalized_pcm)

        question = transcribe(wav_bytes)
        answer = ask_gpt(question)

        set_status("ready")
        show_answer(question + "\n\n" + answer)

Starten des Assistenten und der GUI-Hauptschleife

Zum Schluss wird die Assistenten-Schleife in einem Daemon-Thread gestartet, sodass sie im Hintergrund läuft. Dann wird die Haupt-Event-Schleife der GUI mit root.mainloop() gestartet, die das Fenster offen und reaktionsfähig hält.

threading.Thread(target=assistant_loop, daemon=True).start()
root.mainloop()

Fehlermeldungen

Beim Ausführen des Codes können folgende Warnungen in der Thonny-Konsole erscheinen:

ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.rear
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.center_lfe
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.side
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.iec958
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.iec958
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.iec958
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.hdmi
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.hdmi
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.modem
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.modem
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.phoneline
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.phoneline
ALSA lib pcm_oss.c:377:(_snd_pcm_oss_open) Unknown field port
ALSA lib pcm_oss.c:377:(_snd_pcm_oss_open) Unknown field port
ALSA lib pcm_a52.c:823:(_snd_pcm_a52_open) a52 is only for playback
ALSA lib conf.c:5014:(snd_config_expand) Unknown parameters {AES0 0x6 AES1 0x82 AES2 0x0 AES3 0x2  CARD 0}
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM iec958:{AES0 0x6 AES1 0x82 AES2 0x0 AES3 0x2  CARD 0}
ALSA lib pcm_usb_stream.c:486:(_snd_pcm_usb_stream_open) Invalid type for card
ALSA lib pcm_usb_stream.c:486:(_snd_pcm_usb_stream_open) Invalid type for card

Das liegt daran, dass beim Initialisieren von pyaudio.PyAudio() die zugrundeliegende ALSA (Advanced Linux Sound Architecture) Bibliothek dein System nach allen möglichen Audiogeräten und Konfigurationen durchsucht (z. B. 5.1 Surround Sound, HDMI oder digitales S/PDIF).

Da der UNIHIKER M10 ein kompaktes Single Board Computer ist, gibt es keine „Rear Speakers“ oder „Modem Ports“, weshalb ALSA meldet, dass diese nicht gefunden werden können. Wenn dein Code trotz dieser Warnungen läuft und Audio verarbeitet, funktioniert dein Setup einwandfrei.

Fazit

In diesem Tutorial hast du gelernt, wie man einen Sprachassistenten auf dem UNIHIKER M10 mit OpenAI-Diensten implementiert. Für weitere, einfachere Codebeispiele schau dir den Python Coding Examples Abschnitt der UNIHIKER-Dokumentation an.

Beachte, dass du den Sprachassistenten leicht erweitern und verbessern kannst, indem du eine Chat-Historie und Tool-Aufruf-Funktionalität hinzufügst. Außerdem habe ich Tkinter für eine einfache Benutzeroberfläche verwendet, aber für eine bessere GUI könntest du QtPy nutzen, das ebenfalls auf dem UNIHIKER M10 vorinstalliert ist.

Außerdem könntest du einen Lautsprecher hinzufügen, entweder über USB, Bluetooth oder Line-Out, damit dein Sprachassistent auch mit Audio antwortet. Siehe das AI Language Tutor with UNIHIKER M10 Tutorial für ein Beispiel.

Für einen Vision-Chatbot, der Bilder sehen und Fragen dazu beantworten kann, siehe unser Vision Chatbot with DFRobot ESP32-S3 AI Camera and OpenAI Tutorial. Und für weitere KI-Beispiele siehe den AI Projects Abschnitt der UNIHIKER-Dokumentation.

Wenn du Fragen hast, kannst du sie gerne im Kommentarbereich stellen.

Viel Spaß beim Tüfteln 😉