Shubh Jaiswal
Published

NOVA - An AI assistant for optical store

Nova is interactive AI kiosk that lets you virtually try on glasses to find perfect pair and buy them instantly!

AdvancedFull instructions provided3 hours295
NOVA - An AI assistant for optical store

Things used in this project

Story

Read more

Schematics

epaper_driver_board- Schematic

CIRCUIT DIAGRAM

Code

NOVA - AI Smart Glasses Assistant

Python
NOTE:
Before running, replace the openai.api_key value with your own from OpenAI, update device paths (/dev/ttyACM0 for serial, /dev/video0 for camera), adjust the Chromium launch command for your OS, and install dependencies.
import os
import cv2
import time
import wave
import serial
import asyncio
import openai
import sounddevice as sd
import numpy as np
import edge_tts
import subprocess
from faster_whisper import WhisperModel
import mediapipe as mp

# ---------------- CONFIG ----------------
SERIAL_PORT = "/dev/ttyACM0"
BAUD_RATE = 115200
AUDIO_FILE = "input.wav"
IMAGE_FILE = "/home/shubh/image/captured.jpg"
MP3_OUTPUT_PATH = "/home/shubh/Music/response.mp3"
BLANK_HTML = "/home/shubh/blank.html"
MAIN_WEBSITE = "https://dhruvpandit46.github.io/Glasses/"

# Hardcoded API key (replace with yours)
openai.api_key = "sk-REPLACE_WITH_YOUR_KEY"

KEYWORD_URLS = {
    "man": "https://dhruvpandit46.github.io/Glasses/?sku=rayban_justin_noir_rougeMirroir",
    "woman": "https://dhruvpandit46.github.io/Glasses/?sku=rayban_clubround_noir_cuivre_flash",
    "rectangle": "https://dhruvpandit46.github.io/Glasses/?sku=rayban_chris_noir_gun_bleu_mirroir",
    "square": "https://dhruvpandit46.github.io/Glasses/?sku=rayban_justin_noir_rougeMirroir",
    "round": "https://dhruvpandit46.github.io/Glasses/?sku=rayban_round_gun_vert",
    "aviator": "https://dhruvpandit46.github.io/Glasses/?sku=rayban_aviator_or_marron",
    "metal": "https://dhruvpandit46.github.io/Glasses/?sku=rayban_erika_marronArgent_marronVioletDegrade",
    "plastic": "https://dhruvpandit46.github.io/Glasses/?sku=rayban_justin_noir_bleuMirroir"
}

# Whisper model (lightweight)
whisper_model = WhisperModel("tiny", compute_type="int8")

# Globals
face_shape_memory = None
last_url = MAIN_WEBSITE
recording_buffer = []
recording_stream = None

# ---------------- AUDIO RECORDING ----------------
def audio_callback(indata, frames, time_info, status):
    if status:
        print(status)
    recording_buffer.append(indata.copy())

def start_audio_recording(sr=16000):
    global recording_buffer, recording_stream
    recording_buffer = []
    try:
        recording_stream = sd.InputStream(samplerate=sr, channels=1, dtype='int16', callback=audio_callback)
        recording_stream.start()
        print("Recording started...")
    except Exception as e:
        print("Failed to start audio recording:", e)
        recording_stream = None

def stop_audio_recording(file, sr=16000):
    global recording_stream
    if recording_stream:
        try:
            recording_stream.stop()
            recording_stream.close()
        except Exception as e:
            print("Error stopping stream:", e)
    if not recording_buffer:
        print("No audio recorded.")
        with wave.open(file, 'wb') as wf:
            wf.setnchannels(1)
            wf.setsampwidth(2)
            wf.setframerate(sr)
            wf.writeframes(b'')
        return
    audio = np.concatenate(recording_buffer, axis=0)
    try:
        with wave.open(file, 'wb') as wf:
            wf.setnchannels(1)
            wf.setsampwidth(2)
            wf.setframerate(sr)
            wf.writeframes(audio.tobytes())
        print("Audio saved")
    except Exception as e:
        print("Failed to write audio file:", e)

# ---------------- CAMERA ----------------
def capture_image():
    for attempt in range(3):
        cap = cv2.VideoCapture("/dev/video0")
        cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
        cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
        time.sleep(0.5)
        if cap.isOpened():
            ret, frame = cap.read()
            cap.release()
            if ret:
                try:
                    os.makedirs(os.path.dirname(IMAGE_FILE), exist_ok=True)
                    cv2.imwrite(IMAGE_FILE, frame)
                    print(f"Image saved at {IMAGE_FILE}")
                    return True
                except Exception as e:
                    print("Failed to save image:", e)
                    return False
        else:
            cap.release()
        print(f"Retrying camera... (attempt {attempt+1}/3)")
        time.sleep(1)
    print("Error: Camera not accessible")
    return False

