# Project export: SnapLens

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: UC Berkeley AI Hackathon 2025
- Tagline: Frictionless AI-powered translation for social good
- Devpost: https://devpost.com/software/smartglasses
- GitHub: https://github.com/tmabdull/snap-lens
- Video: https://www.youtube.com/embed/h9CoXwnKWus?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Taha Abdullah (3 commits), Youssef Qteishat (1 commits)

## Devpost submission (written by the team)

### Overview

The Team Taha Abdullah, Computer Science @ UC Davis '24 [in] Yahya Qteishat, Electrical Engineering & Computer Sciences @ UC Berkeley '24 [in] Youssef Qteishat, Computer Science @ UC Davis '24 [in] Nour Zahzah, Mechanical Engineering @ UC Berkeley '27 [in]

### Inspiration

Struggles of immigrants and refugees navigating language barriers Hands-off user experiences offered by commercial smart glasses (Meta glasses, Google Glass, AR, etc.)

### What it does

Glasses capture a live stream of the user's POV via CAM module attached to glasses frame Snapshot triggered on dev GUI ESP32 sends POV image to Flask backend hosted on Render (mocked in demo video bc of SSL cert issues) Gemini interprets the language and translates into English, and vice versa Translated text is displayed in server console and LCD display (WIP)

### How we built it

Hardware: Nour custom modeled and printed the following components: glasses frame with integrated camera housing, LCD display mount Middleware: Youssef and Taha wrote the Python (Flask) middleware server, hosted on Render, that integrates with Gemini for multimodal translation of uploaded image Electronics: Yahya integrated the ESP32-CAM micro-controller with a 2 MP camera and the FTDI USB-to-TTL adapter Programmed the ESP32 to capture the image through the camera and pass that as the body of a POST request to the api endpoint. (mocked in the demo bc running into cert issues on ESP32)

### Challenges we ran into

Getting a snap fit when attaching the camera module to its holder on the frame Handling latency when making API calls from ESP32 to Flask Server ESP32-CAM doesn't have a usb chip. First tried using the usb chip from a second USB-32 but we didn't have a micro-usb data transfer cable. so we switched to the FTDI approach. HTML on ESP32 is a zipped archive thats cut up into a C byte array, very cumbersome to change Displaying languages with non-Latin and especially cursive scripts

### Accomplishments we're proud of

The quality of the hardware! Nour did an amazing amazing job. Establishing a wifi connection between the ESP32 and Flask backend Getting live video feed from CAM module Hosting Flask servers on Render

### What we learned

Prompt Engineering Integrating an ESP32-CAM micro-controller with an FTDI USB-TTL Adapter Designing CAD models in Fusion 360 3D printing models Refinements Triggering image capture and translation from button on the side instead of from a dev GUI on laptop Finishing the integration of our LCD screen so we can display the output there isntead of in server console

### What's next

Integrating a GPS module to get the location to account for differences between standard and colloquial language, updating the user prompt accordingly Utilizing a microphone, speaker, and STT/TTS provider to translate a conversation in real-time Bigger display and additional buttons for custom user configuration

## README (from the GitHub repository)

# SnapLens
Frictionless Translation for social good

## The Team
Taha Abdullah, Computer Science @ UC Davis '24 [[in]](https://www.linkedin.com/in/taha-m-abdullah/) <br>
Yahya Qteishat, Electrical Engineering & Computer Sciences @ UC Berkeley '24 [[in]](https://www.linkedin.com/in/yahyaq/) <br>
Youssef Qteishat, Computer Science @ UC Davis '24 [[in]](https://www.linkedin.com/in/youssef-qteishat/) <br>
Nour Zahzah, Mechanical Engineering @ UC Berkeley '27 [[in]](https://www.linkedin.com/in/nourzahzah/) <br>

## Inspiration
- Struggles of immigrants and refugees navigating language barriers
- Hands-off user experiences offered by commercial smart glasses (Meta glasses, Google Glass, AR, etc.)

## What it does

- Glasses capture a live stream of the user's POV via CAM module attached to glasses frame
- Snapshot triggered on dev GUI
- ESP32 sends POV image to Flask backend hosted on Render (mocked in demo video bc of SSL cert issues)
- Gemini interprets the language and translates into English, and vice versa
- Translated text is displayed in server console and LCD display (WIP)

