diff --git a/docs/edge_ai/chat_with_llm/1_chat_request.md b/docs/edge_ai/chat_with_llm/1_chat_request.md
new file mode 100644
index 0000000..946bd2b
--- /dev/null
+++ b/docs/edge_ai/chat_with_llm/1_chat_request.md
@@ -0,0 +1,11 @@
+# Exercise 01: Simple Chat Request
+
+During the workshop, we will be using Gemma 3 1B as our language model. The models are deployed using llama.cpp, which exposes an OpenAI-compatible API on port 8080.
+
+We have defined the necessary structs to interact with the model API.
+
+A chat request consists of the model name, an array of messages and optionally tools and response format.
+
+A message consists of the role (user, assistant, system) and the content.
+
+Complete the TODO 1 to implement the chat interaction logic.
diff --git a/docs/edge_ai/chat_with_llm/2_RAG.md b/docs/edge_ai/chat_with_llm/2_RAG.md
new file mode 100644
index 0000000..9b19226
--- /dev/null
+++ b/docs/edge_ai/chat_with_llm/2_RAG.md
@@ -0,0 +1,21 @@
+# Exercise 02: Retrieval-Augmented Generation (RAG)
+
+In this section, we will implement a RAG system that combines the language model with a document retrieval system.
+
+The embeddings model is also deployed using llama.cpp and exposes a slightly different API on port 8081.
+
+A RAG system is implemented as follows:
+1. Calculate embeddings on documents inside the knowledge base.
+2. Calculate the embedding of the user query.
+3. Store the document embeddings in a vector database (for simplicity, we will use an in-memory vector store).
+4. Get the most similar documents from the knowledge base using the query embedding, with a metric such as cosine similarity.
+5. Pass the retrieved documents as context to the language model and generate a response.
+
+Here are some examples that you can add to the database and ask questions about them:
+
+1. The secret code to access the project is 'quantum_leap_42'.
+2. Alice is the lead engineer for the new 'Orion' feature.
+3. The project deadline has been moved to next Friday.
+
+
+For this exercise, solve TODO 2 to implement the document retrieval logic.
\ No newline at end of file
diff --git a/docs/edge_ai/chat_with_llm/3_structured_outputs.md b/docs/edge_ai/chat_with_llm/3_structured_outputs.md
new file mode 100644
index 0000000..96699c1
--- /dev/null
+++ b/docs/edge_ai/chat_with_llm/3_structured_outputs.md
@@ -0,0 +1,33 @@
+# Exercise 03: Structured Outputs
+
+Structured outputs are a way to format the model's responses, such that they can be parsed by other systems. Information extraction is a common use case for structured outputs, where the model is asked to extract specific information from a given text.
+
+Structured outputs are defined by a JSON Schema that describes the structure of the expected output.
+
+The schema is passed in the API request in the `response_format` field. An example schema for extracting the city from a given text looks like this:
+
+```json
+{
+ "type": "json_schema",
+ "json_schema": {
+ "name": "example_schema",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "city": {
+ "type": "string",
+ }
+ }
+ }
+ }
+}
+```
+
+In the background, llama.cpp parses this schema and creates a GBNF grammar that guides the model's response generation. More information in the [llama.cpp documentation](https://github.com/ggml-org/llama.cpp/tree/master/grammars).
+
+Keep in mind that using structured outputs can degrade the performance of LLMs, as shown by [Tam et al.](https://arxiv.org/abs/2408.02442)
+
+For this exercise, solve TODO 3 in order to extract the name, city and age of user from a given text.
+
+Here's an example prompt you can use to test your implementation:
+```John is a 25 years old software engineer living in New York.```
\ No newline at end of file
diff --git a/docs/edge_ai/chat_with_llm/4_tool_calling.md b/docs/edge_ai/chat_with_llm/4_tool_calling.md
new file mode 100644
index 0000000..9abe02f
--- /dev/null
+++ b/docs/edge_ai/chat_with_llm/4_tool_calling.md
@@ -0,0 +1,48 @@
+# Exercise 04: Tool Calling
+
+LLMs are very good at generating text, but they are not very good at performing tasks that require letter-perfect accuracy, such as calculations. Try asking the model to calculate the sum of two numbers over 10000, and you will see that it often makes mistakes.
+These weaknesses can be mitigated by using tools, which are functions that can be called by the model to perform specific tasks.
+
+Tool calling is a technique that builds on structured outputs. It allows the user to define functions that can be called by the language model and executed during the conversation.
+
+Tool calling also uses structured outputs under the hood, as defining a tool is done using a JSON Schema.
+
+A tool for calculating the sum of two numbers might look like this:
+
+```json
+[
+ {
+ "type": "function",
+ "function": {
+ "name": "add",
+ "description": "Add two numbers.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "num1": {
+ "type": "integer",
+ "description": "The first number."
+ },
+ "num2": {
+ "type": "integer",
+ "description": "The second number."
+ },
+ },
+ "required": [
+ "num1",
+ "num2",
+ ]
+ }
+ }
+ }
+]
+```
+
+In this exercise, solve TODO 4 to implement a tool that calculates mathematical operations (add, subtract, multiply, divide) between two numbers.
+
+
+### 5. Extra
+Congratulations, you implemented a basic agent! If you want to extend it, you can try these other options:
+1. Replace the in-memory RAG implementation with a proper vector database (e.g. Qdrant).
+2. Add more tools for the agent to use - e.g. a web search tool, a bash file finding tool, etc.
+3. Try to extract data from other types of documents (e.g. logs) or use other data types of [JSON Schema](https://json-schema.org/understanding-json-schema/reference/type) (e.g. arrays, enums).
\ No newline at end of file
diff --git a/docs/edge_ai/chat_with_llm/index.md b/docs/edge_ai/chat_with_llm/index.md
new file mode 100644
index 0000000..3b7b1c3
--- /dev/null
+++ b/docs/edge_ai/chat_with_llm/index.md
@@ -0,0 +1,48 @@
+---
+position: 3
+---
+# Chat With LLM
+The Chat with LLM workshop will guide you through four essential techniques used for interacting with LLMs:
+* Simple chat request
+* RAG
+* Structured outputs
+* Tool calling
+
+The application runs in the CLI and expects a user prompt. The user then selects one of the available techniques to interact with the LLM. The model will respond. The messages inside the conversation are stored in memory. The application will keep running until the user types "exit".
+
+## Slides
+
+
+
+download the slides.
+
+## Quick Start
+
+### Prerequisites
+The following are already installed on the Raspberry Pi:
+* [Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html)
+* [Llama.cpp](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md#cpu-build)
+
+### Deploying the models
+```bash
+llama-server --embeddings --hf-repo second-state/All-MiniLM-L6-v2-Embedding-GGUF --hf-file all-MiniLM-L6-v2-ggml-model-f16.gguf --port 8081 # embeddings model available on localhost:8081
+llama-server --jinja --hf-repo MaziyarPanahi/gemma-3-1b-it-GGUF --hf-file gemma-3-1b-it.Q5_K_M.gguf # llm available on localhost:8080
+```
+
+## Repository
+
+Please clone the repository.
+
+```bash
+git clone https://github.com/Wyliodrin/edge-ai-chat-with-llm.git
+cd edge-ai-chat-with-llm
+```
+
+## Workshop
+You will be working inside the `workshop.rs` file. The full implementation is available in the `full_demo.rs` file, in case you get stuck.
+In order to run the workshop, execute:
+```bash
+RUST_LOG=info cargo run --bin workshop
+```
\ No newline at end of file
diff --git a/docs/edge_ai/face_authentication/1_image_processing.md b/docs/edge_ai/face_authentication/1_image_processing.md
new file mode 100644
index 0000000..a6ced24
--- /dev/null
+++ b/docs/edge_ai/face_authentication/1_image_processing.md
@@ -0,0 +1,119 @@
+# Exercise 01. Image Processing and Normalization
+
+## Overview
+
+This exercise teaches you how to properly preprocess images for computer vision models, specifically focusing on ImageNet normalization. You'll implement the `image_with_std_mean` function that transforms raw images into model-ready tensors.
+
+## Understanding Tensors and Image Processing
+
+### What is a Tensor?
+
+A **tensor** is a multi-dimensional array that serves as the fundamental data structure in machine learning. Think of it as:
+
+- **1D tensor**: A vector (like `[1, 2, 3, 4]`)
+- **2D tensor**: A matrix (like a spreadsheet with rows and columns)
+- **3D tensor**: A cube of data (like our image with height × width × channels)
+- **4D tensor**: A batch of 3D tensors (multiple images)
+
+For images, we use **3D tensors** with dimensions:
+- **Channels**: Color information (3 for RGB: Red, Green, Blue)
+- **Height**: Number of pixel rows
+- **Width**: Number of pixel columns
+
+ConvNeXt expects tensors in **"channels-first"** format: `(channels, height, width)` rather than `(height, width, channels)`.
+
+### What is Normalization?
+
+**Normalization** transforms data to have consistent statistical properties. For images, we perform two types:
+
+1. **Scale Normalization**: Convert pixel values from `[0-255]` to `[0-1]` by dividing by 255
+2. **Statistical Normalization**: Transform to have zero mean and unit variance using: `(value - mean) / standard_deviation`
+
+### Why Use Mean and Standard Deviation?
+
+The **ImageNet mean and standard deviation** values aren't arbitrary - they're computed from millions of natural images:
+
+- **Mean `[0.485, 0.456, 0.406]`**: Average pixel values across Red, Green, Blue channels
+- **Std `[0.229, 0.224, 0.225]`**: Standard deviation for each channel
+
+**Why these specific values matter for ConvNeXt:**
+
+1. **Distribution Matching**: ConvNeXt was trained on ImageNet data with these exact statistics. Using different values would be like speaking a different language to the model.
+
+2. **Zero-Centered Data**: Subtracting the mean centers pixel values around zero, which helps neural networks learn faster and more stably.
+
+3. **Unit Variance**: Dividing by standard deviation ensures all channels contribute equally to learning, preventing one color channel from dominating.
+
+4. **Gradient Flow**: Normalized inputs lead to better gradient flow during training, preventing vanishing or exploding gradients.
+
+## Why ImageNet Normalization is Critical for ConvNeXt
+
+**ImageNet normalization is essential for four key reasons:**
+
+1. **Neural Network Stability**: Raw pixel values (0-255) are too large and cause training instability. Normalizing to smaller ranges helps gradients flow properly during backpropagation.
+
+2. **Pre-trained Model Compatibility**: ConvNeXt models are trained on ImageNet-normalized data. Using the same normalization ensures your input matches what the model expects - like using the same units of measurement.
+
+3. **Feature Standardization**: Different color channels have different statistical distributions in natural images. Per-channel normalization gives equal importance to all color information.
+
+4. **Mathematical Optimization**: The normalization formula `(pixel/255 - mean) / std` transforms arbitrary pixel values into a standardized range that neural networks can process efficiently.
+
+**Without proper normalization, ConvNeXt will produce poor results** because the input distribution doesn't match its training data - imagine trying to use a thermometer calibrated in Celsius to read Fahrenheit temperatures!
+
+## Your Task
+
+Implement the `image_with_std_mean` function that:
+
+1. **Resizes** the input image to the specified resolution using Triangle filtering
+2. **Converts** to RGB8 format to ensure consistent color channels
+3. **Creates** a tensor with shape `(3, height, width)` - channels first format
+4. **Normalizes** pixel values from [0-255] to [0-1] range
+5. **Applies** ImageNet standardization: `(pixel/255 - mean) / std`
+
+## Implementation Steps
+
+```rust
+pub fn image_with_std_mean(
+ img: &DynamicImage,
+ res: usize,
+ mean: &[f32; 3],
+ std: &[f32; 3],
+) -> Result
+```
+
+### Implementation Approach:
+
+1. **Resize Image**: Use appropriate image resizing methods
+2. **Convert Format**: Ensure consistent color channel format
+3. **Extract Data**: Get raw pixel data from the image
+4. **Create Tensor**: Build tensor with correct shape and dimensions
+5. **Normalize**: Apply scaling and ImageNet standardization
+
+### Key Operations Needed:
+- Image resizing and format conversion
+- Tensor creation from raw data
+- Dimension reordering (channels-first format)
+- Mathematical operations for normalization
+- Broadcasting for per-channel operations
+
+**Hint**: Check the CHEATSHEET.md for specific API calls and tensor operations.
+
+## Testing
+
+The test verifies that:
+- Tensor values are in the expected normalized range (approximately [-2.5, 2.5])
+- Values are actually normalized (not just zeros or ones)
+- The transformation follows ImageNet standards
+
+Run the test with:
+```bash
+cargo test
+```
+
+## Expected Output Format
+
+- **Input**: DynamicImage of any size
+- **Output**: Tensor with shape `(3, 224, 224)` and ImageNet-normalized values
+- **Value Range**: Approximately [-2.12, 2.64] based on ImageNet constants
+
+This preprocessing step is crucial for the face authentication pipeline, as it ensures images are in the exact format expected by the ConvNeXt model in the next exercise.
diff --git a/docs/edge_ai/face_authentication/2_embeddings.md b/docs/edge_ai/face_authentication/2_embeddings.md
new file mode 100644
index 0000000..1e50961
--- /dev/null
+++ b/docs/edge_ai/face_authentication/2_embeddings.md
@@ -0,0 +1,139 @@
+# Exercise 02. ConvNeXt Model and Embedding Generation
+
+## Overview
+
+This exercise teaches you how to load a pre-trained ConvNeXt model and use it to generate face embeddings. You'll implement two key functions: `build_model()` to load the model and `compute_embedding()` to generate feature vectors from facial images.
+
+## What is ConvNeXt?
+
+ConvNeXt (Convolution meets NeXt) is a modern convolutional neural network architecture that bridges the gap between traditional CNNs and Vision Transformers (ViTs). Introduced by Facebook AI Research in 2022, ConvNeXt modernizes the standard ResNet architecture by incorporating design choices inspired by Vision Transformers.
+
+### Key Features of ConvNeXt:
+- **Pure Convolutional Architecture**: Uses only convolutions, no self-attention mechanisms
+- **Modernized ResNet Design**: Incorporates macro and micro design choices from ViTs
+- **Competitive Performance**: Achieves performance comparable to Swin Transformers
+- **Efficiency**: Maintains the computational efficiency of traditional CNNs
+
+### ConvNeXt-Atto Variant:
+We use **ConvNeXt-Atto**, an ultra-lightweight variant that provides excellent performance for face recognition tasks while being computationally efficient.
+
+## What are Face Embeddings?
+
+Embeddings are dense, low-dimensional vector representations that capture the essential characteristics of a face in numerical form.
+
+### Purpose of Face Embeddings:
+1. **Dimensionality Reduction**: Convert 224×224×3 images (~150K pixels) to compact vectors (~320 dimensions)
+2. **Feature Extraction**: Capture essential facial characteristics (eye shape, nose structure, etc.)
+3. **Similarity Computation**: Enable mathematical comparison between different faces
+4. **Efficient Storage**: Store compact representations instead of full images
+
+### Properties of Good Face Embeddings:
+- **Discriminative**: Different people produce different embeddings
+- **Robust**: Similar embeddings for the same person under different conditions
+- **Compact**: Much smaller than original images
+- **Comparable**: Can be compared using mathematical similarity metrics
+
+## Your Tasks
+
+### Task 1: Implement `build_model()`
+
+```rust
+pub fn build_model() -> Result>
+```
+
+This function should:
+1. **Download Model**: Use Hugging Face Hub API to get "timm/convnext_atto.d2_in1k"
+2. **Load Weights**: Load the SafeTensors model file
+3. **Create Model**: Build ConvNeXt without the final classification layer
+4. **Return Function**: Return a callable model function
+
+#### Why "Without Final Layer"?
+
+The original ConvNeXt model was trained for ImageNet classification (1000 classes). It has:
+- **Feature Extraction Layers**: Extract meaningful patterns from images
+- **Final Classification Layer**: Maps features to 1000 ImageNet class probabilities
+
+For face embeddings, we want:
+- ✅ **Feature Extraction**: The rich feature representations (embeddings)
+- ❌ **Classification**: We don't need ImageNet class predictions
+
+By removing the final layer, we get the raw feature vectors (embeddings) that capture facial characteristics, which we can then use for similarity comparison: Use `convnext::convnext_no_final_layer` - CHECK CANDLE CONVNEXT
+
+
+#### Implementation Approach:
+- Use Hugging Face Hub API for model download
+- Load model weights with VarBuilder
+- Create ConvNeXt architecture without classification head
+- Return the model as a callable function
+
+**Hint**: Check the CHEATSHEET.md for HuggingFace API patterns and model loading.
+
+### Task 2: Implement `compute_embedding()`
+
+```rust
+pub fn compute_embedding(model: &Func, image: &Tensor) -> Result
+```
+
+This function should:
+1. **Handle Input Format**: Check if input is single image or batch
+2. **Add Batch Dimension**: If needed, ensure proper tensor dimensions
+3. **Forward Pass**: Run the image through the model
+4. **Return Embeddings**: Return the feature vectors
+
+#### Implementation Approach:
+- Check tensor dimensions to determine if batching is needed
+- Ensure input tensor has the correct shape for the model
+- Use the model's forward method to generate embeddings
+- Return the resulting embedding tensor
+
+**Hint**: Models typically expect batch dimensions. Check the CHEATSHEET.md for tensor dimension handling.
+
+## Technical Details
+
+### Model Architecture:
+- **Input**: 224×224×3 RGB images (ImageNet normalized)
+- **Output**: 768-dimensional embedding vectors
+- **Weights**: Pre-trained on ImageNet dataset
+- **Format**: SafeTensors for efficient loading
+
+### Tensor Shapes:
+- **Single Image Input**: `[3, 224, 224]` → `[1, 3, 224, 224]` (add batch dim)
+- **Batch Input**: `[N, 3, 224, 224]` → `[N, 3, 224, 224]` (keep as is)
+- **Output**: `[N, 768]` where N is batch size
+
+### Key Dependencies:
+- `hf_hub` - Download models from Hugging Face
+- `candle_transformers::models::convnext` - ConvNeXt implementation
+- `candle_nn::VarBuilder` - Load model weights
+
+## Testing
+
+The test verifies that:
+- Model loads successfully from Hugging Face
+- Embedding computation works with preprocessed images
+- Output tensor has the correct batch dimension
+
+Run the test with:
+```bash
+cargo test
+```
+
+## Expected Behavior
+
+After successful implementation:
+- `build_model()` downloads and loads the ConvNeXt-Atto model
+- `compute_embedding()` processes images and returns 768-dimensional embeddings
+- The model handles both single images and batches automatically
+
+## Next Steps
+
+After completing this exercise, you'll be ready to:
+- Learn similarity computation between embeddings (Exercise 03)
+- Understand how these embeddings enable face recognition
+- Build storage systems for embedding databases (Exercise 04)
+
+## References
+
+- **ConvNeXt Paper**: [A ConvNet for the 2020s](https://arxiv.org/abs/2201.03545)
+- **Hugging Face Model**: [timm/convnext_atto.d2_in1k](https://huggingface.co/timm/convnext_atto.d2_in1k)
+- **Candle ConvNeXt**: [GitHub Implementation](https://github.com/huggingface/candle/blob/main/candle-transformers/src/models/convnext.rs)
diff --git a/docs/edge_ai/face_authentication/3_similarity.md b/docs/edge_ai/face_authentication/3_similarity.md
new file mode 100644
index 0000000..9a3ffd0
--- /dev/null
+++ b/docs/edge_ai/face_authentication/3_similarity.md
@@ -0,0 +1,134 @@
+# Exercise 03: Cosine Similarity for Face Authentication
+
+## Overview
+
+This exercise teaches you how to compute cosine similarity between face embeddings - the core mathematical operation that enables face recognition. You'll implement L2 normalization and cosine similarity functions that determine whether two faces belong to the same person.
+
+## Why Cosine Similarity for Face Recognition?
+
+Cosine similarity is the gold standard for comparing face embeddings because it:
+
+- **Measures Direction, Not Magnitude**: Focuses on the "shape" of the embedding vector, not its size
+- **Handles Lighting Variations**: Less sensitive to brightness changes that might scale embedding values
+- **Provides Intuitive Scores**: Returns values between -1 and 1, where 1 means identical faces
+- **Industry Standard**: Used by most production face recognition systems
+
+## Mathematical Foundation
+
+### L2 Normalization
+**Formula**: `normalized_vector = vector / ||vector||₂`
+
+L2 normalization ensures all embeddings have unit length (magnitude = 1), which:
+- **Standardizes Comparisons**: All vectors have the same magnitude
+- **Improves Robustness**: Reduces sensitivity to lighting and scale variations
+- **Enables Fair Comparison**: Focuses on directional relationships
+- **Optimizes Similarity**: Makes cosine similarity equivalent to dot product
+
+### Cosine Similarity
+**Formula**: `cosine_similarity = (A · B) / (||A|| × ||B||)`
+
+For normalized vectors, this simplifies to just the dot product: `A · B`
+
+**Key Properties**:
+- **Range**: [-1, 1] where 1 = identical, 0 = orthogonal, -1 = opposite
+- **Magnitude Invariant**: Only considers the angle between vectors
+- **Symmetric**: similarity(A, B) = similarity(B, A)
+
+## Your Tasks
+
+### Task 1: Implement `normalize_l2()`
+
+```rust
+fn normalize_l2(v: &Tensor) -> Result
+```
+
+This helper function should:
+1. **Calculate L2 Norm**: Compute the magnitude of the vector
+2. **Normalize**: Divide the vector by its norm to get unit length
+
+#### Implementation Approach:
+- Use tensor operations to compute the L2 norm (square, sum, square root)
+- Apply broadcasting division to normalize the vector
+- Ensure dimensions are handled correctly for broadcasting
+
+**Hint**: Check the CHEATSHEET.md for L2 normalization building blocks.
+
+### Task 2: Implement `cosine_similarity()`
+
+```rust
+pub fn cosine_similarity(emb_a: &Tensor, emb_b: &Tensor) -> Result
+```
+
+This function should:
+1. **Normalize Both Embeddings**: Apply L2 normalization to both inputs
+2. **Compute Dot Product**: Calculate the similarity using matrix operations
+3. **Extract Scalar**: Convert the result tensor to a single f32 value
+
+#### Implementation Approach:
+- Use your `normalize_l2` function on both input embeddings
+- Perform matrix multiplication to compute the dot product
+- Handle tensor dimensions and extract the final scalar value
+
+**Hint**: Check the CHEATSHEET.md for cosine similarity building blocks and tensor operations.
+
+## Technical Details
+
+### Tensor Shapes:
+- **Input Embeddings**: `[1, 768]` (batch size 1, 768 dimensions)
+- **After Normalization**: `[1, 768]` (same shape, unit length)
+- **After Matrix Multiply**: `[1, 1]` (scalar in tensor form)
+- **Final Output**: `f32` scalar value
+
+### Key Candle Operations:
+- `.sqr()` - Element-wise square
+- `.sum_keepdim(1)` - Sum along dimension 1, keep the dimension
+- `.sqrt()` - Element-wise square root
+- `.broadcast_div()` - Element-wise division with broadcasting
+- `.matmul()` - Matrix multiplication
+- `.transpose(0, 1)` - Swap dimensions 0 and 1
+- `.squeeze()` - Remove dimensions of size 1
+- `.to_vec0::()` - Convert 0D tensor to scalar
+
+## Testing
+
+The test verifies that:
+- Same person (brad1.png vs brad2.png) has higher similarity than different people
+- The similarity computation works with real face embeddings
+- Values are in the expected range
+
+Run the test with:
+```bash
+cargo test
+```
+
+## Understanding the Results
+
+### Typical Similarity Ranges:
+- **Same Person**: 0.7 - 0.95 (high similarity)
+- **Different People**: 0.2 - 0.6 (lower similarity)
+- **Identical Images**: ~1.0 (perfect similarity)
+
+### Authentication Thresholds:
+- **High Security**: 0.85+ (few false positives, some false negatives)
+- **Balanced**: 0.75+ (good balance of security and usability)
+- **High Accessibility**: 0.65+ (fewer false negatives, more false positives)
+
+## Real-World Considerations
+
+### Factors Affecting Similarity:
+- **Lighting Conditions**: Dramatic lighting can reduce similarity
+- **Facial Expressions**: Extreme expressions may lower scores
+- **Image Quality**: Blurry or low-resolution images affect accuracy
+- **Pose Variations**: Profile vs frontal views impact similarity
+
+## Next Steps
+
+After completing this exercise, you'll be ready to:
+- Build storage systems for face embeddings (Exercise 04)
+- Implement similarity search and retrieval (Exercise 05)
+
+## References
+
+- **Cosine Similarity**: [Wikipedia - Cosine Similarity](https://en.wikipedia.org/wiki/Cosine_similarity)
+- **Face Recognition Survey**: [Deep Face Recognition: A Survey](https://arxiv.org/abs/1804.06655)
+- **L2 Normalization**: [Unit Vector Normalization](https://en.wikipedia.org/wiki/Unit_vector)
diff --git a/docs/edge_ai/face_authentication/4_local_storage.md b/docs/edge_ai/face_authentication/4_local_storage.md
new file mode 100644
index 0000000..3a98a76
--- /dev/null
+++ b/docs/edge_ai/face_authentication/4_local_storage.md
@@ -0,0 +1,216 @@
+# Exercise 04: Local File Storage for Face Embeddings
+
+## Overview
+
+This exercise teaches you how to build a persistent storage system for face embeddings using local JSON files. You'll implement a complete storage solution that can save, retrieve, and manage embedding records - essential for any face authentication system that needs to remember users between sessions.
+
+## Why Storage Matters
+
+Face authentication systems need persistent storage to:
+- **Remember Users**: Store embeddings from registration for future login attempts
+- **Enable Comparison**: Retrieve stored embeddings to compare against live captures
+- **Manage Identities**: Track multiple embeddings per user for better accuracy
+- **Persist Data**: Maintain user data across application restarts
+
+## Architecture Overview
+
+The storage system uses a trait-based design for flexibility:
+
+```rust
+// Data structure for each stored embedding
+pub struct EmbeddingRecord {
+ pub id: String, // Unique identifier
+ pub name: String, // User name
+ pub embedding: Vec, // Face embedding vector
+ pub created_at: chrono::DateTime, // Timestamp
+ pub metadata: HashMap, // Additional data
+}
+
+// Storage interface (trait)
+pub trait EmbeddingStorage {
+ fn store_embedding(&mut self, record: EmbeddingRecord) -> Result<()>;
+ fn get_embedding(&self, id: &str) -> Result