Skip to Content

Chatbot Vision avec DFRobot ESP32-S3 AI Camera et OpenAI

Chatbot Vision avec DFRobot ESP32-S3 AI Camera et OpenAI

La DFRobot ESP32-S3 AI Camera (DFR1154) est une carte de développement conçue pour les projets d’IA et de vision. Elle intègre un microcontrôleur ESP32-S3 ainsi qu’un module caméra, un microphone et un haut-parleur sur une seule carte.

Dans ce projet, nous allons créer un chatbot visuel utilisant la carte ESP32-S3 AI Camera et OpenAI. La carte capture des images et écoute les questions orales via le microphone. Elle convertit ensuite la parole en texte grâce à OpenAI. Ensuite, elle envoie à un autre modèle OpenAI à la fois l’image et la question textuelle pour analyse. Ce modèle d’IA traite les données, génère une réponse concernant l’image, et renvoie la réponse sous forme de texte. Enfin, nous utilisons OpenAI une troisième fois pour convertir le texte de la réponse en parole et la diffuser via le haut-parleur.

Le court clip vidéo ci-dessous montre le chatbot visuel en action. Je tiens la carte à gauche et sa caméra est dirigée vers un petit crâne modèle sur mon bureau. J’appuie sur le bouton BOOT de la carte pour démarrer le chatbot visuel, et vous entendrez ma question ainsi que la réponse du bot.

Ajustez le volume si vous n’entendez pas l’audio et notez qu’il y a un délai de quelques secondes entre la question et la réponse en raison du transfert de données et du traitement chez OpenAI :

Chatbot Visuel en action

L’enregistrement audio, la lecture audio et la capture d’image sont gérés localement par la carte ESP32-S3 AI Camera. Mais l’analyse d’image, la synthèse vocale (TTS) et la reconnaissance vocale (STT) utilisent les modèles d’IA d’OpenAI. Une connexion WiFi et une clé API OpenAI sont donc nécessaires.

Pièces requises

Pour ce projet, j’utilise le module ESP32-S3 AI Camera (DFR1154) de DFRobot. Vous pouvez l’obtenir chez DFRobot via les liens ci-dessous. Assurez-vous de prendre la version 1.1 et non l’ancienne version 1.0. Un câble USB-C peut également être nécessaire.

DFRobot ESP32-S3 AI Camera

Câble USB-C

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.

Matériel de la DFRobot AI Camera

La DFRobot ESP32-S3 AI Camera (DFR1154) est une carte compacte de développement embarqué pour vision et IA, basée sur le microcontrôleur ESP32-S3 d’Espressif. Elle intègre connectivité sans fil, imagerie caméra, entrée/sortie audio et capacités d’accélération IA dans un seul module. Cette carte est conçue pour des tâches de calcul en périphérie telles que la reconnaissance d’objets, l’interaction vocale et l’analyse visuelle en temps réel.

Son format physique est une carte de circuit imprimé carrée d’environ 42 mm de côté, ce qui la rend adaptée à l’intégration dans la robotique, les capteurs intelligents et les systèmes de surveillance IoT. L’image ci-dessous montre le recto et le verso de la carte :

Front and Back of DFRobot ESP32-S3 AI Camera (source)
Recto et verso de la DFRobot ESP32-S3 AI Camera (source)

Microcontrôleur et architecture mémoire

Au cœur de la carte, le microcontrôleur ESP32-S3 exécute le code applicatif et gère les communications. Ce microcontrôleur dispose d’un CPU Tensilica Xtensa® dual-core 32 bits LX7 cadencé jusqu’à 240 MHz. Il inclut une SRAM embarquée pour un accès rapide aux données et instructions ainsi qu’une SRAM RTC dédiée pour les opérations horloge basse consommation.

La mémoire intégrée est complétée par un stockage externe : la carte offre 16 Mo de mémoire flash pour le firmware et les données, ainsi qu’une puce PSRAM de 8 Mo pour supporter des tas d’exécution plus grands nécessaires au buffering d’images ou à l’exécution de modèles IA. L’interface USB est conforme à USB 2.0 OTG full-speed, permettant à la fois alimentation et transfert de données.

