# Project export: VisionMate: the AI agent to help visually impaired people

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2025
- Tagline: VisionMate is a context-aware AI agent designed to support visually impaired individuals by selecting the most suitable tool—text-to-braille conversion, image-to-audio narration, or watch vibrations.
- Devpost: https://devpost.com/software/orion-pcwlg3
- GitHub: https://github.com/anaiskillian/treehacks25
- Demo: https://www.vision-m8.com/
- Video: https://www.youtube.com/embed/B5CtpyK8U3E?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — AnaisKillian (40 commits), Jeet Dekivadia (14 commits), Eric (5 commits), Pratham Pilli (1 commits)

## Devpost submission (written by the team)

### Overview

Our

### Inspiration

We’ve seen that visually impaired individuals face challenges in navigating everyday environments and accessing information. These challenges include reading documents, detecting nearby objects, and listening to environments. Current assistive technologies often require users to manually switch between multiple tools, creating inefficiency. Introducing VisionMate We created an iPhone Operating Systems app, a webpage, and an Apple Watch app specially designed for this suite of products: text-to-braille conversion, image-to-audio conversion, and object-awareness detection. VisionMate is the first AI Agent Suite designed to help visually impaired individuals. The iOS app automatically detects objects that are nearby, sending a pulsing signal to the Apple Watch when objects are within one foot. The agent automatically determines whether to use text-to-braille conversion or image-to-audio conversion when using the video. The Apple Watch app was trained on motion-moving datasets to detect a thumbs-up trigger. When the user gives a thumbs-up in the app, this stops the video streaming in the webpage and any audio or braille that is being played. We created a convolutional neural network which was converted into the Core-ML format for Watch iOS.

### How we built it

We knew coming into TreeHacks of our project idea, and we got right to building in minute 1. Jeet worked on all things front-end, Pratham worked on the object detection, Anaïs worked on text-to-braille conversion and image-to-audio conversion, and Eric worked on training the watch motion detection using a convolutional neural net. The object detection was done to detect objects in a one-foot-radius of the app. We use an augmented reality kit which helps us get in-depth footage of objects around us. Once we detect an object, we send a signal to the watch app to trigger a response as haptic feedback. We used the Stanford Product Lab to create the hardware component of the project with the braille.

### Challenges we ran into

and accomplishments that we're proud of We ran into the challenge of connecting the frontend to the backend (classic) because we had a bunch of different languages (Python, Node.js, Swift, Typescript, CSS, JavaScript, etc.) that were not compatible with each other. We also ran into the challenge of connecting the watch and iOS appes, and creating the agent to decide the context. With lots of debugging and hours of work, we were able to overcome these challenges and make our suite of products that we are very proud of!

### What we learned

We learned about a variety of frameworks and how to integrate them together, including how to train convolutional neural networks using motion training data. We also learned about more Apple Watch development and iOS development.

### What's next

We are excited to expand this product and hopefully take it to become a real AI Agent that can be used for sale. We are looking into speaking to accelerator programs about our product and/ or venture capital firms. Tracks For the Edge AI Track Challenge 1: We created an Apple Core ML embedded on the watch with watch OS. We created a Convolutional Neural Network to train the motion using a dataset with accelerometer data, and gyroscope data. Our model is 181 kB, trained with tensor flow for 15 epochs. For Rox, we created an iPhone Operating Systems app, a webpage, and an Apple Watch app specially designed for this suite of products: text-to-braille conversion, image-to-audio conversion, and object-awareness detection. VisionMate is the first AI Agent Suite designed to help visually impaired individuals. For the Vercel Track: We created an Apple Core ML embedded on the watch with watch OS. We created a Convolutional Neural Network to train the motion using a dataset with accelerometer data, and gyroscope data. Our model is 181 kB, trained with tensor flow for 15 epochs. For the DAIN Labs: We created an iPhone Operating Systems app, a webpage, and an Apple Watch app specially designed for this suite of products: text-to-braille conversion, image-to-audio conversion, and object-awareness detection. VisionMate is the first AI Agent Suite designed to help visually impaired individuals. For LumaLabs: We created an iPhone Operating Systems app, a webpage, and an Apple Watch app specially designed for this suite of products: text-to-braille conversion, image-to-audio conversion, and object-awareness detection. VisionMate is the first AI Agent Suite designed to help visually impaired individuals. For Vespa.ai: We created an iPhone Operating Systems app, a webpage, and an Apple Watch app specially designed for this suite of products: text-to-braille conversion, image-to-audio conversion, and object-awareness detection. VisionMate is the first AI Agent Suite designed to help visually impaired individuals. For EigenLayer: We created an iPhone Operating Systems app, a webpage, and an Apple Watch app specially designed for this suite of products: text-to-braille conversion, image-to-audio conversion, and object-awareness detection. VisionMate is the first AI Agent Suite designed to help visually impaired individuals. For OpenAI: We used the OpenAI API to detect what type of image was used and based on that, chose text-to-braille or image-to-audio. For Hudson River Trading (HRT): We created an Apple Core ML embedded on the watch with watch OS. We created a Convolutional Neural Network to train the motion using a dataset with accelerometer data, and gyroscope data. Our model is 181 kB, trained with tensor flow for 15 epochs.