## How we built it 

Hardware:
- Nour custom modeled and printed the following components: glasses frame with integrated camera housing, LCD display mount

Middleware:
- Youssef and Taha wrote the Python (Flask) middleware server, hosted on Render, that integrates with Gemini for multimodal translation of uploaded image

Electronics:
- Yahya integrated the ESP32-CAM micro-controller with a 2 MP camera and the FTDI USB-to-TTL adapter
- Programmed the ESP32 to capture the image through the camera and pass that as the body of a POST request to the api endpoint. (mocked in the demo bc running into cert issues on ESP32)


## Challenges we ran into

- Getting a snap fit when attaching the camera module to its holder on the frame
- Handling latency when making API calls from ESP32 to Flask Server
- ESP32-CAM doesn't have a usb chip. First tried using the usb chip from a second USB-32 but we didn't have a micro-usb data transfer cable. so we switched to the FTDI approach.
- HTML on ESP32 is a zipped archive thats cut up into a C byte array, very cumbersome to change
- Displaying languages with non-Latin and especially cursive scripts

## Accomplishments that we're proud of

- The quality of the hardware! Nour did an amazing amazing job.
- Establishing a wifi connection between the ESP32 and Flask backend
- Getting live video feed from CAM module
- Hosting Flask servers on Render

## What we learned

- Prompt Engineering
- Integrating an ESP32-CAM micro-controller with an FTDI USB-TTL Adapter
- Designing CAD models in Fusion 360
- 3D printing models


## Refinements
- Triggering image capture and translation from button on the side instead of from a dev GUI on laptop
- Finishing the integration of our LCD screen so we can display the output there isntead of in server console

## What's next for SnapLens
- Integrating a GPS module to get the location to account for differences between standard and colloquial language, updating the user prompt accordingly
- Utilizing a microphone, speaker, and STT/TTS provider to translate a conversation in real-time
- Bigger display and additional buttons for custom user configuration


## Detected evidence (automated analysis)