Sous-système caméra

Le sous-système d’imagerie est centré sur un capteur caméra OmniVision OV3660. Ce capteur capture des images jusqu’à 2 mégapixels et est sensible à la lumière visible ainsi qu’à l’infrarouge 940 nm, ce qui étend la plage d’utilisation en conditions de faible luminosité ou vision nocturne.

L’optique offre un champ de vision d’environ 160 degrés, et le système à focale fixe a une longueur focale d’environ 0,95 mm avec une ouverture proche de f/2.0. Quatre LED IR sont également présentes pour l’illumination.

Sans fil et connectivité

La carte supporte une communication sans fil en double mode. Pour la connectivité réseau locale, elle implémente le Wi-Fi IEEE 802.11b/g/n dans la bande 2,4 GHz, avec prise en charge des canaux 20 MHz et 40 MHz et plusieurs modes opérationnels incluant station, point d’accès soft, et modes combinés station+AP. Le Bluetooth est disponible selon les protocoles Bluetooth 5 et Bluetooth Mesh, permettant une communication basse consommation entre pairs ou la participation à un réseau de capteurs.

Interfaces audio et capteurs

En plus de l’imagerie, la carte dispose d’un microphone PDM I2S intégré pour la capture audio, acheminé via un amplificateur interne (MAX98357) et exposé à une interface haut-parleur dédiée. Un port carte SD permet également le stockage de données audio et vidéo.

Le capteur de lumière ambiante LTR-308 permet d’adapter l’imagerie ou la gestion de l’énergie selon l’éclairage ambiant. Ceci est particulièrement utile en lien avec les 4 LED IR présentes pour l’illumination.

La carte offre une interface Gravity 4 broches (3,3 V, GND, GPIO44/RX, GPIO43/TX) qui fournit une connectivité UART/I2C simple vers des périphériques ou capteurs externes. Notez que dans la version antérieure V1.0 de la carte, la broche 1 de l’interface Gravity était une entrée 3,3-5 V. Mais dans la version actuelle V1.1, la broche 1 est une sortie 3,3 V !

Broches GPIO

Le tableau ci-dessous liste les broches GPIO et leur affectation aux différents composants matériels tels que la caméra (CAM), le microphone (MIC), l’amplificateur audio (MAX98357), le capteur de lumière ambiante (ALS), la carte SD (SD), les LED IR, le bouton BOOT et la LED d’état :

Pinout de la DFRobot ESP32-S3 AI Camera (source)

Gestion de l’alimentation et design physique

Le DFR1154 accepte plusieurs configurations d’alimentation. Il fonctionne nominalement à 3,3 V, et peut recevoir une alimentation via un connecteur USB-C à 5 V DC ou via un connecteur VIN avec une plage plus large de 3,7 V à 15 V DC. Un circuit intégré de gestion d’alimentation dédié (HM6245) régule ces entrées vers les tensions nécessaires au cœur.

La plage de température de fonctionnement est spécifiée d’environ -10 °C à 60 °C, ce qui est prévu pour des environnements intérieurs standards ou extérieurs protégés.

Spécifications techniques

Le tableau suivant résume les principales spécifications matérielles du module DFRobot ESP32-S3 AI Camera (DFR1154) (source):

Spécification Détails
Microcontrôleur ESP32-S3R8 avec CPU dual-core Tensilica Xtensa LX7, 240 MHz
SRAM 512 Ko
ROM 384 Ko
Flash externe 16 Mo
PSRAM externe 8 Mo
SRAM RTC 16 Ko
Capteur caméra OV3660, 2 MP, FoV 160°, support infrarouge
Optique Focale fixe 0,95 mm, ouverture f/2.0, distorsion <8 %
Sans fil Wi-Fi 802.11b/g/n (2,4 GHz) ; Bluetooth 5 & Mesh
Audio Microphone I2S PDM ; interface haut-parleur via amplificateur MAX98357
Capteurs Capteur de lumière ambiante LTR-308
USB USB 2.0 OTG Full Speed (Type-C)
Alimentation Tension de fonctionnement 3,3 V ; USB-C 5 V ; VIN 3,7–15 V
Boutons Reset et Boot
Dimensions 42 mm × 42 mm
Température de fonctionnement -10 °C à 60 °C

