Skip to Content

Vision Chatbot con DFRobot ESP32-S3 AI Camera e OpenAI

Vision Chatbot con DFRobot ESP32-S3 AI Camera e OpenAI

La DFRobot ESP32-S3 AI Camera (DFR1154) è una scheda di sviluppo progettata per progetti di intelligenza artificiale e visione. È dotata di un microcontrollore ESP32-S3 e integra un modulo fotocamera, un microfono e un altoparlante su un’unica scheda.

In questo progetto creeremo un Vision Chatbot utilizzando la scheda ESP32-S3 AI Camera e OpenAI. La scheda cattura immagini e ascolta domande pronunciate tramite il microfono. Successivamente converte il parlato in testo usando OpenAI. In seguito invia sia l’immagine che la domanda testuale a un altro modello OpenAI per l’analisi. Questo modello AI elabora l’input, genera una risposta sull’immagine e restituisce la risposta in formato testo. Infine, usiamo OpenAI una terza volta per convertire il testo della risposta in parlato e riprodurlo tramite l’altoparlante.

Il breve video qui sotto mostra il Vision Chatbot in azione. Tengo la scheda a sinistra e la sua fotocamera è puntata su un piccolo modello di teschio sulla mia scrivania. Premo il pulsante BOOT sulla scheda per avviare il Vision Chatbot e potrai sentire la mia domanda e la risposta del bot.

Regola il volume se non senti l’audio e tieni presente che c’è un ritardo di qualche secondo tra la domanda e la risposta a causa del trasferimento dati e dell’elaborazione su OpenAI:

Vision Chatbot in azione

La registrazione audio, la riproduzione audio e la cattura delle immagini sono gestite localmente dalla scheda ESP32-S3 AI Camera. Tuttavia, l’analisi delle immagini, il Text-to-Speech (TTS) e lo Speech-to-Text (STT) utilizzano i modelli AI di OpenAI. Pertanto è necessaria una connessione WiFi e una chiave API OpenAI.

Componenti necessari

Per questo progetto utilizzo il modulo ESP32-S3 AI Camera (DFR1154) di DFRobot. Puoi acquistarlo da DFRobot tramite i link sottostanti. Assicurati di prendere la Versione 1.1 e non la più vecchia Versione 1.0. Potresti anche aver bisogno di un cavo USB-C.

DFRobot ESP32-S3 AI Camera

Cavo 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.

Hardware della DFRobot AI Camera

La DFRobot ESP32-S3 AI Camera (DFR1154) è una scheda compatta per visione embedded e sviluppo AI basata sul microcontrollore ESP32-S3 di Espressif. Integra connettività wireless, imaging della fotocamera, I/O audio e capacità di accelerazione AI in un unico modulo. Questa scheda è progettata per compiti di edge computing come riconoscimento oggetti, interazione vocale e analisi visiva in tempo reale.

Le dimensioni fisiche sono di un circuito stampato quadrato di circa 42 mm per lato, rendendola adatta per integrazione in robotica, sensori intelligenti e sistemi di monitoraggio IoT. L’immagine sotto mostra il fronte e il retro della scheda:

Front and Back of DFRobot ESP32-S3 AI Camera (source)
Fronte e retro della DFRobot ESP32-S3 AI Camera (source)

Microcontrollore e architettura della memoria

Al centro della scheda, il microcontrollore ESP32-S3 esegue il codice applicativo e gestisce le comunicazioni. Questo microcontrollore dispone di una CPU dual-core Tensilica Xtensa® LX7 a 32 bit che può arrivare fino a 240 MHz. Include SRAM integrata per accesso rapido a dati e istruzioni, oltre a SRAM RTC dedicata per operazioni a basso consumo.

La memoria on-die è integrata da storage esterno: la scheda offre 16 MB di memoria flash per firmware e dati, e un chip PSRAM da 8 MB per supportare heap di runtime più grandi necessari per il buffering delle immagini o l’esecuzione di modelli AI. L’interfaccia USB è conforme a USB 2.0 OTG full-speed, permettendo sia alimentazione che trasferimento dati.

Sottosistema fotocamera

Il sottosistema di imaging si basa su un sensore fotocamera OmniVision OV3660. Questo sensore cattura immagini fino a 2 megapixel e include sensibilità sia alla luce visibile che all’infrarosso a 940 nm, ampliando il raggio operativo in condizioni di scarsa illuminazione o visione notturna.