## README (from the GitHub repository)

# VisionMate 🦾🔵

**An AI-powered assistive suite for the visually impaired.**

VisionMate is a **context-aware AI agent** designed to help visually impaired individuals navigate the world seamlessly. By integrating real-time **text-to-braille conversion, image-to-audio narration, and object-awareness detection**, VisionMate provides an intuitive and accessible experience across multiple platforms.

## 🚀 Features

✨ **Text-to-Braille Conversion** - Converts written text into Braille output for enhanced accessibility.  
🎙️ **Image-to-Audio Narration** - Uses AI-powered object detection and scene description to generate audio descriptions.  
🦾 **Object Awareness Detection** - Detects nearby objects and provides haptic feedback via Apple Watch.  
⌚ **Apple Watch Integration** - Motion-triggered controls and real-time notifications.  
🤖 **Seamless Context Switching** - AI agent determines the best assistive tool automatically.  

## 🛠️ Built With

🔹 **AI/ML**: OpenAI API, Apple Core ML, TensorFlow  
💻 **Frontend**: Swift (iOS), TypeScript, React  
🖥 **Backend**: Python, Node.js  
⌚ **Hardware**: Apple Watch (WatchOS)  
📊 **Data Processing**: Accelerometer & Gyroscope Data, Augmented Reality Kit (ARKit)  

## 📸 How It Works

1️⃣ **Real-time Object Detection:** VisionMate detects objects within a 1-foot radius using ARKit.  
2️⃣ **Adaptive AI Decision-Making:** Determines whether to convert text to Braille or narrate the scene via audio.  
3️⃣ **Haptic Feedback for Navigation:** Apple Watch vibrates when an object is detected nearby.  
4️⃣ **Gesture-Based Control:** A thumbs-up gesture stops all AI-generated outputs.  

## 🎯 Our Mission

We aim to **redefine accessibility** by providing an AI-powered assistive technology that adapts to individual needs, ensuring inclusivity and independence for visually impaired users.

## 👥 Teammates

🧑‍💻 **Eric Wang**  
🧑‍💻 **Pratham Pilli**  
🧑‍💻 **Jeet Hirenkumar Dekivadia**  
🧑‍💻 **Anaïs Killian** 


## Detected evidence (automated analysis)

