Skip to Content

Vision-Chatbot mit DFRobot ESP32-S3 AI Camera und OpenAI

Vision-Chatbot mit DFRobot ESP32-S3 AI Camera und OpenAI

Die DFRobot ESP32-S3 AI Camera (DFR1154) ist ein Entwicklungsboard für KI- und Vision-Projekte. Es verfügt über einen ESP32-S3 Mikrocontroller und integriert eine Kamera, ein Mikrofon und einen Lautsprecher auf einer einzigen Platine.

In diesem Projekt erstellen wir einen Vision Chatbot mit dem ESP32-S3 AI Camera Board und OpenAI. Das Board nimmt Bilder auf und hört über das Mikrofon gesprochene Fragen. Anschließend wandelt es die Sprache mit OpenAI in Text um. Danach sendet es sowohl das Bild als auch die Textfrage an ein weiteres OpenAI-Modell zur Analyse. Dieses KI-Modell verarbeitet die Eingaben, generiert eine Antwort zum Bild und gibt die Antwort als Text zurück. Schließlich nutzen wir OpenAI ein drittes Mal, um den Antworttext in Sprache umzuwandeln und über den Lautsprecher abzuspielen.

Der kurze Videoclip unten zeigt den Vision Chatbot in Aktion. Ich halte das Board in der linken Hand, und die Kamera zeigt auf einen kleinen Modellschädel auf meinem Schreibtisch. Ich drücke die BOOT-Taste am Board, um den Vision Chatbot zu starten, und du hörst meine Frage sowie die Antwort des Bots.

Passe die Lautstärke an, falls du den Ton nicht hörst, und beachte, dass es aufgrund der Datenübertragung und Verarbeitung bei OpenAI eine Verzögerung von einigen Sekunden zwischen Frage und Antwort gibt:

Vision Chatbot in Aktion

Die Audioaufnahme, Audiowiedergabe und Bildaufnahme werden lokal vom ESP32-S3 AI Camera Board durchgeführt. Die Bildanalyse, Text-zu-Sprache (TTS) und Sprache-zu-Text (STT) nutzen jedoch OpenAI’s KI-Modelle. Daher sind eine WLAN-Verbindung und ein OpenAI API-Schlüssel erforderlich.

Benötigte Teile

Für dieses Projekt verwende ich das ESP32-S3 AI Camera Modul (DFR1154) von DFRobot. Du kannst es über die untenstehenden Links bei DFRobot beziehen. Achte darauf, Version 1.1 und nicht die ältere Version 1.0 zu bekommen. Möglicherweise benötigst du auch ein USB-C-Kabel.

DFRobot ESP32-S3 AI Camera

USB-C Kabel

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 der DFRobot AI Camera

Die DFRobot ESP32-S3 AI Camera (DFR1154) ist ein kompaktes Embedded Vision- und KI-Entwicklungsboard, das auf dem ESP32-S3 Mikrocontroller von Espressif basiert. Es integriert drahtlose Konnektivität, Kamerabildgebung, Audio-Ein-/Ausgabe und KI-Beschleunigung in einem Modul. Dieses Board ist für Edge-Computing-Aufgaben wie Objekterkennung, Sprachinteraktion und Echtzeit-Visuelle Analyse konzipiert.

Die Platine ist ein quadratisches PCB mit etwa 42 mm Seitenlänge, was sie ideal für die Integration in Robotik, intelligente Sensoren und IoT-Überwachungssysteme macht. Das Bild unten zeigt Vorder- und Rückseite des Boards:

Front and Back of DFRobot ESP32-S3 AI Camera (source)
Vorder- und Rückseite der DFRobot ESP32-S3 AI Camera (source)

Mikrocontroller- und Speicherarchitektur

Im Kern des Boards führt der ESP32-S3 Mikrocontroller den Anwendungscode aus und steuert die Kommunikation. Er verfügt über eine Tensilica Xtensa® Dual-Core 32-Bit LX7 CPU mit bis zu 240 MHz. Eingebaut sind SRAM für schnellen Daten- und Instruktionszugriff sowie dediziertes RTC SRAM für stromsparende Uhrenfunktionen.

Der On-Die-Speicher wird durch externen Speicher ergänzt: Das Board bietet 16 MB Flash-Speicher für Firmware und Daten sowie einen 8 MB PSRAM-Chip zur Unterstützung größerer Laufzeitspeicher, die für Bildpufferung oder KI-Modell-Ausführung benötigt werden. Die USB-Schnittstelle entspricht USB 2.0 OTG Full-Speed und ermöglicht sowohl Stromversorgung als auch Datenübertragung.

Kamerasubsystem

Das Bildgebungssystem basiert auf einem OmniVision OV3660 Kamerasensor. Dieser Sensor nimmt Bilder mit bis zu 2 Megapixeln auf und ist empfindlich für sichtbares Licht sowie 940 nm Infrarot, was den Einsatz auch bei schlechten Lichtverhältnissen oder Nachtsicht ermöglicht.

Die Optik bietet ein Sichtfeld von etwa 160 Grad, das Festbrennweitensystem hat eine Brennweite von ca. 0,95 mm und eine Blende von etwa f/2.0. Vier IR-LEDs sorgen für Beleuchtung.

Drahtlose Kommunikation und Konnektivität

Das Board unterstützt Dual-Mode-Wireless-Kommunikation. Für lokale Netzwerke implementiert es IEEE 802.11b/g/n Wi-Fi im 2,4-GHz-Band mit Unterstützung für 20 MHz und 40 MHz Kanäle sowie verschiedene Betriebsmodi wie Station, Soft Access Point und kombiniert Station+AP. Bluetooth ist gemäß Bluetooth 5 und Bluetooth Mesh verfügbar, was energiesparende Peer-Kommunikation oder Sensornetzwerke ermöglicht.

Audio- und Sensor-Schnittstellen

Neben der Bildgebung verfügt das Board über ein integriertes I2S PDM-Mikrofon zur Audioaufnahme, das über einen internen Verstärker (MAX98357) an eine dedizierte Lautsprecherschnittstelle angeschlossen ist. Außerdem gibt es einen SD-Kartenanschluss zur Speicherung von Audio- und Videodaten.

Der LTR-308 Umgebungslichtsensor ermöglicht adaptive Bildgebung oder Leistungsanpassung basierend auf der Umgebungshelligkeit. Dies ist besonders nützlich in Verbindung mit den vier IR-LEDs zur Beleuchtung.

Das Board bietet eine Gravity 4-Pin-Schnittstelle (3,3 V, GND, GPIO44/RX, GPIO43/TX) für einfache UART/I2C-Verbindungen zu externen Peripheriegeräten oder Sensoren. Beachte, dass in der älteren Version V1.0 des Boards Pin 1 der Gravity-Schnittstelle ein 3,3–5 V Eingang war. In der aktuellen Version V1.1 ist Pin 1 ein 3,3 V Ausgang!