L’ottica offre un campo visivo di circa 160 gradi, e il sistema a fuoco fisso ha una lunghezza focale di circa 0,95 mm con apertura intorno a f/2.0. Sono presenti anche quattro LED IR per l’illuminazione.

Wireless e connettività

La scheda supporta comunicazioni wireless dual-mode. Per la connettività di rete locale implementa Wi-Fi IEEE 802.11b/g/n nella banda 2.4 GHz, con supporto per canali da 20 MHz e 40 MHz e modalità operative multiple tra cui station, soft access point e modalità combinate station+AP. Il Bluetooth è disponibile secondo i protocolli Bluetooth 5 e Bluetooth Mesh, permettendo comunicazioni peer a basso consumo o partecipazione a reti di sensori.

Interfacce audio e sensori

Oltre all’imaging, la scheda dispone di un microfono I2S PDM integrato per la cattura audio, che passa attraverso un amplificatore interno (MAX98357) e si collega a un’interfaccia altoparlante dedicata. È inoltre presente una porta per schede SD che consente la memorizzazione di dati audio e video.

Il sensore di luce ambientale LTR-308 permette di adattare l’imaging o la gestione energetica in base all’illuminazione ambientale. Questo è particolarmente utile in combinazione con i 4 LED IR presenti per l’illuminazione.

La scheda offre un’interfaccia Gravity a 4 pin (3.3V, GND, GPIO44/RX, GPIO43/TX) che fornisce connettività UART/I2C semplice verso periferiche o sensori esterni. Nota che nella versione precedente V1.0 del modulo il pin 1 dell’interfaccia Gravity era un ingresso 3.3-5V, mentre nella versione attuale V1.1 il pin 1 è un’uscita 3.3V!

Pin GPIO

La tabella seguente elenca i pin GPIO e la loro assegnazione ai vari componenti hardware come fotocamera (CAM), microfono (MIC), amplificatore audio (MAX98357), sensore di luce ambientale (ALS), scheda SD (SD), LED IR, pulsante BOOT e LED di stato:

Pinout della DFRobot ESP32-S3 AI Camera (source)

Gestione dell’alimentazione e design fisico

La DFR1154 accetta diverse configurazioni di alimentazione. Opera nominalmente a 3.3 V e può ricevere alimentazione tramite connettore USB-C a 5 V DC o tramite connettore VIN con un intervallo più ampio da 3.7 V a 15 V DC. Un circuito integrato di gestione dell’alimentazione dedicato (HM6245) regola questi ingressi alle tensioni core richieste.

L’intervallo di temperatura operativo è specificato da circa -10 °C fino a 60 °C, pensato per ambienti interni standard o esterni protetti.

Specifiche tecniche

La tabella seguente riassume le principali specifiche hardware del modulo DFRobot ESP32-S3 AI Camera (DFR1154) (source):

Specifiche Dettagli
Microcontrollore ESP32-S3R8 con CPU dual-core Tensilica Xtensa LX7, 240 MHz
SRAM 512 KB
ROM 384 KB
Flash esterna 16 MB
PSRAM esterna 8 MB
RTC SRAM 16 KB
Sensore fotocamera OV3660, 2 MP, FoV 160°, supporto infrarosso
Ottica Focale fissa 0,95 mm, apertura f/2.0, distorsione <8 %
Wireless Wi-Fi 802.11b/g/n (2.4 GHz); Bluetooth 5 & Mesh
Audio Microfono I2S PDM; interfaccia altoparlante tramite amplificatore MAX98357
Sensori Sensore di luce ambientale LTR-308
USB USB 2.0 OTG Full Speed (Type-C)
Alimentazione Tensione operativa 3.3 V; USB-C 5 V; VIN 3.7–15 V
Pulsanti Reset e Boot
Dimensioni 42 mm × 42 mm
Temperatura operativa -10 °C a 60 °C

Ottenere la chiave API OpenAI

Il Vision Chatbot utilizza modelli AI forniti da OpenAI. Perciò è necessario un account OpenAI. Vai su https://platform.openai.com e registrati con un indirizzo email o un account Google o Microsoft esistente.

Dopo aver verificato la tua email e completato la configurazione iniziale, accedi alla dashboard OpenAI, platform.openai.com/api-keys e trova o crea la tua chiave API (=SECRET KEY) come mostrato di seguito:

OpenAI API keys
Chiavi API OpenAI

La chiave API è una stringa unica e lunga, che inizia con “sk-proj-“, necessaria per autenticare le richieste API (vedi sotto). Successivamente dovrai copiare questa stringa intera nel codice del Vision Chatbot.

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