Obtenir une clé API OpenAI

Le chatbot visuel utilise des modèles d’IA fournis par OpenAI. Vous aurez donc besoin d’un compte OpenAI. Rendez-vous sur https://platform.openai.com et inscrivez-vous avec une adresse email ou un compte Google ou Microsoft existant.

Après avoir vérifié votre email et complété la configuration initiale, connectez-vous au tableau de bord OpenAI, platform.openai.com/api-keys et trouvez ou créez votre clé API (=CLÉ SECRÈTE) comme montré ci-dessous :

OpenAI API keys
Clés API OpenAI

La clé API est une chaîne unique et longue, commençant par « sk-proj-« , nécessaire pour authentifier vos requêtes API (voir ci-dessous). Vous devrez copier cette chaîne entière dans le code du chatbot visuel.

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

C’est tout ce dont vous avez vraiment besoin, mais je recommande de définir aussi une limite d’utilisation pour votre compte. Cela évite de recevoir une facture élevée par erreur due à un bug dans votre code (par exemple, l’envoi de centaines d’images).

Vous pouvez définir des limites d’utilisation et consulter les tarifs (coûts) des différents modèles d’IA sous l’onglet Facturation (platform.openai.com/settings/organization/billing).

Billing Overview
Vue d’ensemble de la facturation

Pour ma part, j’ai fixé une limite d’utilisation à 20 USD et je n’ai pas activé la recharge automatique. Ce petit budget dure longtemps, tant que les modèles d’IA peu coûteux sont utilisés. Comme vous pouvez le voir, mon solde est encore de 14 USD et je teste les modèles OpenAI depuis plusieurs mois.

Installer le core ESP32

Si c’est votre premier projet avec une carte de la série ESP32, vous devrez aussi installer le core ESP32. Si les cartes ESP32 sont déjà installées dans votre Arduino IDE, vous pouvez passer cette section.

Commencez par ouvrir la boîte de dialogue Préférences en sélectionnant “Preferences…” dans le menu “File”. Cela ouvrira la fenêtre de préférences ci-dessous.

Sous l’onglet Settings, vous trouverez une zone de saisie en bas de la fenêtre intitulée “Additional boards manager URLs“ :

Additional boards manager URLs in Preferences
URLs supplémentaires du gestionnaire de cartes dans Préférences

Dans ce champ, copiez l’URL suivante :

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

Cela permettra à l’Arduino IDE de savoir où trouver les bibliothèques du core ESP32. Ensuite, nous installerons les cartes ESP32 via le gestionnaire de cartes.

Ouvrez le gestionnaire de cartes via « Tools -> Boards -> Board Manager ». Le gestionnaire apparaîtra dans la barre latérale gauche. Tapez « ESP32 » dans le champ de recherche en haut et vous verrez deux types de cartes ESP32 : « Arduino ESP32 Boards » et « esp32 by Espressif ». Nous voulons les « esp32 libraries by Espressif ». Cliquez sur INSTALL et attendez la fin du téléchargement et de l’installation.

Install ESP32 Core libraries
Installation des bibliothèques ESP32 Core

J’utilise ici la version actuelle 3.3.5, mais toute autre version 3.x devrait aussi fonctionner pour ce projet.

Sélection de la carte

Vous devez aussi sélectionner une carte ESP32. Pour la DFRobot ESP32-S3 AI Camera, choisissez le module générique « ESP32S3 Dev Module ». Pour cela, cliquez sur le menu déroulant puis sur « Select other board and port… »:

Drop-down Menu for Board Selection
Menu déroulant pour la sélection de la carte