GPIO-Pins

Die folgende Tabelle listet die GPIO-Pins und ihre Zuordnung zu den verschiedenen Hardwarekomponenten wie Kamera (CAM), Mikrofon (MIC), Audioverstärker (MAX98357), Umgebungslichtsensor (ALS), SD-Karte (SD), IR-LEDs, BOOT-Taste und Status-LED auf:

Pinout der DFRobot ESP32-S3 AI Camera (source)

Stromversorgung und physikalisches Design

Das DFR1154 akzeptiert verschiedene Versorgungskonfigurationen. Es arbeitet nominal mit 3,3 V und kann über einen USB-C-Anschluss mit 5 V DC oder über einen VIN-Anschluss mit einem Bereich von 3,7 V bis 15 V DC mit Strom versorgt werden. Ein dedizierter Power-Management-IC (HM6245) regelt diese Eingänge auf die benötigten Kernspannungen.

Der spezifizierte Betriebstemperaturbereich liegt ungefähr zwischen -10 °C und 60 °C, was für Standard-Innen- oder geschützte Außenumgebungen gedacht ist.

Technische Spezifikationen

Die folgende Tabelle fasst die wichtigsten Hardwarespezifikationen des DFRobot ESP32-S3 AI Camera (DFR1154) Moduls zusammen (source):

Spezifikation Details
Mikrocontroller ESP32-S3R8 mit Dual-Core Tensilica Xtensa LX7 CPU, 240 MHz
SRAM 512 KB
ROM 384 KB
Externer Flash 16 MB
Externer PSRAM 8 MB
RTC SRAM 16 KB
Kamerasensor OV3660, 2 MP, 160° Sichtfeld, Infrarot-Unterstützung
Optik 0,95 mm Festbrennweite, f/2.0 Blende, <8 % Verzerrung
Drahtlos Wi-Fi 802.11b/g/n (2,4 GHz); Bluetooth 5 & Mesh
Audio I2S PDM Mikrofon; Lautsprecherschnittstelle über MAX98357 Verstärker
Sensoren LTR-308 Umgebungslichtsensor
USB USB 2.0 OTG Full Speed (Typ-C)
Stromversorgung Betriebsspannung 3,3 V; USB-C 5 V; VIN 3,7–15 V
Tasten Reset und Boot
Abmessungen 42 mm × 42 mm
Betriebstemperatur -10 °C bis 60 °C

OpenAI API-Schlüssel erhalten

Der Vision Chatbot verwendet 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 logge dich im OpenAI-Dashboard ein, 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 einzigartige, 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 Vision Chatbots einfügen.

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. das Senden von hunderten Bildern).

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

Billing Overview
Abrechnungsübersicht

Ich selbst habe ein Nutzungslimit von 20 USD gesetzt und keine automatische Aufladung aktiviert. Dieses kleine Budget reicht lange, solange günstige KI-Modelle verwendet werden. Wie du siehst, habe ich noch 14 USD Guthaben und spiele seit mehreren Monaten mit den OpenAI-Modellen.

ESP32 Core installieren

Wenn dies dein erstes Projekt mit einem ESP32-Board ist, musst du den ESP32 Core installieren. Falls ESP32-Boards bereits in deiner Arduino IDE installiert sind, kannst du diesen Abschnitt überspringen.

Öffne zunächst den Preferences-Dialog über „Preferences…“ im „File“-Menü. Es öffnet sich der unten gezeigte Dialog.

Unter dem Reiter Settings findest du unten ein Eingabefeld mit der Bezeichnung „Additional boards manager URLs“:

Additional boards manager URLs in Preferences
Zusätzliche URLs für den Boards Manager in den Preferences

Füge in dieses Feld die folgende URL ein:

https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json

Damit weiß die Arduino IDE, wo sie die ESP32 Core-Bibliotheken findet. Als Nächstes installieren wir die ESP32-Boards über den Boards Manager.

Öffne den Boards Manager über „Tools -> Boards -> Board Manager“. Der Boards Manager erscheint in der linken Seitenleiste. Gib oben im Suchfeld „ESP32“ ein, und du solltest zwei Arten von ESP32-Boards sehen: die „Arduino ESP32 Boards“ und die „esp32 by Espressif“ Boards. Wir wollen die „esp32 libraries by Espressif“. Klicke auf INSTALL und warte, bis der Download und die Installation abgeschlossen sind.

Install ESP32 Core libraries
ESP32 Core Bibliotheken installieren

Ich verwende hier die aktuelle Version 3.3.5, aber jede andere 3.x-Version sollte für dieses Projekt ebenfalls funktionieren.

Board auswählen

Du musst auch ein ESP32-Board auswählen. Für die DFRobot ESP32-S3 AI Camera kannst du das generische „ESP32S3 Dev Module“ wählen. Klicke dazu auf das Dropdown-Menü und dann auf „Select other board and port…“:

Drop-down Menu for Board Selection
Dropdown-Menü zur Board-Auswahl

Es öffnet sich ein Dialog, in dem du „esp32s3 dev“ in die Suchleiste eingibst. Du siehst das Board „ESP32S3 Dev Module“ unter Boards. Klicke darauf, wähle den COM-Port aus, um es zu aktivieren, und bestätige mit OK:

Board Selection Dialog "ESP32S3 Dev Module" board
Board-Auswahl-Dialog „ESP32S3 Dev Module“

Beachte, dass du das Board per USB-Kabel mit deinem Computer verbinden musst, bevor du einen COM-Port auswählen kannst.

Werkzeugeinstellungen

Unten findest du die Einstellungen, die du für das Board verwenden musst. Du findest sie im Tools-Menü deiner Arduino IDE.

Tool settings for DFRobot ESP32-S3 AI Camera
Werkzeugeinstellungen für DFRobot ESP32-S3 AI Camera

Die wichtigsten Einstellungen sind „16MB Flash Size“, „Huge APP partition“ und „OPI PSRAM“. Um Textausgaben im Serial Monitor zu sehen, stelle sicher, dass USB CDC on Boot „Enabled“ ist. Die anderen Einstellungen sind meist Standard und können so bleiben.

Bibliotheken installieren

Der Code für den Vision Chatbot verwendet zwei Bibliotheken, die ArduinoJson und die ESP32-audioI2S Bibliothek. Öffne den LIBRARY MANAGER, suche nach „ArduinoJson“ und „ESP32-audioI2S“ und klicke auf INSTALL, um diese Bibliotheken zu installieren:

Installing "ESP32-audioI2S" and "ArduinoJson" libraries
Installation der Bibliotheken „ESP32-audioI2S“ und „ArduinoJson“

Wie du siehst, habe ich Version 3.4.5 der ESP32-audioI2S Bibliothek und Version 7.4.2 der ArduinoJson Bibliothek installiert. Die genaue Version ist aber nicht entscheidend.

Code für den Vision Chatbot

In diesem Abschnitt zeige ich dir den Code für den Vision Chatbot. Er ist eine Neuimplementierung des OpenAI image recognition Beispiels, das du im DFRobot github repo findest, aber leider nicht zuverlässig funktioniert. Deshalb habe ich es komplett neu geschrieben.

Der Code beginnt damit, auf das Drücken der BOOT-Taste zu warten und nimmt Audio auf, bis die BOOT-Taste losgelassen wird oder mehr als 3 Sekunden vergangen sind. Die BOOT-Taste befindet sich unten rechts auf der Rückseite des Boards über der Reset-Taste (RST):

BOOT Button
BOOT-Taste

Anschließend sendet der Code das aufgenommene Audio an OpenAI’s Text-zu-Sprache-Modell zur Transkription, nimmt ein Bild mit der Kamera auf und fragt dann OpenAI’s visionfähiges GPT-Modell, um Fragen zum Bild zu beantworten. Die Antwort wird von OpenAI’s Sprache-zu-Text-Modell in Audio umgewandelt und über den kleinen Lautsprecher des ESP32-S3 AI Camera Moduls abgespielt.

Schau dir den Code zuerst kurz an, bevor wir die Details besprechen. Wenn du den Code nicht kopieren und einfügen möchtest, kannst du die vision_chatbot.zip Datei herunterladen, die den kompletten Code enthält. Du musst jedoch weiterhin die WLAN-Zugangsdaten und den OpenAI API-Schlüssel im Code setzen.

#include "WiFi.h"
#include "WiFiClientSecure.h"
#include "HTTPClient.h"
#include "ArduinoJson.h"
#include "esp_heap_caps.h"
#include "ESP_I2S.h"
#include "base64.h"
#include "camera.h"
#include "wav_header.h"
#include "Audio.h"

/* ===================== Pins ===================== */
#define BUTTON_PIN 0
#define LED_PIN 3

#define MIC_DATA_PIN 39
#define MIC_CLK_PIN 38

#define I2S_DOUT 42
#define I2S_BCLK 45
#define I2S_LRC 46

/* ===================== Audio ===================== */
#define SAMPLE_RATE 16000
#define MAX_RECORD_TIME_MS 3000
#define WAV_HEADER_SZ PCM_WAV_HEADER_SIZE

/* ===================== Models ===================== */
#define TTS_MODEL "tts-1"
#define TTS_VOICE "shimmer"
#define TTS_VOLUME 16
#define STT_MODEL "whisper-1"
#define VISION_MODEL "gpt-4o-mini"

/* ===================== Network ===================== */
const char* ssid = "ssid";
const char* password = "pwd";
const char* apiKey = "api-key";

/* ===================== Globals ===================== */
WiFiClientSecure secureClient;

I2SClass I2S;
Audio audio;

uint8_t* wavBuf = nullptr;
size_t wavSize = 0;
size_t wavMax = 0;

bool recording = false;
bool busy = false;
unsigned long recordStartMs = 0;


void initMic() {
  I2S.setPinsPdmRx(MIC_CLK_PIN, MIC_DATA_PIN);
  I2S.begin(I2S_MODE_PDM_RX, SAMPLE_RATE,
            I2S_DATA_BIT_WIDTH_16BIT,
            I2S_SLOT_MODE_MONO);      
}

void startRecording() {
  digitalWrite(LED_PIN, HIGH);

  wavMax = WAV_HEADER_SZ + SAMPLE_RATE * 2 * MAX_RECORD_TIME_MS / 1000;
  wavBuf = (uint8_t*)heap_caps_malloc(wavMax, MALLOC_CAP_SPIRAM);
  Serial.printf("[REC] WAV buffer allocated in PSRAM (%u bytes)\n", wavMax);

  if (!wavBuf) {
    Serial.println("[ERR] WAV buffer allocation failed");
    return;
  }

  pcm_wav_header_t hdr = PCM_WAV_HEADER_DEFAULT(0, 16, SAMPLE_RATE, 1);
  memcpy(wavBuf, &hdr, WAV_HEADER_SZ);

  wavSize = WAV_HEADER_SZ;
  recordStartMs = millis();
  recording = true;

  Serial.println("[REC] Recording started");
}

void pollRecording() {
  size_t avail = I2S.available();
  if (!avail) return;

  if (wavSize + avail > wavMax) {
    stopRecording();
    return;
  }

  wavSize += I2S.readBytes((char*)(wavBuf + wavSize), avail);
}

void stopRecording() {
  recording = false;

  if (!wavBuf || wavSize <= WAV_HEADER_SZ) {
    Serial.println("[ERR] No audio recorded");
    return;
  }

  pcm_wav_header_t* h = (pcm_wav_header_t*)wavBuf;
  h->descriptor_chunk.chunk_size = wavSize - 8;
  h->data_chunk.subchunk_size = wavSize - WAV_HEADER_SZ;

  Serial.printf("[REC] Recording stopped, %u bytes total\n", wavSize);
  digitalWrite(LED_PIN, LOW);
}

bool isValidWavBuffer() {
  if (!wavBuf || wavSize < WAV_HEADER_SZ) {
    Serial.println("[STT] Invalid WAV buffer");
    return false;
  }
  return true;
}

void releaseWavBuffer() {
  free(wavBuf);
  wavBuf = nullptr;
  wavSize = 0;
}

String buildMultipartHead(const char* boundary) {
  return String("--") + boundary + "\r\n"
       + "Content-Disposition: form-data; name=\"model\"\r\n\r\n"
       + STT_MODEL + "\r\n--"
       + boundary + "\r\n"
       + "Content-Disposition: form-data; name=\"file\"; filename=\"audio.wav\"\r\n"
       + "Content-Type: audio/wav\r\n\r\n";
}

String buildMultipartTail(const char* boundary) {
  return "\r\n--" + String(boundary) + "--\r\n";
}

uint8_t* buildMultipartBody(
  const String& head,
  const String& tail,
  size_t& outLen
) {
  outLen = head.length() + wavSize + tail.length();

  uint8_t* body =
    (uint8_t*)heap_caps_malloc(outLen, MALLOC_CAP_SPIRAM);

  if (!body) {
    Serial.println("[STT] Out of memory (multipart)");
    return nullptr;
  }

  memcpy(body, head.c_str(), head.length());
  memcpy(body + head.length(), wavBuf, wavSize);
  memcpy(body + head.length() + wavSize, tail.c_str(), tail.length());

  Serial.printf("[STT] Multipart body in PSRAM (%u bytes)\n", outLen);
  return body;
}