Questo è tutto ciò che serve, ma ti consiglio di impostare anche un limite di utilizzo per il tuo account. Questo evita di incorrere in costi elevati a causa di un bug nel codice (ad esempio inviando centinaia di immagini).

Puoi impostare i limiti di utilizzo e consultare i prezzi (costi) dei diversi modelli AI nella scheda Billing (platform.openai.com/settings/organization/billing).

Billing Overview
Panoramica della fatturazione

Io stesso ho impostato un limite di utilizzo di 20 USD e non ho abilitato la ricarica automatica. Questo piccolo budget dura a lungo, purché si usino modelli AI economici. Come vedi, il mio saldo è ancora di 14 USD e gioco con i modelli OpenAI da diversi mesi.

Installare il core ESP32

Se è il tuo primo progetto con una scheda della serie ESP32, dovrai installare il core ESP32. Se le schede ESP32 sono già installate nel tuo Arduino IDE, puoi saltare questa sezione.

Inizia aprendo la finestra Preferenze selezionando “Preferences…” dal menu “File”. Si aprirà la finestra Preferenze mostrata qui sotto.

Nella scheda Settings troverai una casella di testo in fondo alla finestra etichettata “Additional boards manager URLs“:

Additional boards manager URLs in Preferences
URL aggiuntivi per il board manager nelle Preferenze

In questo campo incolla il seguente URL:

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

Questo permetterà all’Arduino IDE di trovare le librerie core ESP32. Successivamente installeremo le schede ESP32 tramite il Boards Manager.

Apri il Boards Manager tramite “Tools -> Boards -> Board Manager”. Vedrai il Boards Manager apparire nella barra laterale sinistra. Digita “ESP32” nel campo di ricerca in alto e dovresti vedere due tipi di schede ESP32; le “Arduino ESP32 Boards” e le schede “esp32 by Espressif”. Vogliamo le “esp32 libraries by Espressif”. Clicca sul pulsante INSTALL e attendi il completamento del download e dell’installazione.

Install ESP32 Core libraries
Installare le librerie core ESP32

Sto usando la versione attuale 3.3.5, ma qualsiasi altra versione 3.x dovrebbe funzionare per questo progetto.

Selezione della scheda

Devi anche selezionare una scheda ESP32. Nel caso della DFRobot ESP32-S3 AI Camera, puoi scegliere la scheda generica “ESP32S3 Dev Module”. Per farlo, clicca sul menu a tendina e poi su “Select other board and port…”:

Drop-down Menu for Board Selection
Menu a tendina per la selezione della scheda

Si aprirà una finestra dove puoi digitare “esp32s3 dev” nella barra di ricerca. Vedrai la scheda “ESP32S3 Dev Module” sotto Boards. Cliccaci sopra, seleziona la porta COM per attivarla e poi clicca OK:

Board Selection Dialog "ESP32S3 Dev Module" board
Finestra di selezione scheda “ESP32S3 Dev Module”

Nota che devi collegare la scheda al computer tramite cavo USB prima di poter selezionare una porta COM.

Impostazioni degli strumenti

Di seguito le impostazioni da usare con la scheda. Le trovi nel menu Tools del tuo Arduino IDE.

Tool settings for DFRobot ESP32-S3 AI Camera
Impostazioni strumenti per DFRobot ESP32-S3 AI Camera

Le impostazioni più importanti sono “16MB Flash Size”, “Huge APP partition” e “OPI PSRAM”. Per vedere l’output testuale sul Monitor Seriale assicurati che USB CDC on Boot sia “Enabled”. Le altre impostazioni sono tipicamente quelle di default e vanno bene così.

Installare le librerie

Il codice per il Vision Chatbot usa due librerie, la ArduinoJson e la ESP32-audioI2S. Apri il LIBRARY MANAGER, cerca “ArduinoJson” e “ESP32-audioI2S” e premi INSTALL per installare queste librerie:

Installing "ESP32-audioI2S" and "ArduinoJson" libraries
Installazione delle librerie “ESP32-audioI2S” e “ArduinoJson”

Come vedi, ho installato la Versione 3.4.5 della libreria ESP32-audioI2S e la Versione 7.4.2 della libreria ArduinoJson. Tuttavia, la versione esatta non dovrebbe influire molto.

Codice per il Vision Chatbot