Indexed codebase: 5 recognized source files, 184 KB.
- C (language) — detected in the code
- C++ (language) — detected in the code
- Flask (technology) — detected in the code
- Python (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (10 of 10)

```
.gitignore
app.py
firmware/app_httpd.cpp
firmware/camera_index.h
firmware/camera_pins.h
firmware/CameraWebServer.ino
firmware/ci.json
firmware/partitions.csv
README.md
requirements.txt
```

### Dependencies

- requirements.txt: arabic-reshaper, Flask, google-genai, gunicorn, Pillow, python-dotenv

### Recent commits (newest first)

- Parsing raw jpg data instead of pulling from img path
- Revert "implemented proxy server"
- implemented proxy server
- Firmware for ESP32
- Update README.md
- Update README.md
- Gunicorn server + Error handling
- Translation backend done
- Initial commit

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

### requirements.txt

```
Flask
python-dotenv
google-genai
Pillow
arabic-reshaper
gunicorn

```

### app.py

```python
from flask import Flask, request, jsonify
from dotenv import load_dotenv
from google import genai
from PIL import Image, UnidentifiedImageError
import io
import arabic_reshaper

load_dotenv()
app = Flask(__name__)

# Initialize Gemini client (API key from env)
client = genai.Client()

def handle_translation_response(response, language):
    """
    Helper to process the Gemini response text based on language.
    """
    
    text = getattr(response, 'text', None)
    if not text or not text.strip():
        return "No translation result."
    
    right_to_left_languages = {
        "arabic",
        "aramaic",
        "azeri", 
        "dhivehi", "maldivian",
        "hebrew",
        "kurdish", "sorani",
        "persian", "farsi",
        "urdu"
    }
    
    if (language.lower() in right_to_left_languages):
        text = text[::-1]

    if language.lower() == "arabic":
        text = arabic_reshaper.reshape(text)

    return text

@app.route('/translate', methods=['POST'])
def translate():
    language = request.form.get('language', 'English')

    prompt = (
        f"""
        This is an image (jpg, png, jpeg, etc) of some text. 
        Extract and translate the text to ${language}. 
        Don't include any extra words in your response -- only the 
        translation. 
        Account for colloqiual dialects in addition to the standard form of 
        the input language.
        If the text is formatted in a specific way with a series of items 
        (e.g. a foreign-language menu), translate each item and format the 
        response line by line so that the client can choose to print just 
        one item / line.
        """
    )

    contents = [prompt]

    # Try to get image from form (multipart/form-data)
    image_file = request.files.get('image')
    if image_file:
        try:
            img = Image.open(io.BytesIO(image_file.read()))
        except UnidentifiedImageError:
            return jsonify({'result': 'Invalid image file.'}), 400
    # If not present, try to get raw data (application/octet-stream or image/jpeg)
    elif request.data:
        try:
            img = Image.open(io.BytesIO(request.data))
        except UnidentifiedImageError:
            return jsonify({'result': 'Invalid image data.'}), 400
    else:
        return jsonify({'result': 'No image file provided.'}), 400

    contents.append(img)

    try:
        response = client.models.generate_content(
            model='gemini-2.0-flash',
            contents=contents
        )

        translated = handle_translation_response(response, language)
        print("Translated Text:\n", translated)

        return jsonify({'result': translated})
    except Exception:
        return jsonify({'result': 'Translation failed.'}), 500

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

```

### firmware/camera_pins.h

```c

#if defined(CAMERA_MODEL_WROVER_KIT)
#define PWDN_GPIO_NUM  -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM  21
#define SIOD_GPIO_NUM  26
#define SIOC_GPIO_NUM  27

#define Y9_GPIO_NUM    35
#define Y8_GPIO_NUM    34
#define Y7_GPIO_NUM    39
#define Y6_GPIO_NUM    36
#define Y5_GPIO_NUM    19
#define Y4_GPIO_NUM    18
#define Y3_GPIO_NUM    5
#define Y2_GPIO_NUM    4
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM  23
#define PCLK_GPIO_NUM  22

#elif defined(CAMERA_MODEL_ESP_EYE)
#define PWDN_GPIO_NUM  -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM  4
#define SIOD_GPIO_NUM  18
#define SIOC_GPIO_NUM  23

#define Y9_GPIO_NUM    36
#define Y8_GPIO_NUM    37
#define Y7_GPIO_NUM    38
#define Y6_GPIO_NUM    39
#define Y5_GPIO_NUM    35
#define Y4_GPIO_NUM    14
#define Y3_GPIO_NUM    13
#define Y2_GPIO_NUM    34
#define VSYNC_GPIO_NUM 5
#define HREF_GPIO_NUM  27
#define PCLK_GPIO_NUM  25

#define LED_GPIO_NUM 22

#elif defined(CAMERA_MODEL_M5STACK_PSRAM)
#define PWDN_GPIO_NUM  -1
#define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM  27
#define SIOD_GPIO_NUM  25
#define SIOC_GPIO_NUM  23

#define Y9_GPIO_NUM    19
#define Y8_GPIO_NUM    36
#define Y7_GPIO_NUM    18
#define Y6_GPIO_NUM    39
#define Y5_GPIO_NUM    5
#define Y4_GPIO_NUM    34
#define Y3_GPIO_NUM    35
#define Y2_GPIO_NUM    32
#define VSYNC_GPIO_NUM 22
#define HREF_GPIO_NUM  26
#define PCLK_GPIO_NUM  21

#elif defined(CAMERA_MODEL_M5STACK_V2_PSRAM)
#define PWDN_GPIO_NUM  -1
#define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM  27
#define SIOD_GPIO_NUM  22
#define SIOC_GPIO_NUM  23

#define Y9_GPIO_NUM    19
#define Y8_GPIO_NUM    36
#define Y7_GPIO_NUM    18
#define Y6_GPIO_NUM    39
#define Y5_GPIO_NUM    5
#define Y4_GPIO_NUM    34
#define Y3_GPIO_NUM    35
#define Y2_GPIO_NUM    32
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM  26
#define PCLK_GPIO_NUM  21

#elif defined(CAMERA_MODEL_M5STACK_WIDE)
#define PWDN_GPIO_NUM  -1
#define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM  27
#define SIOD_GPIO_NUM  22
#define SIOC_GPIO_NUM  23

#define Y9_GPIO_NUM    19
#define Y8_GPIO_NUM    36
#define Y7_GPIO_NUM    18
#define Y6_GPIO_NUM    39
#define Y5_GPIO_NUM    5
#define Y4_GPIO_NUM    34
#define Y3_GPIO_NUM    35
#define Y2_GPIO_NUM    32
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM  26
#define PCLK_GPIO_NUM  21

#define LED_GPIO_NUM 2

#elif defined(CAMERA_MODEL_M5STACK_ESP32CAM)
#define PWDN_GPIO_NUM  -1
#define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM  27
#define SIOD_GPIO_NUM  25
#define SIOC_GPIO_NUM  23

#define Y9_GPIO_NUM    19
#define Y8_GPIO_NUM    36
#define Y7_GPIO_NUM    18
#define Y6_GPIO_NUM    39
#define Y5_GPIO_NUM    5
#define Y4_GPIO_NUM    34
#define Y3_GPIO_NUM    35
#define Y2_GPIO_NUM    17
#define VSYNC_GPIO_NUM 22
#define HREF_GPIO_NUM  26
#define PCLK_GPIO_NUM  21

#elif defined(CAMERA_MODEL_M5STACK_UNITCAM)
#define PWDN_GPIO_NUM  -1
#define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM  27
#define SIOD_GPIO_NUM  25
#define SIOC_GPIO_NUM  23

#define Y9_GPIO_NUM    19
#define Y8_GPIO_NUM    36
#define Y7_GPIO_NUM    18
#define Y6_GPIO_NUM    39
#define Y5_GPIO_NUM    5
#define Y4_GPIO_NUM    34
#define Y3_GPIO_NUM    35
#define Y2_GPIO_NUM    32
#define VSYNC_GPIO_NUM 22
#define HREF_GPIO_NUM  26
#define PCLK_GPIO_NUM  21

#elif defined(CAMERA_MODEL_M5STACK_CAMS3_UNIT)
#define PWDN_GPIO_NUM  -1
#define RESET_GPIO_NUM 21
#define XCLK_GPIO_NUM  11
#define SIOD_GPIO_NUM  17
#define SIOC_GPIO_NUM  41

#define Y9_GPIO_NUM    13
#define Y8_GPIO_NUM    4
#define Y7_GPIO_NUM    10
#define Y6_GPIO_NUM    5
#define Y5_GPIO_NUM    7
#define Y4_GPIO_NUM    16
#define Y3_GPIO_NUM    15
#define Y2_GPIO_NUM    6
#define VSYNC_GPIO_NUM 42
#define HREF_GPIO_NUM  18
#define PCLK_GPIO_NUM  12

#define LED_GPIO_NUM 14

#elif defined(CAMERA_MODEL_AI_THINKER)
#define PWDN_GPIO_NUM  32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM  0
#define SIOD_GPIO_NUM  26
#define SIOC_GPIO_NUM  27

#define Y9_GPIO_NUM    35
#define Y8_GPIO_NUM    34
#define Y7_GPIO_NUM    39
#define Y6_GPIO_NUM    36
#define Y5_GPIO_NUM    21
#define Y4_GPIO_NUM    19
#define Y3_GPIO_NUM    18
#define Y2_GPIO_NUM    5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM  23
#define PCLK_GPIO_NUM  22

// 4 for flash led or 33 for normal led
#define LED_GPIO_NUM   4

#elif defined(CAMERA_MODEL_TTGO_T_JOURNAL)
#define PWDN_GPIO_NUM  0
#define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM  27
#define SIOD_GPIO_NUM  25
#define SIOC_GPIO_NUM  23

#define Y9_GPIO_NUM    19
#define Y8_GPIO_NUM    36
#define Y7_GPIO_NUM    18
#define Y6_GPIO_NUM    39
#define Y5_GPIO_NUM    5
#define Y4_GPIO_NUM    34
#define Y3_GPIO_NUM    35
#define Y2_GPIO_NUM    17
#define VSYNC_GPIO_NUM 22
#define HREF_GPIO_NUM  26
#define PCLK_GPIO_NUM  21

#elif defined(CAMERA_MODEL_XIAO_ESP32S3)
#define PWDN_GPIO_NUM  -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM  10
#define SIOD_GPIO_NUM  40
#define SIOC_GPIO_NUM  39

#define Y9_GPIO_NUM    48
#define Y8_GPIO_NUM    11
#define Y7_GPIO_NUM    12
#define Y6_GPIO_NUM    14
#define Y5_GPIO_NUM    16
#define Y4_GPIO_NUM    18
#define Y3_GPIO_NUM    17
#define Y2_GPIO_NUM    15
#define VSYNC_GPIO_NUM 38
#define HREF_GPIO_NUM  47
#define PCLK_GPIO_NUM  13

#elif defined(CAMERA_MODEL_ESP32_CAM_BOARD)
// The 18 pin header on the board has Y5 and Y3 swapped
#define USE_BOARD_HEADER 0
#define PWDN_GPIO_NUM    32
#define RESET_GPIO_NUM   33
#define XCLK_GPIO_NUM    4
#define SIOD_GPIO_NUM    18
#define SIOC_GPIO_NUM    23

#define Y9_GPIO_NUM 36
#define Y8_GPIO_NUM 19
#define Y7_GPIO_NUM 21
#define Y6_GPIO_NUM 39
#if USE_BOARD_HEADER
#define Y5_GPIO_NUM 13
#else
#define Y5_GPIO_NUM 35
#endif
#define Y4_GPIO_NUM 14
#if USE_BOARD_HEADER
#define Y3_GPIO_NUM 35
#else
#define Y3_GPIO_NUM 13
#endif
#define Y2_GPIO_NUM    34
#define VSYNC_GPIO_NUM 5
#define HREF_GPIO_NUM  27
#define PCLK_GPIO_NUM  25

#elif defined(CAMERA_MODEL_ESP32S3_CAM_LCD)
#define PWDN_GPIO_NUM  -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM  40
#define SIOD_GPIO_NUM  17
#define SIOC_GPIO_NUM  1
[truncated — 1925 more characters]
```

### firmware/app_httpd.cpp

```c++
#include "Arduino.h"
#include "esp_http_server.h"
#include "esp_timer.h"
#include "esp_camera.h"
#include "img_converters.h"
#include "fb_gfx.h"
#include "esp32-hal-ledc.h"
#include "sdkconfig.h"
#include "camera_index.h"
#include "esp_http_client.h" // Make sure this is included at the top of your file


#if defined(ARDUINO_ARCH_ESP32) && defined(CONFIG_ARDUHAL_ESP_LOG)
#include "esp32-hal-log.h"
#endif

// Enable LED FLASH setting
#define CONFIG_LED_ILLUMINATOR_ENABLED 1

// LED FLASH setup
#if CONFIG_LED_ILLUMINATOR_ENABLED

#define LED_LEDC_GPIO            22  //configure LED pin
#define CONFIG_LED_MAX_INTENSITY 255

int led_duty = 0;
bool isStreaming = false;

#endif

typedef struct {
  httpd_req_t *req;
  size_t len;
} jpg_chunking_t;

#define PART_BOUNDARY "123456789000000000000987654321"
static const char *_STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char *_STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char *_STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\nX-Timestamp: %d.%06d\r\n\r\n";

httpd_handle_t stream_httpd = NULL;
httpd_handle_t camera_httpd = NULL;

typedef struct {
  size_t size;   //number of values used for filtering
  size_t index;  //current value index
  size_t count;  //value count
  int sum;
  int *values;  //array to be filled with values
} ra_filter_t;

static ra_filter_t ra_filter;

static ra_filter_t *ra_filter_init(ra_filter_t *filter, size_t sample_size) {
  memset(filter, 0, sizeof(ra_filter_t));

  filter->values = (int *)malloc(sample_size * sizeof(int));
  if (!filter->values) {
    return NULL;
  }
  memset(filter->values, 0, sample_size * sizeof(int));

  filter->size = sample_size;
  return filter;
}

#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO
static int ra_filter_run(ra_filter_t *filter, int value) {
  if (!filter->values) {
    return value;
  }
  filter->sum -= filter->values[filter->index];
  filter->values[filter->index] = value;
  filter->sum += filter->values[filter->index];
  filter->index++;
  filter->index = filter->index % filter->size;
  if (filter->count < filter->size) {
    filter->count++;
  }
  return filter->sum / filter->count;
}
#endif

#if CONFIG_LED_ILLUMINATOR_ENABLED
void enable_led(bool en) {  // Turn LED On or Off
  int duty = en ? led_duty : 0;
  if (en && isStreaming && (led_duty > CONFIG_LED_MAX_INTENSITY)) {
    duty = CONFIG_LED_MAX_INTENSITY;
  }
  ledcWrite(LED_LEDC_GPIO, duty);
  //ledc_set_duty(CONFIG_LED_LEDC_SPEED_MODE, CONFIG_LED_LEDC_CHANNEL, duty);
  //ledc_update_duty(CONFIG_LED_LEDC_SPEED_MODE, CONFIG_LED_LEDC_CHANNEL);
  log_i("Set LED intensity to %d", duty);
}
#endif

static esp_err_t bmp_handler(httpd_req_t *req) {
  camera_fb_t *fb = NULL;
  esp_err_t res = ESP_OK;
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO
  uint64_t fr_start = esp_timer_get_time();
#endif
  fb = esp_camera_fb_get();
  if (!fb) {
    log_e("Camera capture failed");
    httpd_resp_send_500(req);
    return ESP_FAIL;
  }

  httpd_resp_set_type(req, "image/x-windows-bmp");
  httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.bmp");
  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");

  char ts[32];
  snprintf(ts, 32, "%lld.%06ld", fb->timestamp.tv_sec, fb->timestamp.tv_usec);
  httpd_resp_set_hdr(req, "X-Timestamp", (const char *)ts);

  uint8_t *buf = NULL;
  size_t buf_len = 0;
  bool converted = frame2bmp(fb, &buf, &buf_len);
  esp_camera_fb_return(fb);
  if (!converted) {
    log_e("BMP Conversion failed");
    httpd_resp_send_500(req);
    return ESP_FAIL;
  }
  res = httpd_resp_send(req, (const char *)buf, buf_len);
  free(buf);
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO
  uint64_t fr_end = esp_timer_get_time();
#endif
  log_i("BMP: %llums, %uB", (uint64_t)((fr_end - fr_start) / 1000), buf_len);
  return res;
}

static size_t jpg_encode_stream(void *arg, size_t index, const void *data, size_t len) {
  jpg_chunking_t *j = (jpg_chunking_t *)arg;
  if (!index) {
    j->len = 0;
  }
  if (httpd_resp_send_chunk(j->req, (const char *)data, len) != ESP_OK) {
    return 0;
  }
  j->len += len;
  return len;
}


static esp_err_t capture_handler(httpd_req_t *req) {
  camera_fb_t *fb = NULL;
  esp_err_t res = ESP_OK;
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO
  int64_t fr_start = esp_timer_get_time();
#endif

#if CONFIG_LED_ILLUMINATOR_ENABLED
  enable_led(true);
  vTaskDelay(150 / portTICK_PERIOD_MS);
  fb = esp_camera_fb_get();
  enable_led(false);
#else
  fb = esp_camera_fb_get();
#endif

  if (!fb) {
    log_e("Camera capture failed");
    httpd_resp_send_500(req);
    return ESP_FAIL;
  }

  // --- SnapLens Forwarding ---
esp_http_client_config_t config = {};
config.url = "https://snap-lens.onrender.com/translate";
config.method = HTTP_METHOD_POST;
config.timeout_ms = 10000;
config.cert_pem = NULL;
config.skip_cert_common_name_check = true;

  esp_http_client_handle_t client = esp_http_client_init(&config);
  esp_http_client_set_post_field(client, (const char *)fb->buf, fb->len);
  esp_http_client_set_header(client, "Content-Type", "image/jpeg");

  esp_err_t post_err = esp_http_client_perform(client);
  if (post_err == ESP_OK) {
    int status = esp_http_client_get_status_code(client);
    log_i("SnapLens HTTP POST Status = %d", status);
  } else {
    log_e("SnapLens POST failed: %s", esp_err_to_name(post_err));
  }
  esp_http_client_cleanup(client);
  // --- End SnapLens Forwarding ---

  httpd_resp_set_type(req, "image/jpeg");
  httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");

  char ts[32];
  snprintf(ts, 32, "%lld.%06ld", fb->timestamp.tv_sec, fb->timestamp.tv_usec);
  httpd_resp_set_hdr(req, "X-Timestamp", (const char *)ts);

  size_t fb_len = 0;
  if (fb->format == PIXFORMAT_JPEG) {
    fb_len = fb->len;
    res = httpd_resp_send(req, (const char *)fb->buf, fb->len);
  } else {
    jpg_chunking_t jchunk = {req
[truncated — 21056 more characters]
```