int postMultipart(
  uint8_t* body,
  size_t bodyLen,
  const char* boundary,
  String& response
) {
  HTTPClient http;

  http.begin(secureClient,
             "https://api.openai.com/v1/audio/transcriptions");
  http.addHeader("Authorization", String("Bearer ") + apiKey);
  http.addHeader("Content-Type",
                 String("multipart/form-data; boundary=") + boundary);

  Serial.printf("[STT] Uploading %u bytes\n", bodyLen);

  int code = http.POST(body, bodyLen);
  Serial.printf("[STT] HTTP code: %d\n", code);

  if (code == 200) {
    response = http.getString();
  } else {
    Serial.println("[STT] Error response:");
    Serial.println(http.getString());
  }

  http.end();
  return code;
}

String parseSttResponse(const String& resp) {
  StaticJsonDocument<512> doc;

  if (deserializeJson(doc, resp)) {
    Serial.println("[STT] JSON parse failed");
    return "";
  }

  return doc["text"] | "";
}

String speechToText() {
  Serial.println("[STT] Building multipart body");
  Serial.printf(
    "[HEAP] Internal heap free before TLS: %u\n",
    heap_caps_get_free_size(MALLOC_CAP_INTERNAL));

  if (!isValidWavBuffer()) {
    return "";
  }

  const char* boundary = "----ESP32Boundary";

  String head = buildMultipartHead(boundary);
  String tail = buildMultipartTail(boundary);

  size_t bodyLen = 0;
  uint8_t* body = buildMultipartBody(head, tail, bodyLen);
  if (!body) {
    return "";
  }

  releaseWavBuffer();

  String response;
  int code = postMultipart(body, bodyLen, boundary, response);
  free(body);

  if (code != 200) {
    return "";
  }

  return parseSttResponse(response);
}

void textToSpeech(String text) {
  busy = true;
  audio.openai_speech(apiKey, TTS_MODEL,
                      text.c_str(), "",
                      TTS_VOICE, "mp3", "1");
}

String visionAnswer(String question, camera_fb_t* fb) {
  Serial.println("[VISION] Encoding image...");
  String imageBase64 = base64::encode(fb->buf, fb->len);

  HTTPClient http;
  http.begin(secureClient, "https://api.openai.com/v1/chat/completions");
  http.addHeader("Authorization", String("Bearer ") + apiKey);
  http.addHeader("Content-Type", "application/json");

  StaticJsonDocument<3072> req;
  req["model"] = "gpt-4o-mini";

  JsonArray msgs = req.createNestedArray("messages");

  JsonObject system = msgs.createNestedObject();
  system["role"] = "system";
  system["content"] =
    "You are a helpful vision assistant. Analyze images and answer questions concisely";

  JsonObject user = msgs.createNestedObject();
  user["role"] = "user";

  JsonArray content = user.createNestedArray("content");

  JsonObject txt = content.createNestedObject();
  txt["type"] = "text";
  txt["text"] = question;

  JsonObject img = content.createNestedObject();
  img["type"] = "image_url";
  img["image_url"]["url"] =
    "data:image/jpeg;base64," + imageBase64;

  String body;
  serializeJson(req, body);

  Serial.println("[VISION] Sending request...");
  int code = http.POST(body);

  esp_camera_fb_return(fb);

  Serial.printf("[VISION] HTTP code: %d\n", code);

  if (code <= 0) {
    http.end();
    return "";
  }

  String payload = http.getString();
  http.end();

  // Serial.println("[VISION] Response:");
  // Serial.println(payload);

  StaticJsonDocument<1024> resp;
  if (deserializeJson(resp, payload)) {
    Serial.println("[ERR] Vision JSON parse failed");
    return "";
  }

  return resp["choices"][0]["message"]["content"] | "";
}

camera_fb_t* captureImage() {
  return esp_camera_fb_get();
}

void initSpeaker() {
  audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
  audio.setVolume(TTS_VOLUME);
}

void initPins() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
}

void initTime() {
  configTime(0, 0, "pool.ntp.org", "time.nist.gov");
  time_t now;
  while (time(&now) < 100000) delay(100);
}

void initWiFi() {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(200);
}

void initSerial() {
  Serial.begin(115200);
  delay(100);
}

void setup() {
  secureClient.setInsecure();

  initSerial();
  initPins();
  initWiFi();
  initTime();
  initMic();
  initSpeaker();
  initCamera();
  
  Serial.println("Ready");
}

void loop() {
  audio.loop();

  if (busy && !audio.isRunning()) {
    busy = false;
  }
  if (busy) return;

  static bool lastBtn = HIGH;
  bool btn = digitalRead(BUTTON_PIN);

  if (btn == LOW && lastBtn == HIGH && !recording) {
    startRecording();
  }

  if (recording) {
    pollRecording();

    bool released = (btn == HIGH && lastBtn == LOW);
    bool timeout = (millis() - recordStartMs >= MAX_RECORD_TIME_MS);

    if (released || timeout) {
      stopRecording();
      String q = speechToText();
      if (q.length()) {
        Serial.printf("[STT] %s\n", q.c_str());
        camera_fb_t* fb = captureImage();
        if (fb) {
          String a = visionAnswer(q, fb);
          Serial.printf("[VISION] %s\n", a.c_str());
          if (a.length()) {
            textToSpeech(a);
          }
        }
      }
    }
  }

  lastBtn = btn;
  delay(5);
}

Imports

Der Code beginnt mit dem Einbinden notwendiger Bibliotheken für WLAN-Verbindung, sichere HTTP-Kommunikation, JSON-Verarbeitung, Kamerasteuerung, Audiobearbeitung und Base64-Codierung. Diese Bibliotheken ermöglichen dem ESP32-S3 die Interaktion mit Hardware und die Kommunikation mit OpenAI’s Cloud-APIs.

#include "WiFi.h"
#include "WiFiClientSecure.h"
#include "HTTPClient.h"
#include "ArduinoJson.h"
#include "esp_heap_caps.h"
#include "ESP_I2S.h"
#include "base64.h"
#include "camera.h"
#include "wav_header.h"
#include "Audio.h"

Pins und Audio-Konfiguration

Mehrere Konstanten definieren die GPIO-Pins für Taste, LED, Mikrofon-Daten und -Takt sowie I2S Audio-Ausgangspins. Audio-Parameter wie Abtastrate, maximale Aufnahmezeit und WAV-Header-Größe sind ebenfalls definiert, um Audioaufnahme und -wiedergabe zu konfigurieren.