Cela ouvrira une fenêtre où vous pouvez taper « esp32s3 dev » dans la barre de recherche. Vous verrez la carte « ESP32S3 Dev Module » sous Boards. Cliquez dessus, sélectionnez le port COM pour l’activer, puis cliquez sur OK :

Board Selection Dialog "ESP32S3 Dev Module" board
Dialogue de sélection de carte « ESP32S3 Dev Module »

Notez que vous devez connecter la carte via le câble USB à votre ordinateur avant de pouvoir sélectionner un port COM.

Paramètres des outils

Voici les paramètres à utiliser avec la carte. Vous les trouverez dans le menu Tools de votre Arduino IDE.

Tool settings for DFRobot ESP32-S3 AI Camera
Paramètres des outils pour la DFRobot ESP32-S3 AI Camera

Les paramètres les plus importants sont « 16MB Flash Size », « Huge APP partition » et « OPI PSRAM ». Pour voir la sortie texte sur le moniteur série, assurez-vous que USB CDC on Boot est « Enabled ». Les autres paramètres sont généralement les paramètres par défaut et conviennent tels quels.

Installer les bibliothèques

Le code du chatbot visuel utilise deux bibliothèques, la ArduinoJson et la ESP32-audioI2S. Ouvrez le LIBRARY MANAGER, cherchez « ArduinoJson » et « ESP32-audioI2S » puis cliquez sur INSTALL pour les installer :

Installing "ESP32-audioI2S" and "ArduinoJson" libraries
Installation des bibliothèques « ESP32-audioI2S » et « ArduinoJson »

Comme vous pouvez le voir, j’ai installé la version 3.4.5 de la bibliothèque ESP32-audioI2S et la version 7.4.2 de la bibliothèque ArduinoJson. Cependant, la version exacte importe peu.

Code du chatbot visuel

Dans cette section, je vous montre le code du chatbot visuel. C’est une réimplémentation de l’exemple OpenAI image recognition que vous pouvez trouver dans le DFRobot github repo mais qui ne fonctionne malheureusement pas de manière fiable. Je l’ai donc entièrement réécrit.

Le code commence par attendre que le bouton BOOT soit pressé et enregistre l’audio jusqu’à ce que le bouton BOOT soit relâché ou que plus de 3 secondes se soient écoulées. Le bouton BOOT se trouve en bas à droite au dos de la carte, au-dessus du bouton reset (RST) :

BOOT Button
Bouton BOOT

Ensuite, le code envoie l’audio enregistré au modèle de synthèse vocale d’OpenAI pour transcription, capture une image avec la caméra, puis interroge le modèle GPT compatible vision d’OpenAI pour répondre aux questions sur l’image. La réponse est convertie en audio par le modèle de reconnaissance vocale d’OpenAI et diffusée via le petit haut-parleur du module ESP32-S3 AI Camera.

Jetez un coup d’œil rapide au code avant que nous en discutions en détail. Si vous ne voulez pas copier-coller le code, vous pouvez télécharger le fichier vision_chatbot.zip qui contient le code complet. Vous devrez cependant toujours configurer les identifiants WiFi et la clé API OpenAI dans le code.

#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

Le code commence par inclure les bibliothèques nécessaires pour la connectivité WiFi, la communication HTTP sécurisée, le parsing JSON, le contrôle de la caméra, le traitement audio et l’encodage base64. Ces bibliothèques permettent à l’ESP32-S3 d’interagir avec les périphériques matériels et de communiquer avec les API cloud d’OpenAI.

#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"

Broches et configuration audio

Plusieurs constantes définissent les broches GPIO utilisées pour le bouton, la LED, les données et l’horloge du microphone, ainsi que les broches de sortie audio I2S. Les paramètres audio tels que la fréquence d’échantillonnage, la durée maximale d’enregistrement et la taille de l’en-tête WAV sont aussi définis pour configurer la capture et la lecture audio.

#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

Modèles et identifiants réseau

