# Project export: AdSmart

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: Cal Hacks 12.0
- Tagline: Adsmart is our solution for intelligent ad recommendation. We extract high-value features and insights from both static and video advertisements to feed into your ad recommendation engine.
- Devpost: https://devpost.com/software/adsmart
- GitHub: https://github.com/PolnareffTurtle/ad-intelligence
- Video: https://www.youtube.com/embed/YQHsXMglC9A?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — PolnareffTurtle (9 commits), Theo Dela Cruz (6 commits), Kshitij (2 commits)

## Devpost submission (written by the team)

### Inspiration

We were inspired by the Applovin Ad Intelligence Challenge!

### What it does

Our project is an app where users can upload video/image advertisements, and it returns all the important features and insights from that ad that might inform an ad recommendation engine.

### How we built it

We built the backend using python and various computer vision / audio processing frameworks including OpenCV, Gemini API, FFmpeg, TensorFlow, etc. We collaborated using a streamlined Git workflow.

### Challenges we ran into

The biggest challenge we ran into that probably many other hackers shared was the persistent WiFi problem. We had to relocate to an area with better connection to work.

### Accomplishments we're proud of

We're really proud of the efficiency of our code! The image/video processing for 12 unique features took on average 1 minute, and the audio processing for 4 other features took on average only 30 seconds! Plus most of our feature detection was highly accurate. We're also very proud about the unique features we came up with: for images, our best feature was a measure of negative space, and for videos, we examined the number of sound peaks that were in the audio.

### What we learned

We learned that it is very important to plan ahead and reroute when necessary, especially due to connection issues.

### What's next

In the future, we will make the website fully functional and polish it! Furthermore we plan to examine some more difficult signals that we came up with, but didn't have enough time to implement. These include: sychronization of audio/video, overall presentation method of video using audio transcription, etc.

## README (from the GitHub repository)

# 📊 AdSmart - Intelligent Advertisement Feature Extraction

**Calhacks 12.0 Submission**

AdSmart is a comprehensive web application that leverages computer vision, audio analysis, and generative AI to provide actionable insights on advertisement performance. Upload your image or video ads and receive instant, data-driven feedback on key metrics that influence audience engagement.

---

## 🎯 Problem Statement

Modern advertisers struggle to quantify what makes an ad effective. Traditional A/B testing is expensive and time-consuming. AdMetrics AI solves this by providing instant, objective analysis of ad creative elements before launch, helping marketers optimize their content for maximum impact.

---

## 📊 Signals Explained

### Image Signals

| Metric | Range | Description |
|--------|-------|-------------|
| **Color Contrast** | 0.0 - 1.0 | Measures visual distinction and readability |
| **Product Size Ratio** | 0.0 - 1.0 | Product prominence (0=small, 1=large) |
| **Text Number** | Int | Answers the question: How many words appear on the ad? |
| **Negative Space Ratio** | 0.0 - 1.0 | White space for visual breathing room |
| **Product Name** | String | Answers the question: What product is being sold? |
| **External Buttons** | List | Detected CTA text elements |
| **Human Features** | List | Detected human characteristics |
| **Appeal** | String | Explains the appeal of product |

### Video Signals

| Metric | Range | Description |
|--------|-------|-------------|
| **Scene Count** | Integer | Number of distinct scenes/cuts |
| **Dimensions** | Float | Ratio of width/length |
| **Product Name** | String | Answers the question: What is the name of the product? |
| **Human Features** | List | Detected human characteristics |
| **Sound Peaks** | dB/10s | Number of significant peaks |
| **Speech Coverage** | 0.0 - 100% | Voice-over percentage |
| **Music Coverage** | 0.0 - 100% | Music-over percentage |
| **Time Until First Significant Peak** | seconds | When the first significant sound change is created (whether music or speech) |

---

### 🎨 User Experience
- **Batch Upload Support** - Analyze multiple ads simultaneously
- **Persistent Media Library** - View and re-analyze historical uploads
- **Real-time Processing** - Instant feedback with progress indicators
- **Responsive Design** - Works seamlessly on desktop and mobile
- **Exportable Results** - Download full analysis as JSON

---

## 🛠️ Tech Stack

### Frontend
- **Streamlit** - Rapid web app development with Python
- **Custom CSS** - Polished, professional UI/UX