#define BUTTON_PIN 0
#define LED_PIN 3

#define MIC_DATA_PIN 39
#define MIC_CLK_PIN 38

#define I2S_DOUT 42
#define I2S_BCLK 45
#define I2S_LRC 46

#define SAMPLE_RATE 16000
#define MAX_RECORD_TIME_MS 3000
#define WAV_HEADER_SZ PCM_WAV_HEADER_SIZE

Modelle und Netzwerk-Zugangsdaten

Der Code spezifiziert die OpenAI-Modelle für Text-zu-Sprache (TTS), Sprache-zu-Text (STT) und Vision-Fragenbeantwortung. Außerdem werden WLAN-SSID, Passwort und OpenAI API-Schlüssel als Konstanten für Netzwerkverbindung und API-Authentifizierung gespeichert.

#define TTS_MODEL "tts-1"
#define TTS_VOICE "shimmer"
#define TTS_VOLUME 16
#define STT_MODEL "whisper-1"
#define VISION_MODEL "gpt-4o-mini"

const char* ssid = "ssid";
const char* password = "pwd";
const char* apiKey = "apikey";

Du musst die Platzhalterwerte für ssid, password und OpenAI apiKey durch die korrekten Werte deines WLAN-Netzwerks und deines OpenAI API-Schlüssels ersetzen. Andernfalls kann der Vision Chatbot nicht mit den KI-Modellen von OpenAI kommunizieren und funktioniert nicht.

Globale Variablen und Objekte

Der Code deklariert einen sicheren WiFi-Client für HTTPS-Kommunikation, ein I2S Audio-Interface-Objekt und ein Audio-Objekt für die Wiedergabe. Außerdem verwaltet er einen Puffer für aufgezeichnete WAV-Audiodaten, Flags für Aufnahme- und Busy-Zustände sowie Zeitvariablen zur Steuerung der Aufnahmezeit.

WiFiClientSecure secureClient;

I2SClass I2S;
Audio audio;

uint8_t* wavBuf = nullptr;
size_t wavSize = 0;
size_t wavMax = 0;

bool recording = false;
bool busy = false;
unsigned long recordStartMs = 0;

Mikrofon-Initialisierung

Die initMic() Funktion konfiguriert das I2S-Peripheriegerät, um PDM-Mikrofon-Daten über die angegebenen Takt- und Datenpins zu empfangen. Sie setzt Abtastrate, Datenbitbreite und Mono-Kanal-Modus, um die Audioaufnahme vorzubereiten.

void initMic() {
  I2S.setPinsPdmRx(MIC_CLK_PIN, MIC_DATA_PIN);
  I2S.begin(I2S_MODE_PDM_RX, SAMPLE_RATE,
            I2S_DATA_BIT_WIDTH_16BIT,
            I2S_SLOT_MODE_MONO);      
}

Audio-Aufnahme-Steuerung

Die startRecording() Funktion startet die Audioaufnahme, schaltet die LED-Anzeige ein und reserviert einen Puffer im PSRAM für die WAV-Daten. Sie schreibt einen Standard-WAV-Header in den Puffer, speichert die Startzeit und setzt das Aufnahme-Flag.

void startRecording() {
  digitalWrite(LED_PIN, HIGH);

  wavMax = WAV_HEADER_SZ + SAMPLE_RATE * 2 * MAX_RECORD_TIME_MS / 1000;
  wavBuf = (uint8_t*)heap_caps_malloc(wavMax, MALLOC_CAP_SPIRAM);
  Serial.printf("[REC] WAV buffer allocated in PSRAM (%u bytes)\n", wavMax);

  if (!wavBuf) {
    Serial.println("[ERR] WAV buffer allocation failed");
    return;
  }

  pcm_wav_header_t hdr = PCM_WAV_HEADER_DEFAULT(0, 16, SAMPLE_RATE, 1);
  memcpy(wavBuf, &hdr, WAV_HEADER_SZ);

  wavSize = WAV_HEADER_SZ;
  recordStartMs = millis();
  recording = true;

  Serial.println("[REC] Recording started");
}

Die pollRecording() Funktion liest verfügbare Audiodaten vom I2S-Peripheriegerät und fügt sie dem WAV-Puffer hinzu. Wenn der Puffer voll ist, stoppt sie die Aufnahme automatisch.

void pollRecording() {
  size_t avail = I2S.available();
  if (!avail) return;

  if (wavSize + avail > wavMax) {
    stopRecording();
    return;
  }

  wavSize += I2S.readBytes((char*)(wavBuf + wavSize), avail);
}

Die stopRecording() Funktion finalisiert die WAV-Daten, indem sie den Header mit den korrekten Größen aktualisiert, das Aufnahme-Flag zurücksetzt und die LED-Anzeige ausschaltet.

void stopRecording() {
  recording = false;

  if (!wavBuf || wavSize <= WAV_HEADER_SZ) {
    Serial.println("[ERR] No audio recorded");
    return;
  }

  pcm_wav_header_t* h = (pcm_wav_header_t*)wavBuf;
  h->descriptor_chunk.chunk_size = wavSize - 8;
  h->data_chunk.subchunk_size = wavSize - WAV_HEADER_SZ;

  Serial.printf("[REC] Recording stopped, %u bytes total\n", wavSize);
  digitalWrite(LED_PIN, LOW);
}

Multipart-Anfrage für Speech-to-Text

Um das aufgenommene Audio an OpenAI’s Speech-to-Text-Modell zu senden, baut der Code einen multipart/form-data HTTP-Anfragekörper auf. Die buildMultipartHead() und buildMultipartTail() Funktionen erzeugen die Multipart-Grenzen und Header, während buildMultipartBody() den kompletten Anfragekörper im PSRAM zusammensetzt, indem Kopf, WAV-Daten und Fuß zusammengefügt werden.

String buildMultipartHead(const char* boundary) {
  return String("--") + boundary + "\r\n"
       + "Content-Disposition: form-data; name=\"model\"\r\n\r\n"
       + STT_MODEL + "\r\n--"
       + boundary + "\r\n"
       + "Content-Disposition: form-data; name=\"file\"; filename=\"audio.wav\"\r\n"
       + "Content-Type: audio/wav\r\n\r\n";
}

String buildMultipartTail(const char* boundary) {
  return "\r\n--" + String(boundary) + "--\r\n";
}

uint8_t* buildMultipartBody(
  const String& head,
  const String& tail,
  size_t& outLen
) {
  outLen = head.length() + wavSize + tail.length();

  uint8_t* body =
    (uint8_t*)heap_caps_malloc(outLen, MALLOC_CAP_SPIRAM);

  if (!body) {
    Serial.println("[STT] Out of memory (multipart)");
    return nullptr;
  }

  memcpy(body, head.c_str(), head.length());
  memcpy(body + head.length(), wavBuf, wavSize);
  memcpy(body + head.length() + wavSize, tail.c_str(), tail.length());

  Serial.printf("[STT] Multipart body in PSRAM (%u bytes)\n", outLen);
  return body;
}