# ---------------- FACE SHAPE DETECTION ----------------
def detect_face_shape(path):
    img = cv2.imread(path)
    if img is None:
        print("detect_face_shape: image not found or unreadable.")
        return None
    with mp.solutions.face_mesh.FaceMesh(static_image_mode=True, max_num_faces=1) as mesh:
        res = mesh.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        if not res.multi_face_landmarks:
            print("No face landmarks detected.")
            return None
        lm = res.multi_face_landmarks[0].landmark
        h, w = img.shape[:2]
        def pt(i): return np.array([lm[i].x * w, lm[i].y * h])
        try:
            jaw_w = np.linalg.norm(pt(234) - pt(454))
            cheek = np.linalg.norm(pt(93) - pt(323))
            forehead = np.linalg.norm(pt(127) - pt(356))
            length = np.linalg.norm(pt(152) - pt(10))
        except Exception as e:
            print("Landmark math failed:", e)
            return None
        r1 = length / max(jaw_w, 1e-6)
        r2 = cheek / max(jaw_w, 1e-6)
        r3 = forehead / max(jaw_w, 1e-6)
        if r1 > 1.5 and r2 < 0.95:
            return "oblong"
        elif r3 > 1.05 and r2 > 1.05:
            return "heart"
        elif r2 > 1.1 and r3 < 0.95:
            return "diamond"
        elif abs(jaw_w - length) < 40:
            return "round"
        elif abs(jaw_w - cheek) < 20:
            return "square"
        return "oval"

# ---------------- SPEECH -> TEXT ----------------
def speech_to_text(file):
    try:
        segs, info = whisper_model.transcribe(file)
        txt = " ".join([s.text for s in segs])
        lang = info.language or "en"
        return txt.strip(), lang
    except Exception as e:
        print("Whisper transcription failed:", e)
        return "", "en"

# ---------------- CHATGPT CALL ----------------
def chatgpt_response(prompt):
    global last_url, face_shape_memory
    system_msg = (
        "You are Nova, a stylist assistant. "
        f"Current preview URL user is seeing: {last_url}. "
        "If user asks about appearance or if glasses suit them, respond accordingly. "
        "If they say 'show me' glasses, reply with a short sentence like 'Here are glasses for man', "
        "followed by exactly one keyword: man, woman, round, oval, square, rectangle, aviator, diamond, heart, metal, plastic."
    )
    if face_shape_memory:
        system_msg += f" Face shape: {face_shape_memory}."
    try:
        res = openai.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": system_msg},
                {"role": "user", "content": prompt}
            ]
        )
        return res.choices[0].message.content.strip()
    except Exception as e:
        print("OpenAI API error:", e)
        return "Sorry, I'm having trouble connecting to my brain. Please try again later."

# ---------------- TTS ----------------
async def speak(text, lang):
    voice = {"en": "en-IN-NeerjaNeural", "hi": "hi-IN-SwaraNeural"}.get(lang, "en-IN-NeerjaNeural")
    try:
        tts = edge_tts.Communicate(text, voice)
        await tts.save(MP3_OUTPUT_PATH)
        os.system(f"ffplay -nodisp -autoexit '{MP3_OUTPUT_PATH}' > /dev/null 2>&1")
    except Exception as e:
        print("TTS failed:", e)

# ---------------- URL KEYWORD HANDLING ----------------
def extract_sku_keywords(text):
    text = text.lower()
    return [k for k in KEYWORD_URLS if k in text]

def open_dynamic_url(keywords):
    global last_url
    if not keywords:
        print("No keyword found. Re-opening last URL.")
        return
    keyword = keywords[0]
    new_url = KEYWORD_URLS.get(keyword)
    if new_url and new_url != last_url:
        last_url = new_url
        print("Opening new target URL:", last_url)
        os.system("pkill -f 'chromium-browser' || true")
        time.sleep(1)
        os.system(f"chromium-browser --kiosk --incognito --disk-cache-dir=/dev/null --disable-logging --noerrdialogs --app='{last_url}' > /dev/null 2>&1 &")
    else:
        print("Keyword already shown or unknown.")