Le code spécifie les modèles OpenAI utilisés pour la synthèse vocale (TTS), la reconnaissance vocale (STT) et la réponse aux questions visuelles. Il stocke aussi le SSID WiFi, le mot de passe et la clé API OpenAI comme constantes pour la connexion réseau et l’authentification API.

#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";

Vous devez remplacer les valeurs fictives pour ssid, password et apiKey OpenAI par les valeurs correctes de votre réseau WiFi et votre clé API OpenAI. Sinon, le chatbot visuel ne pourra pas communiquer avec les modèles IA d’OpenAI et ne fonctionnera pas.

Variables et objets globaux

Le code déclare un client WiFi sécurisé pour la communication HTTPS, un objet interface audio I2S, et un objet Audio pour la lecture. Il gère aussi un tampon pour stocker les données audio WAV enregistrées, des indicateurs d’enregistrement et d’occupation, ainsi que des variables de temps pour contrôler la durée d’enregistrement.

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;

Initialisation du microphone

La fonction initMic() configure le périphérique I2S pour recevoir les données du microphone PDM en utilisant les broches d’horloge et de données spécifiées. Elle définit la fréquence d’échantillonnage, la largeur des bits de données et le mode mono pour préparer l’enregistrement audio.

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);      
}

Contrôle de l’enregistrement audio

La fonction startRecording() démarre la capture audio en allumant la LED indicatrice et en allouant un tampon en PSRAM pour contenir les données WAV. Elle écrit un en-tête WAV par défaut dans le tampon, enregistre l’heure de début, et active le drapeau d’enregistrement.

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");
}

La fonction pollRecording() lit les données audio disponibles depuis le périphérique I2S et les ajoute au tampon WAV. Si le tampon est plein, elle arrête automatiquement l’enregistrement.

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

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

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

La fonction stopRecording() finalise les données WAV en mettant à jour l’en-tête avec les tailles correctes, désactive le drapeau d’enregistrement, et éteint la LED indicatrice.

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);
}

Construction de la requête multipart pour la reconnaissance vocale

Pour envoyer l’audio enregistré au modèle Speech-to-Text d’OpenAI, le code construit un corps de requête HTTP multipart/form-data. Les fonctions buildMultipartHead() et buildMultipartTail() créent les délimitations et en-têtes multipart, tandis que buildMultipartBody() assemble le corps complet en PSRAM en concaténant la tête, les données WAV et la queue.

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;
}

La constante STT_MODEL spécifie le modèle Speech-to-Text utilisé. J’utilise ici « whisper-1 » mais OpenAI propose d’autres Speech-to-Text modèles comme « gpt-4o-mini-transcribe », « gpt-4o-transcribe » ou « gpt-4o-transcribe-diarize » que vous pouvez essayer.

Requête HTTP POST pour la reconnaissance vocale

La fonction postMultipart() effectue la requête HTTPS POST vers le point d’accès de transcription audio d’OpenAI. Elle définit les en-têtes d’autorisation et de type de contenu, télécharge le corps multipart, et récupère la réponse. La fonction retourne le code HTTP et stocke la chaîne de réponse.

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;
}

Traitement de la reconnaissance vocale

La fonction speechToText() orchestre le processus de construction de la requête multipart, son envoi, et le parsing de la réponse JSON pour extraire le texte transcrit. Elle gère aussi la mémoire en libérant le tampon WAV après l’envoi.

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);
}

Réponse aux questions visuelles

La fonction visionAnswer() envoie une question avec une image capturée au modèle GPT-4o-mini d’OpenAI pour répondre aux questions basées sur la vision. Elle encode l’image en base64, construit une requête JSON de chat avec messages système et utilisateur, et analyse la réponse pour extraire la réponse de l’assistant.

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"] | "";
}

La constante VISION_MODEL spécifie le modèle OpenAI vision model utilisé. J’utilise « gpt-4o-mini » mais il en existe d’autres comme « gpt-image-1 », « gpt-5-mini », « gpt-5-nano » ou « gpt-4.1-nano » que vous pouvez tester. Ils ont des capacités, vitesses et coûts différents.