Die Konstante STT_MODEL gibt das verwendete Speech-to-Text-Modell an. Ich nutze hier „whisper-1“, aber OpenAI bietet auch andere Speech-to-Text Modelle wie „gpt-4o-mini-transcribe“, „gpt-4o-transcribe“ oder „gpt-4o-transcribe-diarize“, die du ausprobieren kannst.

HTTP POST für Speech-to-Text

Die postMultipart() Funktion führt die HTTPS POST-Anfrage an OpenAI’s Audio-Transkriptions-Endpunkt aus. Sie setzt die Autorisierungs- und Content-Type-Header, lädt den Multipart-Body hoch und erhält die Antwort. Die Funktion gibt den HTTP-Statuscode zurück und speichert die Antwort als String.

int postMultipart(
  uint8_t* body,
  size_t bodyLen,
  const char* boundary,
  String& response
) {
  HTTPClient http;

  http.begin(secureClient,
             "https://api.openai.com/v1/audio/transcriptions");
  http.addHeader("Authorization", String("Bearer ") + apiKey);
  http.addHeader("Content-Type",
                 String("multipart/form-data; boundary=") + boundary);

  Serial.printf("[STT] Uploading %u bytes\n", bodyLen);

  int code = http.POST(body, bodyLen);
  Serial.printf("[STT] HTTP code: %d\n", code);

  if (code == 200) {
    response = http.getString();
  } else {
    Serial.println("[STT] Error response:");
    Serial.println(http.getString());
  }

  http.end();
  return code;
}

Speech-to-Text Verarbeitung

Die speechToText() Funktion steuert den Prozess des Multipart-Aufbaus, Sendens und Parsens der JSON-Antwort, um den transkribierten Text zu extrahieren. Sie verwaltet auch den Speicher, indem sie den WAV-Puffer nach dem Senden freigibt.

String speechToText() {
  Serial.println("[STT] Building multipart body");
  Serial.printf(
    "[HEAP] Internal heap free before TLS: %u\n",
    heap_caps_get_free_size(MALLOC_CAP_INTERNAL));

  if (!isValidWavBuffer()) {
    return "";
  }

  const char* boundary = "----ESP32Boundary";

  String head = buildMultipartHead(boundary);
  String tail = buildMultipartTail(boundary);

  size_t bodyLen = 0;
  uint8_t* body = buildMultipartBody(head, tail, bodyLen);
  if (!body) {
    return "";
  }

  releaseWavBuffer();

  String response;
  int code = postMultipart(body, bodyLen, boundary, response);
  free(body);

  if (code != 200) {
    return "";
  }

  return parseSttResponse(response);
}

Vision-Fragenbeantwortung

Die visionAnswer() Funktion sendet eine Frage zusammen mit einem aufgenommenen Bild an OpenAI’s GPT-4o-mini Modell für visionbasierte Fragenbeantwortung. Sie kodiert das Bild in Base64, erstellt eine JSON-Chat-Completion-Anfrage mit System- und Benutzer-Nachrichten und parst die Antwort, um die Antwort des Assistenten zu extrahieren.

String visionAnswer(String question, camera_fb_t* fb) {
  Serial.println("[VISION] Encoding image...");
  String imageBase64 = base64::encode(fb->buf, fb->len);

  HTTPClient http;
  http.begin(secureClient, "https://api.openai.com/v1/chat/completions");
  http.addHeader("Authorization", String("Bearer ") + apiKey);
  http.addHeader("Content-Type", "application/json");

  StaticJsonDocument<3072> req;
  req["model"] = VISION_MODEL;

  JsonArray msgs = req.createNestedArray("messages");

  JsonObject system = msgs.createNestedObject();
  system["role"] = "system";
  system["content"] =
    "You are a helpful vision assistant. Analyze images and answer questions concisely";

  JsonObject user = msgs.createNestedObject();
  user["role"] = "user";

  JsonArray content = user.createNestedArray("content");

  JsonObject txt = content.createNestedObject();
  txt["type"] = "text";
  txt["text"] = question;

  JsonObject img = content.createNestedObject();
  img["type"] = "image_url";
  img["image_url"]["url"] =
    "data:image/jpeg;base64," + imageBase64;

  String body;
  serializeJson(req, body);

  Serial.println("[VISION] Sending request...");
  int code = http.POST(body);

  esp_camera_fb_return(fb);

  Serial.printf("[VISION] HTTP code: %d\n", code);

  if (code <= 0) {
    http.end();
    return "";
  }

  String payload = http.getString();
  http.end();

  StaticJsonDocument<1024> resp;
  if (deserializeJson(resp, payload)) {
    Serial.println("[ERR] Vision JSON parse failed");
    return "";
  }

  return resp["choices"][0]["message"]["content"] | "";
}

Die Konstante VISION_MODEL gibt das verwendete Modell an. Ich nutze „gpt-4o-mini“, aber es gibt auch andere wie „gpt-image-1“, „gpt-5-mini“, „gpt-5-nano“ oder „gpt-4.1-nano“, die unterschiedliche Fähigkeiten, Geschwindigkeiten und Kosten haben. OpenAI vision model

Text-zu-Sprache Wiedergabe

Die textToSpeech() Funktion verwendet die ESP32-audioI2S Bibliothek, um Sprachsynthese vom OpenAI TTS-Modell anzufordern. Sie setzt das Busy-Flag, um Überlappungen während der Audiowiedergabe zu verhindern.

void textToSpeech(String text) {
  busy = true;
  audio.openai_speech(apiKey, TTS_MODEL,
                      text.c_str(), "",
                      TTS_VOICE, "mp3", "1");
}

Die Konstante TTS_MODEL gibt das verwendete Text-zu-Sprache-Modell an. Ich nutze „tts-1“, du kannst aber auch „tts-1-hd“ verwenden. Das „tts-1“-Modell bietet geringere Latenz, aber eine niedrigere Qualität als „tts-1-hd“. Ein intelligenteres, aber auch teureres Modell ist „gpt-4o-mini-tts“, das du ebenfalls nutzen kannst.

Die Stimme für die Audioausgabe wird durch die Konstante TTS_VOICE bestimmt, die auf „shimmer“ gesetzt ist. Du kannst andere Stimmen ausprobieren, beachte aber, dass die Verfügbarkeit von Stimmen vom Modell abhängt. Die Modelle tts-1 und tts-1-hd unterstützen eine kleinere Auswahl an Stimmen: „alloy“, „ash“, „coral“, „echo“, „fable“, „onyx“, „nova“, „sage“ und „shimmer“ (platform.openai.com/docs/guides/text-to-speech).