# ---------------- UI PLACEHOLDERS ----------------
def show_blank_loading_page(message):
    with open(BLANK_HTML, "w") as f:
        f.write(f"""
<html><body style='background:black;color:white;font-size:3em;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;text-align:center;'>
<div>{message}</div>
<div style='border:6px solid white;border-top:6px solid transparent;border-radius:50%;width:40px;height:40px;margin-top:20px;animation:spin 1s linear infinite;'></div>
<style>@keyframes spin{{0%{{transform:rotate(0)}}100%{{transform:rotate(360deg)}}}}</style>
</body></html>
""")
    os.system("pkill -f 'chromium-browser' || true")
    os.system(f"chromium-browser --kiosk --incognito --disk-cache-dir=/dev/null --disable-logging --noerrdialogs 'file://{BLANK_HTML}' > /dev/null 2>&1 &")

def display_response_text(reply):
    with open(BLANK_HTML, "w") as f:
        f.write(f"""
<html><body style='background:black;color:white;font-size:3em;display:flex;align-items:center;justify-content:center;height:100vh;text-align:center;'>
<div id='response'></div>
<script>
const text = `{reply}`;
let i = 0;
function typeEffect() {{
    if (i < text.length) {{
        document.getElementById("response").innerHTML += text.charAt(i);
        i++;
        setTimeout(typeEffect, 80);
    }}
}}
typeEffect();
</script>
</body></html>
""")
    os.system("pkill -f 'chromium-browser' || true")
    time.sleep(1)
    os.system(f"chromium-browser --kiosk --incognito --disk-cache-dir=/dev/null --disable-logging --noerrdialogs 'file://{BLANK_HTML}' > /dev/null 2>&1 &")

# ---------------- MAIN LOOP ----------------
def main():
    global face_shape_memory
    print("Starting system...")
    open_dynamic_url(["man"])  # start with a default URL
    while True:
        start_audio_recording()
        time.sleep(5)  # record for 5 seconds
        stop_audio_recording(AUDIO_FILE)
        text, lang = speech_to_text(AUDIO_FILE)
        if not text:
            continue
        print(f"User said: {text}")
        if "photo" in text.lower() or "picture" in text.lower():
            if capture_image():
                face_shape = detect_face_shape(IMAGE_FILE)
                if face_shape:
                    face_shape_memory = face_shape
                    print(f"Detected face shape: {face_shape}")
                else:
                    face_shape_memory = None
        reply = chatgpt_response(text)
        print(f"Assistant: {reply}")
        display_response_text(reply)
        asyncio.run(speak(reply, lang))
        keywords = extract_sku_keywords(reply)
        open_dynamic_url(keywords)

if __name__ == "__main__":
    main()

XIAO C3 - MQTT E-Paper Order Display(Arduino ide)

C/C++
Update WLAN_SSID/WLAN_PASS with your Wi-Fi details, set AIO_USERNAME/AIO_KEY from your Adafruit IO account, ensure the feed name matches your actual Adafruit IO feed, and install the required Arduino libraries following the Seeed Studio e-paper display guide for Arduino before uploading.
#include <WiFi.h>
#include <Adafruit_MQTT.h>
#include <Adafruit_MQTT_Client.h>
#include <ArduinoJson.h>
#include "TFT_eSPI.h"

#define EPAPER_ENABLE

#ifdef EPAPER_ENABLE
EPaper epaper;
#endif

// WiFi Credentials
#define WLAN_SSID       ""
#define WLAN_PASS       ""

// MQTT (Adafruit IO)
#define AIO_SERVER      "io.adafruit.com"
#define AIO_SERVERPORT  1883
#define AIO_USERNAME    ""
#define AIO_KEY         ""

WiFiClient client;
Adafruit_MQTT_Client mqtt(&client, AIO_SERVER, AIO_SERVERPORT, AIO_USERNAME, AIO_KEY);
Adafruit_MQTT_Subscribe orderFeed = Adafruit_MQTT_Subscribe(&mqtt, AIO_USERNAME "/feeds/QR CODE");