In questa sezione ti mostro il codice per il Vision Chatbot. È una reimplementazione dell’esempio OpenAI image recognition che puoi trovare nel DFRobot github repo ma che purtroppo non funziona in modo affidabile. Perciò l’ho riscritto completamente.

Il codice inizia aspettando che venga premuto il pulsante BOOT e registra l’audio finché il pulsante non viene rilasciato o passano più di 3 secondi. Il pulsante BOOT si trova nell’angolo in basso a destra sul retro della scheda, sopra il pulsante reset (RST):

BOOT Button
Pulsante BOOT

Successivamente il codice invia l’audio registrato al modello text-to-speech di OpenAI per la trascrizione, cattura un’immagine con la fotocamera e poi interroga il modello GPT con capacità visive di OpenAI per rispondere a domande sull’immagine. La risposta viene convertita in audio dal modello speech-to-text di OpenAI e riprodotta tramite il piccolo altoparlante del modulo ESP32-S3 AI Camera.

Dai un’occhiata veloce al codice prima di discuterne i dettagli. Se non vuoi copiare e incollare il codice, puoi scaricare il file vision_chatbot.zip che contiene il codice completo. Dovrai comunque impostare le credenziali WiFi e la chiave API OpenAI nel codice.

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

Importazioni

Il codice inizia includendo le librerie necessarie per la connettività WiFi, comunicazione HTTP sicura, parsing JSON, controllo della fotocamera, elaborazione audio e codifica base64. Queste librerie permettono all’ESP32-S3 di interagire con le periferiche hardware e comunicare con le API cloud di 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"

Configurazione pin e audio

Diverse costanti definiscono i pin GPIO usati per il pulsante, il LED, i dati e il clock del microfono, e i pin di uscita audio I2S. Sono anche definiti parametri audio come frequenza di campionamento, tempo massimo di registrazione e dimensione dell’intestazione WAV per configurare la cattura e la riproduzione 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

Modelli e credenziali di rete

Il codice specifica i modelli OpenAI usati per text-to-speech (TTS), speech-to-text (STT) e risposta a domande visive. Memorizza inoltre SSID WiFi, password e chiave API OpenAI come costanti per la connessione di rete e l’autenticazione 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";

Devi sostituire i valori fittizi di ssid, password e apiKey OpenAI con quelli corretti della tua rete WiFi e della tua chiave API OpenAI. Altrimenti il Vision Chatbot non potrà comunicare con i modelli AI di OpenAI e non funzionerà.

Variabili e oggetti globali

Il codice dichiara un client WiFi sicuro per comunicazioni HTTPS, un oggetto interfaccia audio I2S e un oggetto Audio per la riproduzione. Gestisce anche un buffer per memorizzare i dati audio WAV registrati, flag per gli stati di registrazione e occupato, e variabili temporali per controllare la durata della registrazione.

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;

Inizializzazione microfono

La funzione initMic() configura la periferica I2S per ricevere dati dal microfono PDM usando i pin clock e dati specificati. Imposta la frequenza di campionamento, la larghezza dati e la modalità canale mono per preparare la registrazione 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);      
}

Controllo registrazione audio

La funzione startRecording() avvia la cattura audio accendendo il LED indicatore e allocando un buffer in PSRAM per contenere i dati WAV. Scrive un’intestazione WAV di default nel buffer, registra il tempo di inizio e imposta il flag di registrazione.

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 funzione pollRecording() legge i dati audio disponibili dalla periferica I2S e li aggiunge al buffer WAV. Se il buffer si riempie, interrompe automaticamente la registrazione.

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

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

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

La funzione stopRecording() finalizza i dati WAV aggiornando l’intestazione con le dimensioni corrette, disattiva il flag di registrazione e spegne il LED indicatore.

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

Costruzione della richiesta multipart per Speech-to-Text

Per inviare l’audio registrato al modello Speech-to-Text di OpenAI, il codice costruisce un corpo di richiesta HTTP multipart/form-data. Le funzioni buildMultipartHead() e buildMultipartTail() creano i confini e le intestazioni multipart, mentre buildMultipartBody() assembla il corpo completo della richiesta in PSRAM concatenando testa, dati WAV e coda.

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 costante STT_MODEL specifica il modello Speech-to-Text usato. Qui uso “whisper-1” ma OpenAI offre altri modelli Speech-to-Text come “gpt-4o-mini-transcribe”, “gpt-4o-transcribe” o “gpt-4o-transcribe-diarize” che puoi provare.

HTTP POST per Speech-to-Text