Kameraaufnahme

Die captureImage() Funktion nimmt ein Bild mit der ESP32-Kamera auf und gibt einen Zeiger auf den Frame-Puffer für die weitere Verarbeitung zurück.

camera_fb_t* captureImage() {
  return esp_camera_fb_get();
}

Hardware-Initialisierung

Mehrere Hilfsfunktionen initialisieren Hardwarekomponenten und Systemdienste. initSpeaker() konfiguriert die Audio-Ausgangspins und die Lautstärke. initPins() richtet die GPIO-Modi für Taste und LED ein. initTime() synchronisiert die Systemzeit über NTP-Server. initWiFi() verbindet sich mit dem angegebenen WLAN-Netzwerk. initSerial() startet die serielle Kommunikation für Debugging.

void initSpeaker() {
  audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
  audio.setVolume(TTS_VOLUME);
}

void initPins() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
}

void initTime() {
  configTime(0, 0, "pool.ntp.org", "time.nist.gov");
  time_t now;
  while (time(&now) < 100000) delay(100);
}

void initWiFi() {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(200);
}

void initSerial() {
  Serial.begin(115200);
  delay(100);
}

Setup-Funktion

Die setup() Funktion wird einmal beim Start aufgerufen. Sie konfiguriert den sicheren Client, um Zertifikatsprüfung zu überspringen, initialisiert serielle Kommunikation, GPIO-Pins, WLAN, Systemzeit, Mikrofon, Lautsprecher und Kamera. Zum Schluss gibt sie „Ready“ aus, um anzuzeigen, dass das System bereit ist.

void setup() {
  secureClient.setInsecure();

  initSerial();
  initPins();
  initWiFi();
  initTime();
  initMic();
  initSpeaker();
  initCamera();

  Serial.println("Ready");
}

Loop-Funktion

Die loop() Funktion läuft wiederholt. Sie verarbeitet die Audiowiedergabe im Hintergrund. Wenn das System gerade Audio abspielt, wartet sie. Andernfalls liest sie den Tastenstatus, um Drücken und Loslassen zu erkennen.

Beim Drücken der Taste startet die Aufnahme. Während der Aufnahme werden Audiodaten abgefragt und dem Puffer hinzugefügt. Die Aufnahme endet, wenn die Taste losgelassen wird oder die maximale Aufnahmezeit erreicht ist.

Nach dem Stoppen wird das aufgenommene Audio zur Transkription gesendet. Wenn die Transkription erfolgreich ist, nimmt die Kamera ein Bild auf, und die Frage wird an das Vision-Modell gesendet. Die Antwort wird dann in Sprache umgewandelt und abgespielt. Die Schleife enthält eine kleine Verzögerung zum Entprellen der Taste.

void loop() {
  audio.loop();

  if (busy && !audio.isRunning()) {
    busy = false;
  }
  if (busy) return;

  static bool lastBtn = HIGH;
  bool btn = digitalRead(BUTTON_PIN);

  if (btn == LOW && lastBtn == HIGH && !recording) {
    startRecording();
  }

  if (recording) {
    pollRecording();

    bool released = (btn == HIGH && lastBtn == LOW);
    bool timeout = (millis() - recordStartMs >= MAX_RECORD_TIME_MS);

    if (released || timeout) {
      stopRecording();
      String q = speechToText();
      if (q.length()) {
        Serial.printf("[STT] %s\n", q.c_str());
        camera_fb_t* fb = captureImage();
        if (fb) {
          String a = visionAnswer(q, fb);
          Serial.printf("[VISION] %s\n", a.c_str());
          if (a.length()) {
            textToSpeech(a);
          }
        }
      }
    }
  }

  lastBtn = btn;
  delay(5);
}

Und das ist der komplette Code für den Vision Chatbot selbst. Wir benötigen jedoch auch noch Code für die Kamera, der im nächsten Abschnitt beschrieben wird.

Camera.h Code

Der Kameracode ist im Wesentlichen eine Kopie der „camera.h“-Datei aus dem OpenAI image recognition Beispiel, das du im DFRobot github repo findest.

#include "esp_camera.h"
#include "soc/soc.h"           // Disable brownout problems
#include "soc/rtc_cntl_reg.h"  // Disable brownout problems
#include <Arduino.h>

// OV2640 camera module pins (CAMERA_MODEL_AI_THINKER)
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 5
#define SIOD_GPIO_NUM 8
#define SIOC_GPIO_NUM 9
#define Y9_GPIO_NUM 4
#define Y8_GPIO_NUM 6
#define Y7_GPIO_NUM 7
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 17
#define Y4_GPIO_NUM 21
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 16
#define VSYNC_GPIO_NUM 1
#define HREF_GPIO_NUM 2
#define PCLK_GPIO_NUM 15

void initCamera() {
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 8000000;
  config.frame_size = FRAMESIZE_240X240;
  config.pixel_format = PIXFORMAT_JPEG;  // for streaming
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 2;

  // if PSRAM IC present, init with UXGA resolution and higher JPEG quality
  //                      for larger pre-allocated frame buffer.
  if (config.pixel_format == PIXFORMAT_JPEG) {
    if (psramFound()) {
      config.jpeg_quality = 10;
      config.fb_count = 2;
      config.grab_mode = CAMERA_GRAB_LATEST;
    } else {
      // Limit the frame size when PSRAM is not available
      config.frame_size = FRAMESIZE_SVGA;
      config.fb_location = CAMERA_FB_IN_DRAM;
    }
  } else {
    // Best option for face detection/recognition
    config.frame_size = FRAMESIZE_240X240;
#if CONFIG_IDF_TARGET_ESP32S3
    config.fb_count = 2;
#endif
  }

  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  sensor_t *s = esp_camera_sensor_get();
  // initial sensors are flipped vertically and colors are a bit saturated
  if (s->id.PID == OV3660_PID) {
    s->set_vflip(s, 1);        // flip it back
    s->set_brightness(s, 1);   // up the brightness just a bit
    s->set_saturation(s, -2);  // lower the saturation
  }
  // drop down frame size for higher initial frame rate
  if (config.pixel_format == PIXFORMAT_JPEG) {
    s->set_framesize(s, FRAMESIZE_QVGA);
  }
}

Ich habe jedoch eine wichtige Änderung vorgenommen. Ich bemerkte, dass beim Betrieb des Vision Chatbots „ cam_hal: FB-OVF “ Fehler im Serial Monitor ausgegeben wurden.