Lecture de la synthèse vocale

La fonction textToSpeech() utilise la bibliothèque ESP32-audioI2S pour demander la synthèse vocale au modèle TTS d’OpenAI. Elle active le drapeau d’occupation pour éviter les chevauchements pendant la lecture audio.

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

La constante TTS_MODEL spécifie le modèle Text-to-Speech utilisé. J’utilise « tts-1 » mais vous pouvez aussi utiliser « tts-1-hd ». Le modèle « tts-1 » offre une latence plus faible, mais une qualité inférieure au modèle « tts-1-hd ». Un modèle plus intelligent mais aussi plus coûteux est le modèle « gpt-4o-mini-tts » que vous pouvez également utiliser.

La voix pour la sortie audio est spécifiée par la constante TTS_VOICE, réglée sur « shimmer ». Vous pouvez essayer d’autres voix, mais notez que la disponibilité des voix dépend du modèle. Les modèles tts-1 et tts-1-hd supportent un ensemble plus restreint de voix : « alloy », « ash », « coral », « echo », « fable », « onyx », « nova », « sage » et « shimmer » (platform.openai.com/docs/guides/text-to-speech).

Capture caméra

La fonction captureImage() capture une image depuis la caméra ESP32 et retourne un pointeur vers le tampon de la trame pour un traitement ultérieur.

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

Initialisation matérielle

Plusieurs fonctions d’aide initialisent les composants matériels et services système. initSpeaker() configure les broches de sortie audio et le volume. initPins() configure les modes GPIO du bouton et de la LED. initTime() synchronise l’heure système via des serveurs NTP. initWiFi() connecte au réseau WiFi spécifié. initSerial() démarre la communication série pour le débogage.

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);
}

Fonction Setup

La fonction setup() est appelée une fois au démarrage. Elle configure le client sécurisé pour ignorer la vérification des certificats, initialise la communication série, les broches GPIO, le WiFi, l’heure système, le microphone, le haut-parleur et la caméra. Enfin, elle affiche « Ready » pour indiquer que le système est prêt.

void setup() {
  secureClient.setInsecure();

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

  Serial.println("Ready");
}

Fonction Loop

La fonction loop() s’exécute en boucle. Elle gère la lecture audio en arrière-plan. Si le système est occupé à jouer de l’audio, elle attend. Sinon, elle lit l’état du bouton pour détecter les pressions et relâchements.

Quand le bouton est pressé, l’enregistrement démarre. Pendant l’enregistrement, les données audio sont relevées et ajoutées au tampon. L’enregistrement s’arrête soit quand le bouton est relâché, soit quand la durée maximale est atteinte.

Après l’arrêt, l’audio enregistré est envoyé pour transcription. Si la transcription réussit, la caméra capture une image, et la question est envoyée au modèle vision. La réponse est ensuite convertie en parole et jouée. La boucle inclut un petit délai pour éviter les rebonds du bouton.

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);
}

Et voici le code complet du chatbot visuel lui-même. Cependant, nous avons aussi besoin d’un code pour la caméra, décrit dans la section suivante.

Code Camera.h

Le code de la caméra est essentiellement une copie du fichier « camera.h » de l’exemple OpenAI image recognition que vous pouvez trouver dans le DFRobot github repo.

#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);
  }
}

Cependant, j’ai fait un changement important. J’ai remarqué que des erreurs « cam_hal: FB-OVF » s’affichaient dans le moniteur série lorsque je faisais fonctionner le chatbot visuel.

« FB-OVF » signifie Frame Buffer Overflow. Cela signifie essentiellement que la caméra envoie des données plus rapidement que l’ESP32 ne peut les traiter ou les stocker en mémoire.

La méthode recommandée pour éviter cette erreur est de réduire la fréquence d’images. J’ai donc modifié le code original de la caméra et réglé la fréquence à 8 MHz :

config.xclk_freq_hz = 8000000;

Cela a éliminé les erreurs « cam_hal: FB-OVF ».