Indexed codebase: 81 recognized source files, 212 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- C++ (language) — claimed on Devpost, not found in the code
- Swift (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (100 of 100)

```
.gitignore
.gitmodules
braille_test.py
gesture_classifier/.DS_Store
gesture_classifier/gesture_classifier_3classes.keras
gesture_classifier/gesture_classifier_3classes.mlpackage/Data/com.apple.CoreML/model.mlmodel
gesture_classifier/gesture_classifier_3classes.mlpackage/Manifest.json
gesture_classifier/gesture_classifier.h5
gesture_classifier/gesture_classifier.keras
gesture_classifier/gesture_classifier.mlpackage/Data/com.apple.CoreML/model.mlmodel
gesture_classifier/gesture_classifier.mlpackage/Manifest.json
gesture_classifier/process_data.ipynb
nano.py
opencvtext.py
README.md
refreshable_braille_display/refreshable_braille_display.ino
server/server.py
start.py
test_cameras.py
website/.gitignore
website/app/api/analyze/route.ts
website/app/api/process-image/route.ts
website/app/camera/page.tsx
website/app/globals.css
website/app/layout.tsx
website/app/page.tsx
website/components.json
website/components/AIAssistant.tsx
website/components/CameraVision.tsx
website/components/CTA.tsx
website/components/Dashboard.tsx
website/components/EmergencySOS.tsx
website/components/Features.tsx
website/components/Hero.tsx
website/components/HowItWorks.tsx
website/components/ProblemSolution.tsx
website/components/SmartReading.tsx
website/components/theme-provider.tsx
website/components/ui/accordion.tsx
website/components/ui/alert-dialog.tsx
website/components/ui/alert.tsx
website/components/ui/aspect-ratio.tsx
website/components/ui/avatar.tsx
website/components/ui/badge.tsx
website/components/ui/breadcrumb.tsx
website/components/ui/button.tsx
website/components/ui/calendar.tsx
website/components/ui/card.tsx
website/components/ui/carousel.tsx
website/components/ui/chart.tsx
website/components/ui/checkbox.tsx
website/components/ui/collapsible.tsx
website/components/ui/command.tsx
website/components/ui/context-menu.tsx
website/components/ui/dialog.tsx
website/components/ui/drawer.tsx
website/components/ui/dropdown-menu.tsx
website/components/ui/form.tsx
website/components/ui/hover-card.tsx
website/components/ui/input-otp.tsx
website/components/ui/input.tsx
website/components/ui/label.tsx
website/components/ui/menubar.tsx
website/components/ui/navigation-menu.tsx
website/components/ui/pagination.tsx
website/components/ui/popover.tsx
website/components/ui/progress.tsx
website/components/ui/radio-group.tsx
website/components/ui/resizable.tsx
website/components/ui/scroll-area.tsx
website/components/ui/select.tsx
website/components/ui/separator.tsx
website/components/ui/sheet.tsx
website/components/ui/sidebar.tsx
website/components/ui/skeleton.tsx
website/components/ui/slider.tsx
website/components/ui/sonner.tsx
website/components/ui/switch.tsx
website/components/ui/table.tsx
website/components/ui/tabs.tsx
website/components/ui/textarea.tsx
website/components/ui/toast.tsx
website/components/ui/toaster.tsx
website/components/ui/toggle-group.tsx
website/components/ui/toggle.tsx
website/components/ui/tooltip.tsx
website/components/ui/use-mobile.tsx
website/components/ui/use-toast.ts
website/components/UserPreferences.tsx
website/hooks/use-mobile.tsx
website/hooks/use-toast.ts
website/lib/utils.ts
website/next.config.mjs
website/package.json
website/postcss.config.mjs
website/server.js
website/styles/globals.css
website/tailwind.config.js
website/tsconfig.json
yolov5s.pt
```

### Dependencies

- website/package.json: @emotion/is-prop-valid@latest, @hookform/resolvers@^3.9.1, @radix-ui/react-accordion@^1.2.2, @radix-ui/react-alert-dialog@^1.1.4, @radix-ui/react-aspect-ratio@^1.1.1, @radix-ui/react-avatar@^1.1.2, @radix-ui/react-checkbox@^1.1.3, @radix-ui/react-collapsible@^1.1.2, @radix-ui/react-context-menu@^2.2.4, @radix-ui/react-dialog@^1.1.4, @radix-ui/react-dropdown-menu@^2.1.4, @radix-ui/react-hover-card@^1.1.4, @radix-ui/react-label@^2.1.1, @radix-ui/react-menubar@^1.1.4, @radix-ui/react-navigation-menu@^1.2.3, @radix-ui/react-popover@^1.1.4, @radix-ui/react-progress@^1.1.1, @radix-ui/react-radio-group@^1.2.2, @radix-ui/react-scroll-area@^1.2.2, @radix-ui/react-select@^2.1.4, @radix-ui/react-separator@^1.1.1, @radix-ui/react-slider@^1.2.2, @radix-ui/react-slot@^1.1.1, @radix-ui/react-switch@^1.1.2, @radix-ui/react-tabs@^1.1.2, @radix-ui/react-toast@^1.2.4, @radix-ui/react-toggle@^1.1.1, @radix-ui/react-toggle-group@^1.1.1, @radix-ui/react-tooltip@^1.1.6, @types/node@^22, @types/react@^18, @types/react-dom@^18, autoprefixer@^10.4.20, axios@^1.7.9, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@1.0.4, cors@^2.8.5, date-fns@4.1.0, embla-carousel-react@8.5.1, express@^4.21.2, framer-motion@latest, input-otp@1.4.1, lucide-react@^0.454.0, next@14.2.16, next-themes@^0.4.4, openai@latest, postcss@^8, react@^18, react-day-picker@8.10.1, react-dom@^18, react-hook-form@^7.54.1, react-intersection-observer@latest, react-resizable-panels@^2.1.7, recharts@2.15.0, sonner@^1.7.1, tailwind-merge@^2.5.5, tailwindcss@latest, tailwindcss@^3.4.17, tailwindcss-animate@^1.0.7, typescript@^5, vaul@^0.9.6, zod@^3.24.1

### Recent commits (newest first)

- Update tsconfig.json
- Update globals.css
- Update Hero.tsx
- Update route.ts
- Update package.json
- Create route.ts
- Update CameraVision.tsx
- Create page.tsx
- Update Hero.tsx
- Update CameraVision.tsx
- Update README.md
- ml model
- Add files via upload
- update
- Merge pull request #3 from anaiskillian/revert-2-new-frontend
- Revert "to publich website online"
- Merge pull request #2 from anaiskillian/new-frontend
- for eric
- publish the website
- to publich website online

## Key source files (fetched from GitHub, selected and truncated for size)

### website/package.json

```
{
  "name": "my-v0-project",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@emotion/is-prop-valid": "latest",
    "@hookform/resolvers": "^3.9.1",
    "@radix-ui/react-accordion": "^1.2.2",
    "@radix-ui/react-alert-dialog": "^1.1.4",
    "@radix-ui/react-aspect-ratio": "^1.1.1",
    "@radix-ui/react-avatar": "^1.1.2",
    "@radix-ui/react-checkbox": "^1.1.3",
    "@radix-ui/react-collapsible": "^1.1.2",
    "@radix-ui/react-context-menu": "^2.2.4",
    "@radix-ui/react-dialog": "^1.1.4",
    "@radix-ui/react-dropdown-menu": "^2.1.4",
    "@radix-ui/react-hover-card": "^1.1.4",
    "@radix-ui/react-label": "^2.1.1",
    "@radix-ui/react-menubar": "^1.1.4",
    "@radix-ui/react-navigation-menu": "^1.2.3",
    "@radix-ui/react-popover": "^1.1.4",
    "@radix-ui/react-progress": "^1.1.1",
    "@radix-ui/react-radio-group": "^1.2.2",
    "@radix-ui/react-scroll-area": "^1.2.2",
    "@radix-ui/react-select": "^2.1.4",
    "@radix-ui/react-separator": "^1.1.1",
    "@radix-ui/react-slider": "^1.2.2",
    "@radix-ui/react-slot": "^1.1.1",
    "@radix-ui/react-switch": "^1.1.2",
    "@radix-ui/react-tabs": "^1.1.2",
    "@radix-ui/react-toast": "^1.2.4",
    "@radix-ui/react-toggle": "^1.1.1",
    "@radix-ui/react-toggle-group": "^1.1.1",
    "@radix-ui/react-tooltip": "^1.1.6",
    "autoprefixer": "^10.4.20",
    "axios": "^1.7.9",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "1.0.4",
    "cors": "^2.8.5",
    "date-fns": "4.1.0",
    "embla-carousel-react": "8.5.1",
    "express": "^4.21.2",
    "framer-motion": "latest",
    "input-otp": "1.4.1",
    "lucide-react": "^0.454.0",
    "next": "14.2.16",
    "next-themes": "^0.4.4",
    "openai": "latest",
    "react": "^18",
    "react-day-picker": "8.10.1",
    "react-dom": "^18",
    "react-hook-form": "^7.54.1",
    "react-intersection-observer": "latest",
    "react-resizable-panels": "^2.1.7",
    "recharts": "2.15.0",
    "sonner": "^1.7.1",
    "tailwind-merge": "^2.5.5",
    "tailwindcss": "latest",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "^0.9.6",
    "zod": "^3.24.1"
  },
  "devDependencies": {
    "@types/node": "^22",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "postcss": "^8",
    "tailwindcss": "^3.4.17",
    "typescript": "^5"
  }
}

```

### website/server.js

```javascript
const express = require("express");
const { exec } = require("child_process");
const cors = require("cors");
const path = require("path");

const app = express();
app.use(cors());
app.use(express.json());

const OPENCV_SCRIPT_PATH = "/Users/anaiskillian/treehacks/opencvtext.py";

app.post("/run-opencv", (req, res) => {
  exec(`python3 ${OPENCV_SCRIPT_PATH}`, (error, stdout, stderr) => {
    if (error) {
      console.error(`Error: ${error.message}`);
      return res.status(500).json({ error: error.message });
    }
    if (stderr) {
      console.error(`stderr: ${stderr}`);
      return res.status(500).json({ error: stderr });
    }
    console.log(`stdout: ${stdout}`);
    res.json({ message: "Python script executed successfully!", output: stdout });
  });
});

const PORT = 5001;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

```

### server/server.py

```python
from flask import Flask, request, jsonify
import subprocess
import os
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

# Path to the OpenCV script
OPENCV_SCRIPT_PATH = "opencvtext.py"

# Track running script
running_process = None  

@app.route('/run-opencv', methods=['POST'])
def run_opencv():
    global running_process
    try:
        running_process = subprocess.run(["python3", OPENCV_SCRIPT_PATH], capture_output=True, text=True)
        if running_process.stderr:
            return jsonify({"error": running_process.stderr}), 500
        return jsonify({"message": "Python script executed successfully!", "output": running_process.stdout})
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route('/stop', methods=['POST'])
def stop_script():
    global running_process
    if running_process is not None:
        running_process.terminate()
        running_process = None
        return jsonify({"message": "Stopped the running script"})
    return jsonify({"message": "No script is running"}), 400


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=5001)

```

### website/app/page.tsx

```typescript
import Hero from "@/components/Hero"
import ProblemSolution from "@/components/ProblemSolution"
import Features from "@/components/Features"
import HowItWorks from "@/components/HowItWorks"
import CTA from "@/components/CTA"

export default function Home() {
  return (
    <main className="min-h-screen bg-primary-900 text-primary-100">
      <Hero />
      <ProblemSolution />
      <Features />
      <HowItWorks />
      <CTA />
    </main>
  )
}


```

### website/app/layout.tsx

```typescript
"use client"

import type React from "react"

import { useEffect, useState } from "react"
import { Inter, Orbitron } from "next/font/google"
import "./globals.css"

const inter = Inter({ subsets: ["latin"] })
const orbitron = Orbitron({ subsets: ["latin"], variable: "--font-orbitron" })

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const [cursorPosition, setCursorPosition] = useState({ x: -100, y: -100 })

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      setCursorPosition({ x: e.clientX, y: e.clientY })
    }

    window.addEventListener("mousemove", handleMouseMove)

    return () => {
      window.removeEventListener("mousemove", handleMouseMove)
    }
  }, [])

  return (
    <html lang="en" className={`${orbitron.variable}`}>
      <body className={inter.className}>
        {children}
        <div className="cursor-trail">
          {[...Array(20)].map((_, i) => (
            <div
              key={i}
              style={{
                left: cursorPosition.x,
                top: cursorPosition.y,
                animationDelay: `${i * 0.05}s`,
              }}
            />
          ))}
        </div>
        <footer className="bg-primary-900 text-primary-100 py-4 text-center">
          <p className="cyberpunk-text text-sm">
            This is a project made at TreeHacks 2025 by Jeet Dekivadia, Anais Killian, Eric Wang, and Pratham Pilli
          </p>
        </footer>
      </body>
    </html>
  )
}


```

### website/app/camera/page.tsx

```typescript
import CameraVision from "@/components/CameraVision"

export default function CameraPage() {
  return <CameraVision />
} 

```

### website/app/api/analyze/route.ts

```typescript
import { NextResponse } from 'next/server'
import OpenAI from 'openai'

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY
})

export async function POST(request: Request) {
  try {
    const { image, mode } = await request.json()

    const prompt = mode === "describe" 
      ? "What is in this image? Give a two sentence summary."
      : "Only give the complete text for the following image."

    const visionResponse = await openai.chat.completions.create({
      model: "gpt-4-vision-preview",
      messages: [
        {
          role: "user",
          content: [
            { type: "text", text: prompt },
            {
              type: "image_url",
              image_url: { url: `data:image/jpeg;base64,${image}` },
            },
          ],
        },
      ],
    })

    const description = visionResponse.choices[0].message.content

    // Generate speech
    const speech = await openai.audio.speech.create({
      model: "tts-1",
      voice: "nova",
      input: description || "No description available",
    })

    // Convert speech to base64
    const audioBuffer = Buffer.from(await speech.arrayBuffer())
    const audioBase64 = audioBuffer.toString('base64')
    const audioUrl = `data:audio/mp3;base64,${audioBase64}`

    return NextResponse.json({ 
      description,
      audioUrl
    })

  } catch (error) {
    console.error('Analysis error:', error)
    return NextResponse.json(
      { error: 'Failed to analyze image' },
      { status: 500 }
    )
  }
} 

```

### website/app/api/process-image/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";
import { exec } from "child_process";
import fs from "fs";
import path from "path";

const allowedOrigins = [
  "https://www.vision-m8.com",  // ✅ Add your frontend domain
  "http://localhost:3000",      // ✅ Allow local development
];

// ✅ Helper function to set CORS headers
const setCorsHeaders = (origin: string) => ({
  "Access-Control-Allow-Origin": origin, 
  "Access-Control-Allow-Methods": "POST, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
});

export async function OPTIONS(req: NextRequest) {
  const origin = req.headers.get("origin") || "";

  if (!allowedOrigins.includes(origin)) {
    return NextResponse.json({ error: "CORS Not Allowed" }, { status: 403 });
  }

  return NextResponse.json({}, {
    status: 200,
    headers: setCorsHeaders(origin),
  });
}

export async function POST(req: NextRequest) {
  try {
    const origin = req.headers.get("origin") || "";

    if (!allowedOrigins.includes(origin)) {
      return NextResponse.json({ error: "CORS Not Allowed" }, { status: 403 });
    }

    const body = await req.json();
    if (!body.image) {
      return NextResponse.json({ error: "No image data received." }, { status: 400, headers: setCorsHeaders(origin) });
    }

    const IMAGE_PATH = "/tmp/captured.jpg";
    const OPENCV_SCRIPT_PATH = "/Users/anaiskillian/treehacks/opencvtext.py";

    // Convert base64 to image file
    const base64Image = body.image.replace(/^data:image\/jpeg;base64,/, "");
    fs.writeFileSync(IMAGE_PATH, base64Image, { encoding: "base64" });

    console.log(`✅ Image saved at ${IMAGE_PATH}. Running OpenCV script...`);

    return new Promise((resolve) => {
      exec(`python3 ${OPENCV_SCRIPT_PATH} ${IMAGE_PATH}`, (error, stdout, stderr) => {
        if (error) {
          console.error(`❌ Execution Error: ${error.message}`);
          return resolve(NextResponse.json({ error: `Processing error: ${error.message}` }, { status: 500, headers: setCorsHeaders(origin) }));
        }
        if (stderr) {
          console.error(`⚠️ Python Script Error: ${stderr}`);
          return resolve(NextResponse.json({ error: `Python error: ${stderr}` }, { status: 500, headers: setCorsHeaders(origin) }));
        }

        console.log(`📌 Python Script Output:\n${stdout}`);
        resolve(NextResponse.json(
          { message: stdout.trim() },
          {
            status: 200,
            headers: setCorsHeaders(origin),
          }
        ));
      });
    });
  } catch (error) {
    console.error("❌ Server Error:", error);
    return NextResponse.json({ error: `Failed to process image: ${error.message}` }, { status: 500 });
  }
}

```

### start.py

```python
# python file

```

### test_cameras.py

```python
import cv2

print("Scanning for available cameras...")

available_cameras = []

for i in range(2):
    cap = cv2.VideoCapture(i)
    if cap.isOpened():
        available_cameras.append(i)
        cap.release()
print(available_cameras)

# Prioritize external cameras (assuming index 1+ are external)
selected_camera = None
if len(available_cameras) == 2:
    selected_camera = 1
else:
    selected_camera = 0

print(f"Using Camera Index: {selected_camera}")
camera = cv2.VideoCapture(selected_camera)

print(f"Available Cameras: {available_cameras}")

if not available_cameras:
    print("No cameras detected. Try connecting an external webcam.")
else:
    print(f"First available camera index: {available_cameras[0]}")

```

[71 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]