„FB-OVF“ steht für Frame Buffer Overflow. Das bedeutet im Wesentlichen, dass die Kamera Daten schneller sendet, als der ESP32 sie verarbeiten oder im Speicher ablegen kann.

Die empfohlene Methode, diesen Fehler zu vermeiden, ist die Reduzierung der Bildrate. Ich habe daher den ursprünglichen Kameracode geändert und die Bildrate auf 8 MHz gesetzt:

config.xclk_freq_hz = 8000000;

Das beseitigte die „cam_hal: FB-OVF“ Fehler.

Projektordner

Du kannst die gesamte Projektdatei vision_chatbot.zip herunterladen oder das Arduino-Projekt für den Vision Chatbot selbst erstellen. Erstelle dazu einen Ordner „vision_chatbot“ mit zwei Dateien („camera.h“, „vision_chatbot.ino“):

Die „camera.h“-Datei enthält den Code für die Kamera, und „vision_chatbot.ino“ enthält den Code für den Vision Chatbot. Sobald du das richtige Board („ESP32S3 Dev Module“) und die korrekten Werkzeugeinstellungen (PSRAM, Huge APP, …) ausgewählt hast, kannst du den Code auf das Board flashen und deinen Vision Chatbot in Aktion erleben. Die nächsten zwei Abschnitte zeigen zwei Beispiele mit den Antworten des Vision Chatbots.

Beispiel: Was siehst du?

In diesem ersten Beispiel zeigte ich der ESP32-S3 AI Camera einen kleinen Modellschädel auf meinem Schreibtisch mit etwas anderem im Hintergrund.

Und hier ist die Ausgabe des Vision Chatbots im Serial Monitor, als er dieses Bild sah:

13:22:02.672 -> E (1763) i2s_common: i2s_channel_disable(1217): the channel has not been enabled yet
13:22:03.049 -> Ready
13:25:49.615 -> [REC] WAV buffer allocated in PSRAM (96044 bytes)
13:25:49.615 -> [REC] Recording started
13:25:51.288 -> [REC] Recording stopped, 55724 bytes total
13:25:51.288 -> [STT] Building multipart body
13:25:51.288 -> [HEAP] Internal heap free before TLS: 181256
13:25:51.288 -> [STT] Multipart body in PSRAM (55944 bytes)
13:25:51.288 -> [STT] Uploading 55944 bytes
13:25:52.847 -> [STT] HTTP code: 200
13:25:52.847 -> [STT] What do you see?
13:25:52.847 -> [VISION] Encoding image...
13:25:52.847 -> [VISION] Sending request...
13:25:54.995 -> [VISION] HTTP code: 200
13:25:55.032 -> [VISION] I see a small skull placed on a light-colored surface, likely a table. In the background, there are various objects and possibly some clutter. The setting appears to be an indoor space.

Du siehst die gestellte Frage „What do you see?“ und die Antwort des Chatbots „I see a small skull placed on a light-colored surface, likely a table. In the background, there are various objects and possibly some clutter. The setting appears to be an indoor space.“

Beachte, dass am Anfang eine Fehlermeldung „E (1763) i2s_common: i2s_channel_disable(1217): the channel has not been enabled yet“ erscheint, die du ignorieren kannst. Sie scheint mit einem Problem im aktuellen ESP32 Core zusammenzuhängen, beeinträchtigt aber nicht die Funktion des Chatbots.

Beispiel: Wie viele Uhren?

Du kannst den Chatbot auch nach bestimmten Objekten in einem Bild fragen. Das funktioniert nicht immer, und der Bot hatte z. B. Probleme, Stühle zu erkennen. In der unten gezeigten recht komplexen Szene mit einer Wanduhr erkannte der Chatbot jedoch korrekt, dass eine Uhr vorhanden ist:

Hier die Ausgabe im Serial Monitor. Ich fragte „How many clocks are in this image?“ und die Antwort war „There is one clock visible in the image.“

15:25:00.656 -> Ready
15:28:28.984 -> [REC] WAV buffer allocated in PSRAM (96044 bytes)
15:28:28.984 -> [REC] Recording started
15:28:31.817 -> [REC] Recording stopped, 92204 bytes total
15:28:31.817 -> [STT] Building multipart body
15:28:31.817 -> [HEAP] Internal heap free before TLS: 181256
15:28:31.817 -> [STT] Multipart body in PSRAM (92424 bytes)
15:28:31.817 -> [STT] Uploading 92424 bytes
15:28:33.445 -> [STT] HTTP code: 200
15:28:33.445 -> [STT] How many clocks are in this image?
15:28:33.445 -> [VISION] Encoding image...
15:28:33.445 -> [VISION] Sending request...
15:28:34.953 -> [VISION] HTTP code: 200
15:28:34.989 -> [VISION] There is one clock visible in the image.

Angesichts der niedrigen Qualität, der kleinen Bildgröße und der vielen Objekte im Bild war der Vision Chatbot wirklich gut darin, die Uhr zu finden.

Viel Spaß mit deinem Vision Chatbot, aber behalte die OpenAI-Kosten und dein Budget im Auge, besonders wenn du leistungsfähigere, aber auch teurere Modelle nutzt.

Fazit

In diesem Tutorial hast du gelernt, wie man einen Vision Chatbot mit dem DFRobot ESP32-S3 AI Camera Modul und OpenAI baut. Audioaufnahme, Wiedergabe und Bildaufnahme erfolgen lokal auf dem ESP32, während Text-zu-Sprache, Sprache-zu-Text und Bildanalyse remote von OpenAI-Diensten durchgeführt werden.

Die Remote-Verarbeitung von Bild- und Audiodaten ermöglicht den Einsatz deutlich leistungsfähigerer KI-Modelle, als lokal auf dem ESP32 möglich wären. Das erfordert jedoch eine stabile Internetverbindung zur Kommunikation mit OpenAI’s Cloud-Diensten und bringt Latenz sowie Kosten mit sich. Zudem ist die Privatsphäre ein Thema, wenn Bilder an externe Server gesendet werden.

Wenn du einfache Spracherkennung lokal durchführen möchtest, schau dir die Getting started with Gravity Voice Recognition Module, Voice control with XIAO-ESP32-S3-Sense and Edge Impulse und Using the Voice Recognition Module V3 with Arduino Tutorials an.

Für Sprache-zu-Text siehe das Gravity Text-to-Speech Module Tutorial, das Sprache lokal erzeugen kann, aber mit begrenzter Qualität.

Für weitere Codebeispiele zur DFRobot ESP32-S3 AI Camera schau dir das DFRobot Wiki und deren github repo an.

Wenn du Fragen hast, hinterlasse sie gerne im Kommentarbereich.

Viel Spaß beim Tüfteln ; )