La funzione postMultipart() esegue la richiesta HTTPS POST all’endpoint di trascrizione audio di OpenAI. Imposta le intestazioni di autorizzazione e content-type, carica il corpo multipart e recupera la risposta. La funzione restituisce il codice di stato HTTP e memorizza la risposta in stringa.

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

Elaborazione Speech-to-Text

La funzione speechToText() coordina il processo di costruzione della richiesta multipart, l’invio e il parsing della risposta JSON per estrarre il testo trascritto. Gestisce anche la memoria liberando il buffer WAV dopo l’invio.

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

Risposta a domande visive

La funzione visionAnswer() invia una domanda insieme a un’immagine catturata al modello GPT-4o-mini di OpenAI per risposte basate sulla visione. Codifica l’immagine in base64, costruisce una richiesta JSON di completamento chat con messaggi di sistema e utente, e analizza la risposta per estrarre la risposta dell’assistente.

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 costante VISION_MODEL specifica il modello OpenAI vision model usato. Io uso “gpt-4o-mini” ma ci sono altri come “gpt-image-1”, “gpt-5-mini”, “gpt-5-nano” o “gpt-4.1-nano” che puoi provare. Hanno capacità, velocità e costi diversi.

Riproduzione Text-to-Speech

La funzione textToSpeech() usa la libreria ESP32-audioI2S per richiedere la sintesi vocale dal modello TTS di OpenAI. Imposta il flag busy per evitare operazioni sovrapposte durante la riproduzione audio.

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

La costante TTS_MODEL specifica il modello Text-to-Speech usato. Io uso “tts-1” ma potresti anche usare “tts-1-hd”. Il modello “tts-1” offre latenza inferiore, ma qualità più bassa rispetto a “tts-1-hd”. Un modello più intelligente ma anche più costoso è “gpt-4o-mini-tts” che potresti usare.

La voce per l’output audio è specificata dalla costante TTS_VOICE, impostata su “shimmer”. Puoi provare altre voci, ma la disponibilità dipende dal modello. I modelli tts-1 e tts-1-hd supportano un set più limitato di voci: “alloy”, “ash”, “coral”, “echo”, “fable”, “onyx”, “nova”, “sage” e “shimmer” (platform.openai.com/docs/guides/text-to-speech).

Cattura della fotocamera

La funzione captureImage() cattura un frame dalla fotocamera ESP32 e restituisce un puntatore al buffer del frame per ulteriori elaborazioni.

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

Inizializzazione hardware

Diverse funzioni di supporto inizializzano componenti hardware e servizi di sistema. initSpeaker() configura i pin di uscita audio e il volume. initPins() imposta le modalità GPIO per pulsante e LED. initTime() sincronizza l’orario di sistema usando server NTP. initWiFi() si connette alla rete WiFi specificata. initSerial() avvia la comunicazione seriale per il debug.

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

Funzione Setup

La funzione setup() viene chiamata una volta all’avvio. Configura il client sicuro per saltare la verifica del certificato, inizializza la comunicazione seriale, i pin GPIO, il WiFi, l’orario di sistema, microfono, altoparlante e fotocamera. Infine stampa “Ready” per indicare che il sistema è pronto.

void setup() {
  secureClient.setInsecure();

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

  Serial.println("Ready");
}

Funzione Loop

La funzione loop() viene eseguita ripetutamente. Gestisce la riproduzione audio in background. Se il sistema è occupato a riprodurre audio, attende. Altrimenti legge lo stato del pulsante per rilevare pressioni e rilasci.

Quando il pulsante è premuto, inizia la registrazione. Durante la registrazione, i dati audio vengono letti e aggiunti al buffer. La registrazione si ferma quando il pulsante viene rilasciato o si raggiunge il tempo massimo.

Dopo l’arresto, l’audio registrato viene inviato per la trascrizione. Se la trascrizione ha successo, la fotocamera cattura un’immagine e la domanda viene inviata al modello visivo. La risposta viene quindi convertita in parlato e riprodotta. Il loop include un piccolo ritardo per il debounce del pulsante.

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

E questo è il codice completo per il Vision Chatbot. Tuttavia, serve anche del codice per la fotocamera, descritto nella sezione successiva.

Codice Camera.h

Il codice della fotocamera è essenzialmente una copia del file “camera.h” dall’esempio OpenAI image recognition che puoi trovare nel 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);
  }
}

Tuttavia, ho fatto una modifica importante. Ho notato che venivano stampati errori “cam_hal: FB-OVF” sul Monitor Seriale mentre eseguivo il Vision Chatbot.