Dossier du projet

Vous pouvez télécharger le fichier complet du projet vision_chatbot.zip ou créer vous-même le projet Arduino pour le chatbot visuel. Pour cela, créez un dossier « vision_chatbot » avec deux fichiers (« camera.h », « vision_chatbot.ino ») à l’intérieur :

Le fichier « camera.h » contient le code pour la caméra et « vision_chatbot.ino » contient le code du chatbot visuel. Une fois que vous avez sélectionné la bonne carte (« ESP32S3 Dev Module ») et les bons paramètres outils (PSRAM, Huge APP, …), vous pouvez flasher le code sur la carte et profiter de votre chatbot visuel en action. Les deux sections suivantes montrent deux exemples avec les réponses du chatbot visuel.

Exemple : Que vois-tu ?

Dans ce premier exemple, j’ai montré à la caméra ESP32-S3 un petit crâne modèle sur mon bureau avec d’autres objets en arrière-plan.

Voici la sortie du chatbot visuel sur le moniteur série lorsqu’il a reçu cette image :

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.

Vous pouvez voir la question posée « Que vois-tu ? » et la réponse du chatbot « Je vois un petit crâne posé sur une surface claire, probablement une table. En arrière-plan, il y a divers objets et possiblement un peu de désordre. L’environnement semble être un espace intérieur. »

Notez qu’un message d’erreur « E (1763) i2s_common: i2s_channel_disable(1217): the channel has not been enabled yet » apparaît au début, que vous pouvez ignorer. Cela semble lié à un problème avec le core ESP32 actuel mais n’affecte pas le fonctionnement du chatbot.

Exemple : Combien d’horloges ?

Vous pouvez aussi demander au chatbot de compter des objets spécifiques dans une image. Cela ne fonctionne pas toujours, et le bot a eu des difficultés à reconnaître des chaises, par exemple. Cependant, dans la scène assez complexe ci-dessous contenant une horloge murale, le chatbot a correctement indiqué qu’il y avait une horloge :

Voici la sortie sur le moniteur série. J’ai demandé « Combien d’horloges y a-t-il dans cette image ? » et la réponse a été « Il y a une horloge visible dans l’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.

Compte tenu de la faible qualité, de la petite taille de l’image, et du nombre d’objets présents, le chatbot visuel a vraiment bien identifié l’horloge.

Amusez-vous bien avec votre chatbot visuel, mais gardez un œil sur les coûts OpenAI et votre budget, surtout si vous commencez à utiliser des modèles plus performants mais aussi plus coûteux.

Conclusions

Dans ce tutoriel, vous avez appris à construire un chatbot visuel utilisant le module DFRobot ESP32-S3 AI Camera et OpenAI. L’enregistrement audio, la lecture et la capture d’image sont effectués localement par l’ESP32, tandis que la synthèse vocale, la reconnaissance vocale et l’analyse d’image sont réalisées à distance par les services OpenAI.

Le traitement à distance des données image et audio permet d’utiliser des modèles d’IA beaucoup plus puissants que ceux pouvant tourner localement sur l’ESP32. Cependant, cela nécessite une connexion internet stable pour communiquer avec les services cloud d’OpenAI, et ajoute de la latence et un coût. De plus, la confidentialité est un enjeu lors de l’envoi d’images vers des serveurs externes.

Si vous souhaitez effectuer une reconnaissance vocale simple localement, consultez les tutoriels Getting started with Gravity Voice Recognition Module, Voice control with XIAO-ESP32-S3-Sense and Edge Impulse et Using the Voice Recognition Module V3 with Arduino.

Pour la reconnaissance vocale, voyez le tutoriel Gravity Text-to-Speech Module Tutorial, qui peut générer la parole localement mais avec une qualité limitée.

Pour d’autres exemples de code pour la DFRobot ESP32-S3 AI Camera, consultez le DFRobot Wiki et leur github repo.

Si vous avez des questions, n’hésitez pas à les laisser dans la section commentaires.

Bon bricolage ; )