void connectWiFi() {
  Serial.print("Connecting to WiFi...");
  WiFi.begin(WLAN_SSID, WLAN_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println(" Connected!");
  Serial.println(WiFi.localIP());
}

void connectMQTT() {
  Serial.print("Connecting to MQTT...");
  while (!mqtt.connected()) {
    if (mqtt.connect()) {
      Serial.println(" Connected!");
    } else {
      Serial.print(" Failed: ");
      Serial.println(mqtt.connectErrorString(mqtt.connect()));
      delay(1000);
    }
  }
}

void showOrder(String orderID, String name, String desc, String price, String time) {
  Serial.println("=== Order Received ===");
  Serial.println("Token ID  : " + orderID);
  Serial.println("Name      : " + name);
  Serial.println("Desc      : " + desc);
  Serial.println("Price     : " + price);
  Serial.println("Time      : " + time);
  Serial.println("======================");

#ifdef EPAPER_ENABLE
  epaper.fillScreen(TFT_WHITE);

  // Header
  epaper.fillRect(0, 0, epaper.width(), 60, TFT_BLACK);
  epaper.setTextColor(TFT_WHITE);
  epaper.setTextSize(3);
  epaper.setCursor(20, 15);
  epaper.print("GLASSES ORDER");

  // Token Box
  epaper.setTextColor(TFT_BLACK);
  epaper.drawRect(20, 80, epaper.width() - 40, 60, TFT_BLACK);
  epaper.setTextSize(2);
  epaper.setCursor(30, 100);
  epaper.print("Token ID: " + orderID);

  // Info Box
  epaper.drawRect(20, 160, epaper.width() - 40, 100, TFT_BLACK);
  epaper.setCursor(30, 180);
  epaper.print("Name : " + name);
  epaper.setCursor(30, 200);
  epaper.print("Desc : " + desc);
  epaper.setCursor(30, 220);
  epaper.print("Price: " + price);

  // Footer
  epaper.setCursor(30, 240);
  epaper.print("Time : " + time);

  epaper.update();
#endif
}

void setup() {
  Serial.begin(115200);
  delay(1000);

#ifdef EPAPER_ENABLE
  epaper.begin();
#endif

  connectWiFi();
  mqtt.subscribe(&orderFeed);
  connectMQTT();
}

void reconnectMQTT() {
    int8_t ret;
    
    Serial.print("Connecting to MQTT... ");
    
    uint8_t retries = 3;
    while ((ret = mqtt.connect()) != 0) {
        Serial.println(mqtt.connectErrorString(ret));
        Serial.println("Retrying MQTT connection...");
        mqtt.disconnect();
        delay(5000);  // Wait 5 seconds
        retries--;
        if (retries == 0) {
            Serial.println("MQTT connection failed, will try again later");
            return;
        }
    }
    
    Serial.println("MQTT connected!");
    
    // Re-subscribe to ensure we receive updates
    
}

void loop() {
    if (!mqtt.connected()) {
        reconnectMQTT();
    }

  Adafruit_MQTT_Subscribe *subscription;
  while ((subscription = mqtt.readSubscription(5000))) {
    if (subscription == &orderFeed) {
      Serial.println("MQTT Data Received");

      StaticJsonDocument<512> doc;
      DeserializationError err = deserializeJson(doc, (char *)orderFeed.lastread);

      if (err) {
        Serial.print("JSON Error: ");
        Serial.println(err.c_str());
        return;
      }

      if (doc["cmd"] == "print") {
        String orderID  = doc["orderID"] | "??";
        String name     = doc["name"] | "??";
        String desc     = doc["description"] | "??";
        String price    = doc["price"] | "??";
        String time     = doc["ts"]["display"] | "??";

        showOrder(orderID, name, desc, price, time);
      } else {
        Serial.println("Non-print cmd ignored");
      }
    }
  }
}

XIAO NRF SENSE(arduino ide)

C/C++
Plug your Seeed Studio XIAO directly into the Raspberry Pi via USB. It will appear as /dev/ttyACM0 on the Pi. This code runs on the XIAO and sends simple serial commands — "CAPTURE" when you press the button and "RELEASE" when you let go. The button is wired to the defined pin (D1), and the onboard LED lights up while the button is pressed. Make sure your Pi is listening to /dev/ttyACM0 to receive these commands and act accordingly.
#define BUTTON_PIN D1      // GPIO1 (D1 on XIAO)
#define LED_PIN LED_BUILTIN  // Built-in LED pin

bool buttonPreviouslyPressed = false;

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);   // Internal pull-up resistor
  pinMode(LED_PIN, OUTPUT);            // LED output
  digitalWrite(LED_PIN, LOW);          // Start with LED off
  Serial.begin(115200);
}

void loop() {
  bool buttonPressed = (digitalRead(BUTTON_PIN) == LOW);

  if (buttonPressed && !buttonPreviouslyPressed) {
    Serial.println("CAPTURE");           // Notify Pi to capture image + start audio
    digitalWrite(LED_PIN, LOW);         // Turn LED ON
    buttonPreviouslyPressed = true;
  }

  if (!buttonPressed && buttonPreviouslyPressed) {
    Serial.println("RELEASE");           // Notify Pi to stop audio + send to ChatGPT
    digitalWrite(LED_PIN, HIGH);          // Turn LED OFF
    buttonPreviouslyPressed = false;
  }

  delay(10);  // Simple debounce
}

Credits

Shubh Jaiswal
2 projects • 18 followers
IoT & Embedded Systems Developer | Automation | DIY Enthusiast | Passionate about Smart Solutions | Intern @ Techiesms

Comments