“FB-OVF” significa Frame Buffer Overflow. Indica che la fotocamera invia dati più velocemente di quanto l’ESP32 possa processarli o memorizzarli.

Il metodo consigliato per evitare questo errore è ridurre il framerate. Ho quindi modificato il codice originale della fotocamera impostando il framerate a 8 MHz:

config.xclk_freq_hz = 8000000;

Questo ha eliminato gli errori “cam_hal: FB-OVF”.

Cartella del progetto

Puoi scaricare l’intero file di progetto vision_chatbot.zip o creare tu stesso il progetto Arduino per il Vision Chatbot. Per farlo crea una cartella “vision_chatbot” con due file (“camera.h”, “vision_chatbot.ino”) al suo interno:

Il file “camera.h” contiene il codice per la fotocamera e “vision_chatbot.ino” contiene il codice per il Vision Chatbot. Una volta impostata la scheda corretta (“ESP32S3 Dev Module”) e le impostazioni strumenti corrette (PSRAM, Huge APP, …) puoi caricare il codice sulla scheda e goderti il Vision Chatbot in azione. Le due sezioni successive mostrano due esempi con le risposte del Vision Chatbot.

Esempio: Cosa vedi?

In questo primo esempio, ho mostrato alla ESP32-S3 AI Camera un piccolo modello di teschio sulla mia scrivania con altri oggetti sullo sfondo.

Ecco l’output del Vision Chatbot sul Monitor Seriale quando gli è stata mostrata questa immagine:

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.

Puoi vedere la domanda “Cosa vedi?” e la risposta del Chatbot “Vedo un piccolo teschio posizionato su una superficie chiara, probabilmente un tavolo. Sullo sfondo ci sono vari oggetti e forse un po’ di disordine. L’ambiente sembra essere uno spazio interno.”

Nota che all’inizio compare un messaggio di errore “E (1763) i2s_common: i2s_channel_disable(1217): the channel has not been enabled yet” che puoi ignorare. Sembra legato a un problema con l’attuale core ESP32 ma non influisce sul funzionamento del Chatbot.

Esempio: Quanti orologi?

Puoi anche chiedere al Chatbot di riconoscere oggetti specifici in un’immagine. Non funziona sempre, e il bot ha avuto difficoltà a riconoscere sedie, per esempio. Tuttavia, nella scena piuttosto complessa mostrata sotto, che contiene un orologio da parete, il Chatbot ha correttamente indicato che c’è un orologio:

Ecco l’output sul Monitor Seriale. Ho chiesto “Quanti orologi ci sono in questa immagine?” e la risposta è stata “C’è un orologio visibile nell’immagine.”

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.

Considerando la bassa qualità, la piccola dimensione dell’immagine e la presenza di molti oggetti, il Vision Chatbot è stato davvero bravo a trovare l’orologio.

Divertiti a usare il tuo Vision Chatbot ma tieni d’occhio i costi OpenAI e il tuo budget, specialmente se inizi a usare modelli più potenti ma anche più costosi.

Conclusioni

In questo tutorial hai imparato a costruire un Vision Chatbot usando il modulo DFRobot ESP32-S3 AI Camera e OpenAI. La registrazione audio, la riproduzione e la cattura delle immagini sono state eseguite localmente dall’ESP32, mentre Text-To-Speech, Speech-To-Text e analisi delle immagini sono stati eseguiti da servizi OpenAI remoti.

L’elaborazione remota di immagini e audio ci permette di usare modelli AI molto più potenti di quelli eseguibili localmente sull’ESP32. Tuttavia, questo richiede una connessione internet stabile per comunicare con i servizi cloud di OpenAI e introduce latenza e costi. Inoltre, la privacy è un aspetto da considerare quando si inviano immagini a server esterni.

Se vuoi eseguire un riconoscimento vocale semplice localmente, dai un’occhiata ai tutorial Getting started with Gravity Voice Recognition Module, Voice control with XIAO-ESP32-S3-Sense and Edge Impulse e Using the Voice Recognition Module V3 with Arduino.

Per speech-to-text vedi il tutorial Gravity Text-to-Speech Module Tutorial, che può generare parlato localmente ma con qualità limitata.

Per altri esempi di codice per la DFRobot ESP32-S3 AI Camera dai un’occhiata a DFRobot Wiki e al loro github repo.

Se hai domande, sentiti libero di lasciarle nella sezione commenti.

Buon divertimento con il tuo tinkering ; )