### Computer Vision & Image Processing
- **OpenCV (cv2)** - Advanced image and video analysis
- **PIL/Pillow** - Image manipulation and format conversion
- **NumPy** - Efficient numerical computations

### AI & Machine Learning
- **Google Gemini 2.5 Flash** - State-of-the-art multimodal AI for product detection and feature recognition
- **Pydantic** - Structured data validation and schema enforcement

### Audio Processing
- **FFmpeg** - Audio extraction and conversion
- **Custom Audio Analysis Pipeline** - Peak detection, voice analysis, and music classification

### Backend
- **Python 3.8+** - Core application logic
- **python-dotenv** - Secure environment variable management
- **Tempfile** - Safe file handling and cleanup

---

## 🚀 Installation & Setup

### Prerequisites
```bash
- Python 3.8 or higher
- FFmpeg (for video/audio processing)
- Google Gemini API Key
```

### Installation Steps

1. **Clone the Repository**
```bash
git clone https://github.com/PolnareffTurtle/ad-intelligence
cd ad-intelligence
```

2. **Create Virtual Environment**
```bash
python -m venv .venv
source .venv/bin/activate  # On Windows: venv\Scripts\activate
```

3. **Install Dependencies**
```bash
pip install -r requirements.txt
```

4. **Install FFmpeg**
   - **macOS**: `brew install ffmpeg`
   - **Ubuntu/Debian**: `sudo apt-get install ffmpeg`
   - **Windows**: Download from [ffmpeg.org](https://ffmpeg.org/download.html)

5. **Configure Environment Variables**
```bash
# Create .env file
touch .env

# Add your Gemini API key (add this to the .env file)
echo "GEMINI_API_KEY=your_gemini_api_key_here" >> .env
```

6. **Run the Application**
```bash
streamlit run calhacks.py
```

7. **Alternatives to Website**
```bash
python3 main.py # this runs the image/video processing, shows run time
python3 audio.py # this runs the audio processing
```

The app will open automatically in your browser at `http://localhost:8501`

## 📦 Dependencies
```txt
streamlit>=1.28.0
opencv-python>=4.8.0
numpy>=1.24.0
Pillow>=10.0.0
google-generativeai>=0.3.0
pydantic>=2.0.0
python-dotenv>=1.0.0
```

## 🎮 Usage Guide

### Analyzing an Image Ad

1. Click **"📷 Upload Image Ad"** on the homepage
2. Select one or more image files (PNG, JPG, JPEG)
3. Preview your upload and click **"🔍 Analyze Ad(s)"**
4. Review metrics in the analysis dashboard
5. Download results as JSON for reporting

### Analyzing a Video Ad

1. Click **"🎥 Upload Video Ad"** on the homepage
2. Select a video file (MP4, MOV, AVI)
3. Preview your upload and click **"🔍 Analyze Ad(s)"**
4. Wait for video and audio processing (may take 30-60 seconds)
5. Review comprehensive metrics across visual and audio dimensions

### Batch Analysis

- Upload multiple images at once for comparative analysis
- View timing metrics for each file processed
- Access all results from the media library

### Managing History

- View all uploaded images via **"🖼️ All Images"**
- View all uploaded videos via **"🎬 All Videos"**
- Clear specific media types or entire history via **"🗑️ Clear History"**

---

## 🔬 Methodology

### Image Analysis Pipeline
1. **Preprocessing** - Resize and normalize images for consistent analysis
2. **Edge Detection** - Canny algorithm for negative space calculation
3. **Local Contrast** - Kernel-based intensity variance measurement
4. **AI Vision** - Gemini 2.5 Flash for semantic understanding
5. **Metric Normalization** - Scale all values to [0, 1] for comparability

### Video Analysis Pipeline
1. **Frame Extraction** - Sample key frames (start, end, and midpoints)
2. **Motion Analysis** - Laplacian variance for blur detection
3. **Scene Detection** - Frame differencing for transition identification
4. **Audio Extraction** - FFmpeg conversion to WAV format
5. **Audio Analysis** - Peak detection, voice isolation, music classification
6. **Temporal Metrics** - First 3-second hook and loudness progression

### AI Integration
- **Gemini 2.5 Flash** provides structured JSON responses with thinking budgets
- **Pydantic Models** ensure type safety and schema validation
- **Fallback Handling** for graceful degradation when AI is unavailable

---


---


## 🔮 Future Enhancements

- [ ] **Engagement Prediction Model** - ML model trained on historical ad performance
- [ ] **Competitor Benchmarking** - Compare your ads to industry standards
- [ ] **Platform-Specific Optimization** - Instagram vs. YouTube vs. TikTok recommendations
- [ ] **A/B Test Suggestions** - AI-generated variation recommendations
- [ ] **Real-time Feedback** - Live metrics during ad creation
- [ ] **Team Collaboration** - Shared workspaces and commenting
- [ ] **API Access** - Programmatic analysis for CI/CD pipelines
- [ ] **Advanced Audio NLP** - Sentiment and message clarity analysis


## 👥 Team

**Calhacks 12.0 Team**
- Kshitij Tomar - Team Member #1
- Yechan Park - Team Member #2
- Theo Dela Cruz - Team Member #3
---


## Detected evidence (automated analysis)

Indexed codebase: 5 recognized source files, 81 KB.
- Python (language) — detected in the code
- Streamlit (technology) — detected in the code

## Codebase structure (from repository index)

### Files (7 of 7)

```
.gitignore
audio.py
calhacks.py
image_processing_lib.py
main.py
README.md
requirements.txt
```

### Dependencies

- requirements.txt: google-genai, librosa, numpy, opencv-python, pillow, pytesseract, python-dotenv, soundfile, streamlit

### Recent commits (newest first)

- Update project title and description in README
- Update README.md
- Final Readme
- Remove slight errors
- Update README.md
- READMe then update
- Readme Update
- Final one
- Merge pull request #5 from PolnareffTurtle/audiotojson
- output is json
- Merge branch 'main' of https://github.com/PolnareffTurtle/ad-intelligence
- updated requirements.txt
- Merge pull request #4 from PolnareffTurtle/Kaykay
- Website push request
- Merge pull request #3 from PolnareffTurtle/theo
- implemented yamnet while balding
- Merge pull request #2 from PolnareffTurtle/yechan
- finalized extract_image_signals, extract_video_signals
- Merge pull request #1 from PolnareffTurtle/yechan
- most image/video processing functions created

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

### requirements.txt

```
opencv-python
numpy
google-genai
pillow
python-dotenv
pytesseract
streamlit
librosa
soundfile

```

### main.py

```python
from image_processing_lib import extract_image_signals, extract_video_signals
import time

if __name__ == "__main__":
    total_start = time.time()
    first_start = time.time()
    for i in range(1, 21):
        start = time.time()
        if i < 10:
            video_path = f"ads/videos/v000{i}.mp4"
        else:
            video_path = f"ads/videos/v00{i}.mp4"
        print(i, extract_video_signals(video_path))
        print("Time taken:", time.time() - start)
    print("Total time taken for videos:", time.time() - total_start)
    total_start = time.time()
    for i in range(1, 18):
        start = time.time()
        if i < 10:
            image_path = f"ads/images/i000{i}.png"
        else:
            image_path = f"ads/images/i00{i}.png"
        print(i, extract_image_signals(image_path))
        print("Time taken:", time.time() - start)
    print("Total time taken for images:", time.time() - total_start)
    print("Overall total time taken:", time.time() - first_start)

```

### image_processing_lib.py

```python
import cv2
import numpy as np
from google import genai 
from google.genai import types
from PIL import Image
from dotenv import load_dotenv
import time
import pydantic
import pytesseract
import json


def extract_image_signals(image_path: str) -> dict:
    signals = get_gemini_from_image(image_path)
    signals = json.loads(signals)
    signals['num_words'] = get_num_words(image_path)
    signals['local_contrast'] = get_local_contrast(image_path)
    signals['negative_space_ratio'] = get_negative_space_ratio(image_path)
    return signals

def extract_video_signals(video_path: str) -> dict:
    signals = get_gemini_from_video(video_path)
    signals = json.loads(signals)
    signals['num_scenes'] = get_num_scenes(video_path)
    signals['dimension_ratio'] = get_dimension_ratio(video_path)
    return signals

def get_local_contrast(image_path, ksize=25):
    img = cv2.imread(image_path)
    img = cv2.resize(img, (256, 256))
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    mean = cv2.blur(gray, (ksize, ksize))
    diff = cv2.absdiff(gray, mean)

    # Normalize based on observed min/max values
    min = 0.05
    max = 0.25
    val = diff.mean() / 255.0
    norm_val = (val - min) / (max - min)
    return np.clip(round((norm_val), 4),0.0,1.0) 

def get_negative_space_ratio(image_path: str, low_thresh=1, high_thresh=30) -> float:
    """
    Calculates the negative space ratio using Canny edge detection.

    Args:
        image_path: Path to the ad image file.
        low_thresh: Lower bound for the Canny hysteresis threshold.
        high_thresh: Upper bound for the Canny hysteresis threshold.

    Returns:
        The Negative Space Ratio (float, range [0.0, 1.0]).
    """
    try:
        # 1. Load and Pre-process
        img = cv2.imread(image_path)
        if img is None:
            return 0.0 # Return 0 for failure

        # Convert to grayscale (Edges are based on intensity changes)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        
        # Apply a Gaussian blur to reduce noise and simplify the image structure.
        # This makes the edge count more robust to JPEG/PNG artifacts.
        blurred = cv2.GaussianBlur(gray, (5, 5), 0)

        # 2. Canny Edge Detection
        # Canny returns a binary map where edge pixels are 255 (white) and non-edges are 0 (black).
        edges = cv2.Canny(blurred, low_thresh, high_thresh)

        # 3. Calculate Ratio
        total_pixels = edges.size
        # Count pixels that are NOT 0 (i.e., edge pixels, value 255)
        edge_pixels = np.count_nonzero(edges)
        
        # Calculate the ratio
        edge_ratio = edge_pixels / total_pixels
        
        # Negative space is the inverse of the edge ratio
        negative_space_ratio = 1.0 - edge_ratio
        
        # Clamp to the [0, 1] range just to be safe and return 4 decimal places
        # Normalize based on observed min/max values
        min = 0.8
        max = 0.99
        ratio = (negative_space_ratio - min) / (max - min)
        return round(np.clip(ratio, 0.0, 1.0), 4)

    except Exception as e:
        print(f"Error calculating negative space for {image_path}: {e}")
        return 0.0

def get_num_words(image_path: str) -> int:
    """
    Detects and counts the number of words in an image using Tesseract OCR.
    """
    try:
        # 1. Load and Pre-process Image
        img = cv2.imread(image_path)
        if img is None: return 0
        
        # Convert to RGB (essential for pytesseract compatibility)
        rgb_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        
        # 2. Get Bounding Box Data
        # output_type=pytesseract.Output.DICT includes word-level data
        data = pytesseract.image_to_data(rgb_img, output_type=pytesseract.Output.DICT)
        
        word_count = 0
        
        # 3. Filter and Count
        n_boxes = len(data['level'])
        
        for i in range(n_boxes):
            # Level 5 corresponds to individual words.
            # Confidence filtering (e.g., words detected with > 50% confidence)
            if data['level'][i] == 5 and int(data['conf'][i]) > 50 and data['text'][i].strip():
                word_count += 1
                
        return word_count
    
    except pytesseract.TesseractError:
        print("ERROR: Tesseract not configured. Ensure it's installed and in your PATH.")
        return 0
    except Exception as e:
        # Handle cases where image is too small or other CV error
        print(f"Error during Tesseract processing: {e}")
        return 0
    
def get_dimension_ratio(video_path: str) -> int: # width / height
    cap = cv2.VideoCapture(video_path)
    
    if not cap.isOpened():
        print(f"Error: Could not open video file at {video_path}")
        return None
    width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
    height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
    cap.release()
    return round(width / height, 2)

load_dotenv()
client = genai.Client()

class BaseAdModel(pydantic.BaseModel):
    product_name: str
    human_features: list[str] | None = None
    external_buttons: list[str] | None = None
    
class VideoAdModel(BaseAdModel):
    pass

class ImageAdModel(BaseAdModel):
    pass
    appeal: str | None = None
    product_screen_ratio: str | None = None

def get_gemini_from_image(image_path: str) -> str:
    """
    Uses Google GenAI to identify product in the image.
    """

    img = Image.open(image_path).convert("RGB")

    image_prompt = "The following image is from a static advertisement. Respond in JSON format with keys: product_name, human_features, external_buttons, appeal, product_screen_ratio. For product_name: Identify the product being advertised in 3 words or less. Only respond with the product name, no additional text. For human_features: List any notable human features present in the ad (e.g., smiling, diverse models), or return None if there are none. For external_buttons: List the text of any clickable buttons that appear, or return None if there are n
[truncated — 7190 more characters]
```

### audio.py

```python
import os
import glob
import json
import sys
import pathlib

import numpy as np

import tensorflow as tf

import soundfile as sf
from pathlib import Path
import ffmpeg
import librosa
import pyloudnorm as pyln

from concurrent.futures import ProcessPoolExecutor

def mp4_to_wav(mp4_path, wav_path, sr=16000):
    pathlib.Path(wav_path).parent.mkdir(parents=True, exist_ok=True)
    (
        ffmpeg
        .input(mp4_path)
        .audio
        .output(wav_path, ac=1, ar=sr, f='wav')  # mono, 16k
        .overwrite_output()
        .run(quiet=True)
    )

def load_wav(wav_path, sr=16000):
    y, _ = librosa.load(wav_path, sr=sr, mono=True)
    y = np.clip(y / (np.max(np.abs(y))+ 1e-9), -1, 1)
    return y, sr

def amplitude_envelope(y, sr, frame_ms=10, hop_ms=5, smooth_ms=30):
    """
    Short-time RMS envelope + smoothing.
    """
    frame_len = int(sr * frame_ms / 1000.0)
    hop_len = int(sr * hop_ms / 1000.0)
    # Ensure minimum sizes
    frame_len = max(frame_len, 1)
    hop_len = max(hop_len, 1)

    # RMS (amplitude)
    rms = librosa.feature.rms(y=y, frame_length=frame_len, hop_length=hop_len, center=True)[0]

    # Smooth with moving average over ~smooth_ms
    smooth_len = max(1, int((smooth_ms / hop_ms)))
    if smooth_len > 1:
        kernel = np.ones(smooth_len) / smooth_len
        rms_smooth = np.convolve(rms, kernel, mode="same")
    else:
        rms_smooth = rms

    times = librosa.frames_to_time(np.arange(len(rms_smooth)), sr=sr, hop_length=hop_len)
    return rms_smooth, times

def count_sound_peaks(
    y,
    sr,
    *,
    frame_ms=50,
    hop_ms=20,
    center_baseline=True,
    pre_max=6,
    post_max=6,
    pre_avg=24,
    post_avg=24,
    delta=0.1,
    wait=48,
):
    #Count sharp peaks using librosa.util.peak_pick on an RMS envelope.
    hop_length = max(1, int(sr * hop_ms / 1000.0))
    frame_length = max(hop_length + 1, int(sr * frame_ms / 1000.0))
    env = librosa.feature.rms(y=y, frame_length=frame_length, hop_length=hop_length, center=True)[0]
    if center_baseline and env.size > 0:
        env = env - float(np.median(env))
        peaks = librosa.util.peak_pick(env, pre_max=pre_max, post_max=post_max, pre_avg=pre_avg, post_avg=post_avg, delta=delta, wait=wait)
    return int(peaks.size)

def first_spike_time(
    y,
    sr,
    *,
    threshold_dbfs=-15,
    frame_ms=12,
    hop_ms=5,
    min_duration_ms=50
):
    #Return time (s) of first 'loud' segment using a threshold
    
    hop_length = max(1, int(sr * hop_ms / 1000.0))
    frame_length = max(hop_length + 1, int(sr * frame_ms / 1000.0))

    # Short-time RMS envelope
        # Short-time RMS envelope and dBFS (relative to full-scale 1.0)
    env = librosa.feature.rms(y=y, frame_length=frame_length, hop_length=hop_length, center=True)[0]
    dbfs = 20.0 * np.log10(env + 1e-12)

    # Require sustained exceedance for min_duration_ms
    need = max(1, int(round(min_duration_ms / hop_ms)))
    run = 0
    first_idx = None
    for i, is_loud in enumerate(dbfs >= threshold_dbfs):
        if is_loud:
            run += 1
            if run >= need:
                first_idx = i - need + 1
                break
        else:
            run = 0

    if first_idx is None:
        return None

    return float(first_idx * hop_length) / float(sr)

def audio_coverage_percent_simple(
    y,
    sr,
    *,
    frame_ms=20,
    hop_ms=10,
    threshold_dbfs=-20.0,
    bridge_gap_ms=120,
    min_speech_ms=60,
    adaptive_db=8.0,
    adaptive_window_ms=None,
    min_threshold_dbfs=-35.0,
    max_threshold_dbfs=-10.0
):
    """
    Estimate the percentage of the signal that contains speech-like content.

    Uses an RMS envelope with adaptive thresholding and simple morphological
    smoothing to reject brief noise bursts and bridge short pauses.
    """
    if y is None or len(y) == 0:
        return 0.0

    hop = max(1, int(sr * hop_ms / 1000.0))
    frame = max(hop + 1, int(sr * frame_ms / 1000.0))
    env = librosa.feature.rms(y=y, frame_length=frame, hop_length=hop, center=True)[0].astype(np.float64)
    if env.size == 0:
        return 0.0

    # Convert to dBFS and derive an adaptive threshold relative to the noise floor.
    db = 20.0 * np.log10(env + 1e-12)
        # Either use a global adaptive threshold (noise floor + margin), or a per-window causal baseline.
    if adaptive_window_ms is None:
        noise_floor = np.percentile(db, 15.0)
        thr = float(noise_floor + adaptive_db)
        rel_thr = float(noise_floor + adaptive_db)
        thr = float(np.clip(min(threshold_dbfs, rel_thr), min_threshold_dbfs, threshold_dbfs))
        hot = db >= thr
    else:
        # Causal running mean baseline over adaptive_window_ms (in frames)
        frame_period_ms = 1000.0 * hop / float(sr)
        win = max(1, int(round(adaptive_window_ms / max(frame_period_ms, 1e-9))))
        c = np.cumsum(db, dtype=np.float64)
        baseline = np.empty_like(db, dtype=np.float64)
        upto = min(win, db.size)
        baseline[:upto] = c[:upto] / np.arange(1, upto + 1, dtype=np.float64)
        if db.size > win:
            baseline[win:] = (c[win:] - c[:-win]) / float(win)

        rel_series = baseline + float(adaptive_db)
        thr_series = np.clip(np.minimum(rel_series, threshold_dbfs), min_threshold_dbfs, threshold_dbfs)
        hot = db >= thr_series


    frame_period_ms = 1000.0 * hop / float(sr)
    min_speech_frames = max(1, int(round(min_speech_ms / max(frame_period_ms, 1e-9))))
    gap_frames = max(1, int(round(bridge_gap_ms / max(frame_period_ms, 1e-9))))

    # Remove very short speech detections (likely transient noise).
    if hot.any() and min_speech_frames > 1:
        i = 0
        T = hot.size
        while i < T:
            if hot[i]:
                j = i
                while j < T and hot[j]:
                    j += 1
                if (j - i) < min_speech_frames:
                    hot[i:j] = False
                i = j
            else:
                i += 1

    # Bridge short gap
[truncated — 12161 more characters]
```

### calhacks.py

```python
import streamlit as st
import numpy as np
from PIL import Image
import io
import json
from typing import Dict, List, Optional
import tempfile
import os
import uuid
from datetime import datetime
import glob
import time
from image_processing_lib import extract_image_signals, extract_video_signals

# Set page config
st.set_page_config(
    page_title="Ad Performance Analyzer",
    page_icon="📊",
    layout="wide"
)

try:
    from video_analysis import (
        measure_motion_blur,
        count_scenes,
        get_video_dimensions,
        detect_product_in_video,
        count_sound_peaks,
        detect_number_of_voices,
        detect_voice_type,
        analyze_music_type,
        analyze_first_3_seconds,
        measure_loudness_drift,
        measure_speech_coverage
    )
except ImportError:
    st.warning("Video analysis functions not found. Using placeholder functions.")
    # Placeholder functions for testing
    def measure_motion_blur(video_path): return (0.3, "Motion blur justification")
    def count_scenes(video_path): return (5, "Scene count justification")
    def get_video_dimensions(video_path): return "1920x1080"
    def detect_product_in_video(video_path): return ("Unknown Product", "Product detection justification")
    def count_sound_peaks(audio_path): return (15, "Sound peaks justification")
    def detect_number_of_voices(audio_path): return (2, "Number of voices justification")
    def detect_voice_type(audio_path): return ("human", "Voice type justification")
    def analyze_music_type(audio_path): return ({"bpm": 120, "bass": "medium", "type": "pop"}, "Music analysis justification")
    def analyze_first_3_seconds(audio_path): return ({"intensity": 0.7, "first_nonsilence": 0.5}, "First 3 seconds justification")
    def measure_loudness_drift(audio_path): return (2.5, "Loudness drift justification")
    def measure_speech_coverage(audio_path): return (0.6, "Speech coverage justification")

# Initialize session state
if 'page' not in st.session_state:
    st.session_state.page = 'home'
if 'current_file' not in st.session_state:
    st.session_state.current_file = None
if 'file_type' not in st.session_state:
    st.session_state.file_type = None
if 'view_images' not in st.session_state:
    st.session_state.view_images = False
if 'view_videos' not in st.session_state:
    st.session_state.view_videos = False

# Create uploads directory if it doesn't exist
UPLOADS_DIR = "uploads"
IMAGES_DIR = os.path.join(UPLOADS_DIR, "images")
VIDEOS_DIR = os.path.join(UPLOADS_DIR, "videos")
os.makedirs(IMAGES_DIR, exist_ok=True)
os.makedirs(VIDEOS_DIR, exist_ok=True)


def get_recent_files(directory: str, count: Optional[int] = None) -> List[str]:
    """Get the most recent files from a directory"""
    files = glob.glob(os.path.join(directory, "*"))
    files.sort(key=os.path.getmtime, reverse=True)

    if count is None:
        # If count is None, return all files
        return files
    else:
        # Otherwise, return only the number specified by count
        return files[:count]

def analyze_image(image_path: str) -> Dict:
    """Complete image analysis using custom functions"""

    results = { 
        "color_contrast": 0.0, 
        "product": "ERROR: ANALYSIS FAILED",
        "product_size_ratio": 0.0,
        "external_link_buttons": [],
        "human_features": [],
        "text_ratio": 0.0,
        "negative_space_ratio": 0.0,
        "justifications": {"color_contrast": "Analysis failed: Check file or dependencies."},
        "appeal": "N/A"
    }
    try: 
        signals = extract_image_signals(image_path)

        # Call your custom analysis functions
        color_contrast = signals.get('local_contrast')
        product, product_justification = signals.get('product_name'), "Different products are appealing to different people"
        product_size_ratio, product_size_justification = signals.get('product_screen_ratio'), "Different sizes of products appeal to different people"
        external_links, external_links_justification = signals.get('external_buttons'), "External websites incentive certain populations to buy/not buy a product"
        human_features, human_features_justification = signals.get('human_feature'), "Human features vs AI features helps builds credibility"
        text_ratio, text_ratio_justification = signals.get('num_words'), "How much text there is on a screen vs off the screen helps proffesionalism of an AD"
        negative_space, negative_space_justification = signals.get('negative_space_ratio'), "How much empty space there is in an ad versus cluttered ad. (Minimalist vs Materialistic)"
        appeal, appeal_justification = signals.get('appeal'), "Ethos/Pathos/Logos emphasization of the ad"
        
        results = {
            "color_contrast": color_contrast,
            "product": product,
            "product_size_ratio": product_size_ratio,
            "external_link_buttons": external_links,
            "human_features": human_features,
            "text_ratio": text_ratio,
            "negative_space_ratio": negative_space,
            "appeal": appeal,
            "justifications": {
                "color_contrast": "Some people prefer more color contrast, while others prefer less",
                "product": product_justification,
                "product_size_ratio": product_size_justification,
                "external_link_buttons": external_links_justification,
                "human_features": human_features_justification,
                "text_ratio": text_ratio_justification,
                "negative_space_ratio": negative_space_justification,
                "appeal_justification": appeal_justification
            }
        }

    except Exception as e:
        print(f"Error extracting image signals for {image_path}: {e}")

    return results


def extract_audio_from_video(video_path: str) -> Optional[str]:
    """Extract audio from video file"""
    try:
        audio_path = tempfile.mktemp(suffix='.wav')
     
[truncated — 37255 more characters]
```