# Project export: ADapt

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 2026
- Tagline: One ad. Every market.AI-localized in seconds.
- Devpost: https://devpost.com/software/adapt-ujvn5e
- GitHub: https://github.com/ssiddhantsood/treehacks
- Video: https://www.youtube.com/embed/C3_5c9b4zxg?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Runpod] Best use of Flash (1st Place: 200 Runpod credits per team member (up to 4). 2nd Place: 100 Runpod credits per team member (up to 4). 3rd Place: 50 Runpod credits per team member (up to 4).))
- Team: 5 GitHub contributor(s) — MichaelCai2005 (12 commits), Cursor (12 commits), soradotwav (11 commits), Siddhant Sood (10 commits), lukehuang (3 commits)

## Devpost submission (written by the team)

### Inspiration

Every brand wants personalized ads, but creating unique creatives for every audience segment is expensive and slow. We watched marketing teams manually re-edit the same video dozens of times, tweaking pacing, color, and captions for different demographics. We thought: what if AI could understand both the video and the audience, and automatically generate targeted variants from a single, manually developed ad?

### What it does

ADapt is an AI-powered video ad localization platform. Upload one master video and a CSV of audience profiles, and it: Analyzes your video by extracting scenes, transcribing audio, and generating per-second action descriptions Clusters your audience into meaningful segments using embeddings, visualized on an interactive 3D map Researches each segment with real-time market insights via Perplexity Sonar Generates targeted variants with segment-specific speed adjustments, color grading, text overlays, vertical reframing, and more, all driven by a constraint-aware AI planner The result: multiple production-ready ad variants from a single upload, each tailored to a specific audience.

### How we built it

Backend: FastAPI with a multi-agent architecture. An Orchestrator routes requests, a Transform Planner uses GPT with a constraint-checking review loop, a Market Research Agent queries Perplexity Sonar, and a Group Ads Generator coordinates the full pipeline. Video Processing: FFmpeg handles speed changes (bounded at ±6%), 7 color grading presets, text overlays with impact-scored phrase placement, film grain, backdrop blur, and vertical reframing. Audience Intelligence: Elasticsearch embeddings for clustering with a heuristic fallback, projected into 3D via SVD for interactive visualization. Frontend: Next.js 16, React 19, Tailwind CSS 4, and TypeScript. Features a campaign dashboard, 3-step upload modal, timeline scrubber, 3D embeddings map, and variant gallery. MCP Server: Exposes video editing tools via the Model Context Protocol so external AI agents can use our platform programmatically.

### Challenges we ran into

Getting the LLM to reliably choose the right video transforms. We solved this with a multi-round planner/reviewer loop that enforces explicit constraints over up to 3 revision rounds. Deciding where to place text overlays required building an impact scoring algorithm that considers keywords, punctuation, position in the video, and audio vs. visual source. Cross-platform font handling with FFmpeg's drawtext filter behaves differently across OS, so we built a fallback chain with PIL-based rendering. Making audience clustering work without Elasticsearch by building a heuristic vectorization fallback from raw profile data.

### Accomplishments we're proud of

Full end-to-end automation: from a raw video and a CSV to multiple targeted ad variants with zero manual editing. A constraint system that produces thoughtful, context-aware edit decisions (moody grades for urban professionals, bright grades for teen audiences). An interactive 3D audience visualization that makes segment groupings immediately intuitive. MCP integration that makes ADapt composable with any AI agent workflow.

### What we learned

Structured constraint loops beat elaborate prompting for reliable LLM output. Small bounded transforms (like ±6% speed) compound into meaningful personalization without jarring artifacts. Deterministic randomization (MD5-based stable rolls) is critical for debugging pipelines with many moving parts. Robust fallbacks (heuristic clustering, font rendering, hardware acceleration detection) aren't just safety nets, they make the product actually usable across environments.

### What's next

Generative transforms: connecting stubbed-out background replacement, object erasure, and text replacement to cloud GPUs. A/B testing integration: feeding variant performance data back so ADapt learns which transforms work best per segment. Multi-language voice synthesis: re-voicing ads in different languages while preserving tone and cadence. Production scale: moving to a production database, adding job queues for parallel processing, and deploying on GPU infrastructure.

## README (from the GitHub repository)

# treehacks starter

## Structure
- `frontend/` Next.js (minimal UI)
- `backend/` FastAPI + OpenAI tool-calling agent + FFmpeg + action timeline

## Backend setup (Python)
1. `cd backend`
2. Create a venv and install deps:
   - `python -m venv venv`
   - `source venv/bin/activate`
   - `pip install -r requirements.txt`
3. Copy env: `cp .env.example .env` and set `OPENAI_API_KEY`
4. Set `JWT_SECRET` in `.env` (for auth)
5. Ensure `ffmpeg` is installed and on your PATH
6. Run: `./venv/bin/python -m uvicorn app:app --reload --port 8000 --env-file .env`

Endpoints:
- `POST /api/transform` (multipart form, field name `video`)
- `POST /api/market-research` (JSON body with `description`, optional `product`, `region`, `goal`)
- `GET /media/original/*` and `GET /media/processed/*`
- `GET /media/analysis/*` (JSON action timeline)
- `POST /api/auth/register`
- `POST /api/auth/login`
- `GET /api/me`
- `GET /api/videos`
- `GET /api/videos/{id}`

## Frontend setup (Next.js)
1. `cd frontend`
2. `npm install`
3. `npm run dev`

Optional: set `NEXT_PUBLIC_API_BASE` in `frontend/.env` (default `http://localhost:8000`).

## Notes
- The OpenAI tool-calling agent lives in `backend/ai_agents/agent.py` and always calls `speed_up_video`.
- The video processing logic is isolated in `backend/ai_agents/video.py`.
- Action timeline extraction is in `backend/ai_agents/action_timeline.py` and uses a VLM + optional audio transcription.
- If you want timestamped audio segments, set `OPENAI_ASR_MODEL=whisper-1` (it supports `verbose_json` segments).
- The backend now also generates a couple of random edit variants (combos) and returns them in `variants`.
- For faster video processing on macOS, set `VIDEO_HWACCEL=videotoolbox` and `VIDEO_ENCODER=h264_videotoolbox`.
- You can reduce analysis cost with `ACTION_FPS` and `ACTION_FRAME_SCALE`.
- GPU-heavy generative workflows live in `backend/ai_agents/generative/` (background replace, object erase, text replace) and are triggered via an agent that writes job specs.
- GPU dependencies for those workflows are listed in `backend/ai_agents/generative/requirements-gpu.txt`.
- Text overlays require an ffmpeg build with the `drawtext` filter (libfreetype).


## Detected evidence (automated analysis)

Indexed codebase: 69 recognized source files, 484 KB.
- CSS (language) — detected in the code
- FastAPI (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
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (105 of 105)

```
.dockerignore
.gitignore
backend/.dockerignore
backend/.env.example
backend/action_timeline.py
backend/ai_agents/__init__.py
backend/ai_agents/action_timeline.py
backend/ai_agents/agent.py
backend/ai_agents/generative/agent.py
backend/ai_agents/generative/jobs/.gitkeep
backend/ai_agents/generative/jobs/5a657bf20cb24beaac992d5e85219e24.json
backend/ai_agents/generative/jobs/5a798173630d450ba4275d34fc5b6c9b.json
backend/ai_agents/generative/jobs/73524b1858a14a7cab8408e80774a668.json
backend/ai_agents/generative/lucy_video_to_video/.env.example
backend/ai_agents/generative/lucy_video_to_video/.runpod/resources.pkl
backend/ai_agents/generative/lucy_video_to_video/environment.yml
backend/ai_agents/generative/lucy_video_to_video/lucy_run/__init__.py
backend/ai_agents/generative/lucy_video_to_video/lucy_run/.env.example
backend/ai_agents/generative/lucy_video_to_video/lucy_run/.runpod/resources.pkl
backend/ai_agents/generative/lucy_video_to_video/lucy_run/main.py
backend/ai_agents/generative/lucy_video_to_video/lucy_run/mothership.py
backend/ai_agents/generative/lucy_video_to_video/lucy_run/pyproject.toml
backend/ai_agents/generative/lucy_video_to_video/lucy_run/README.md
backend/ai_agents/generative/lucy_video_to_video/lucy_run/requirements.txt
backend/ai_agents/generative/lucy_video_to_video/lucy_run/workers/__init__.py
backend/ai_agents/generative/lucy_video_to_video/lucy_run/workers/gpu/__init__.py
backend/ai_agents/generative/lucy_video_to_video/lucy_run/workers/gpu/endpoint.py
backend/ai_agents/generative/lucy_video_to_video/main.py
backend/ai_agents/generative/lucy_video_to_video/Makefile
backend/ai_agents/generative/lucy_video_to_video/misc/README.md
backend/ai_agents/generative/lucy_video_to_video/pyproject.toml
backend/ai_agents/generative/lucy_video_to_video/README.md
backend/ai_agents/generative/lucy_video_to_video/requirements.txt
backend/ai_agents/generative/lucy_video_to_video/runpod_flash_examples.egg-info/dependency_links.txt
backend/ai_agents/generative/lucy_video_to_video/runpod_flash_examples.egg-info/PKG-INFO
backend/ai_agents/generative/lucy_video_to_video/runpod_flash_examples.egg-info/requires.txt
backend/ai_agents/generative/lucy_video_to_video/runpod_flash_examples.egg-info/SOURCES.txt
backend/ai_agents/generative/lucy_video_to_video/runpod_flash_examples.egg-info/top_level.txt
backend/ai_agents/generative/lucy_video_to_video/scripts/sync_example_deps.py
backend/ai_agents/generative/lucy_video_to_video/uv.lock
backend/ai_agents/generative/README.md
backend/ai_agents/generative/requirements-gpu.txt
backend/ai_agents/generative/scripts/background_replace.py
backend/ai_agents/generative/scripts/object_erase.py
backend/ai_agents/generative/scripts/text_replace.py
backend/ai_agents/generative/specs/job.schema.json
backend/ai_agents/generative/utils.py
backend/ai_agents/group_ads.py
backend/ai_agents/market_research.py
backend/ai_agents/orchestrator.py
backend/ai_agents/tool_catalog.py
backend/ai_agents/transform_planner.py
backend/ai_agents/video.py
backend/app.py
backend/auth.py
backend/cluster_profiles.py
backend/db.py
backend/mcp_server.py
backend/mock_profiles_large.csv
backend/mock_profiles.csv
backend/requirements.txt
backend/seed.py
backend/test_cluster.py
frontend/.gitignore
frontend/app/console/campaigns/[id]/page.tsx
frontend/app/console/layout.tsx
frontend/app/console/page.tsx
frontend/app/globals.css
frontend/app/layout.tsx
frontend/app/page.tsx
frontend/components/console/campaign-list.tsx
frontend/components/console/sidebar.tsx
frontend/components/console/upload-dialog.tsx
frontend/components/landing/footer.tsx
frontend/components/landing/hero.tsx
frontend/components/landing/how-it-works.tsx
frontend/components/landing/pricing.tsx
frontend/components/nav.tsx
frontend/components/ui/badge.tsx
frontend/components/ui/button.tsx
frontend/components/ui/card.tsx
frontend/components/ui/input.tsx
frontend/components/ui/logo.tsx
frontend/eslint.config.mjs
frontend/lib/api.ts
frontend/lib/auth.ts
frontend/lib/mock.ts
frontend/lib/types.ts
frontend/next.config.js
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/public/mock_profiles.csv
frontend/README.md
frontend/tsconfig.json
README.md
render.yaml
tests/analysis.json
tests/group_ads/metadata.json
tests/README.md
tests/run_analysis.py
tests/run_color_grades.py
tests/run_group_ads.py
tests/run_market_research.py
tests/run_tool_smoke.py
```

### Dependencies

- backend/ai_agents/generative/lucy_video_to_video/lucy_run/pyproject.toml: fastapi, python-dotenv, runpod-flash, uvicorn
- backend/ai_agents/generative/lucy_video_to_video/lucy_run/requirements.txt: fastapi, python-dotenv, runpod-flash, uvicorn
- backend/ai_agents/generative/lucy_video_to_video/pyproject.toml: fastapi@>=0.104.0, numpy@>=2.0.2, pillow@>=10.0.0, python-multipart@>=0.0.6, runpod-flash, structlog@>=23.0.0, uvicorn@>=0.24.0
- backend/ai_agents/generative/lucy_video_to_video/requirements.txt: aiodns@==4.0.0, aiohappyeyeballs@==2.6.1, aiohttp@==3.13.3, aiohttp-retry@==2.9.1, aiosignal@==1.4.0, annotated-doc@==0.0.4, annotated-types@==0.7.0, anyio@==4.12.1, attrs@==25.4.0, backoff@==2.2.1, backports-zstd@==1.3.0, bcrypt@==5.0.0, boto3@==1.42.41, botocore@==1.42.41, brotli@==1.2.0, certifi@==2026.1.4, cffi@==2.0.0, charset-normalizer@==3.4.4, click@==8.3.1, cloudpickle@==3.1.2, colorama@==0.4.6, cryptography@==45.0.7, dnspython@==2.8.0, email-validator@==2.3.0, fastapi@==0.128.0, fastapi-cli@==0.0.20, fastapi-cloud-cli@==0.11.0, fastar@==0.8.0, filelock@==3.20.3, frozenlist@==1.8.0, h11@==0.16.0, httpcore@==1.0.9, httptools@==0.7.1, httpx@==0.28.1, idna@==3.11, inquirerpy@==0.3.4, invoke@==2.2.1, itsdangerous@==2.2.0, jinja2@==3.1.6, jmespath@==1.1.0, markdown-it-py@==4.0.0, markupsafe@==3.0.3, mdurl@==0.1.2, multidict@==6.7.1, numpy@==2.4.2, orjson@==3.11.7, paramiko@==4.0.0, pathspec@==1.0.4, pfzy@==0.3.4, pillow@==12.1.0, prettytable@==3.17.0, prompt-toolkit@==3.0.52, propcache@==0.4.1, py-cpuinfo@==9.0.0, pycares@==5.0.1, pycparser@==3.0, pydantic@==2.12.5, pydantic-core@==2.41.5, pydantic-extra-types@==2.11.0, pydantic-settings@==2.12.0, pygments@==2.19.2, pynacl@==1.6.2, python-dateutil@==2.9.0.post0, python-dotenv@==1.2.1, python-multipart@==0.0.22, pyyaml@==6.0.3, questionary@==2.1.1, requests@==2.32.5, rich@==14.3.2, rich-toolkit@==0.18.1, rignore@==0.7.6, runpod@==1.8.1, runpod-flash@==1.0.0, s3transfer@==0.16.0, sentry-sdk@==2.51.0, shellingham@==1.5.4, six@==1.17.0, starlette@==0.50.0, structlog@==25.5.0, tomli@==2.4.0, tomlkit@==0.14.0, tqdm@==4.67.3, tqdm-loggable@==0.2, typer@==0.21.1, typing-extensions@==4.15.0, typing-inspection@==0.4.2, ujson@==5.11.0, urllib3@==2.6.3, uvicorn@==0.40.0, uvloop@==0.22.1, watchdog@==6.0.0, watchfiles@==1.1.1, wcwidth@==0.5.3, websockets@==16.0, yarl@==1.22.0
- backend/requirements.txt: fastapi@>=0.115.0, mcp[cli]@>=1.26.0, numpy@==2.1.3, openai@==1.55.3, passlib@==1.7.4, Pillow@==11.0.0, PyJWT@>=2.10.1, python-dotenv@==1.0.1, python-multipart@==0.0.9, uvicorn[standard]@>=0.30.6
- frontend/package.json: @tailwindcss/postcss@^4, @types/node@20.19.33, @types/react@19.2.14, @types/react-dom@^19, eslint@^9, eslint-config-next@16.1.6, lucide-react@^0.564.0, next@16.1.6, react@19.2.3, react-dom@19.2.3, tailwindcss@^4, typescript@5.9.3

### Recent commits (newest first)

- final
- cleanup: remove Dockerfile (using local MCP setup instead of cloud)
- remove slider on detailed view
- runpod integrated
- runpod
- added runpod
- revert: remove cloud/Render-specific changes, restore simple local setup
- fix: Dockerfile codec install, add /debug/ffmpeg diagnostic endpoint
- fix: FFmpeg encoder detection, stderr capture, and Dockerfile codec libs
- changes
- fix: prevent LLM from overriding output path, ensure files land in PROCESSED_DIR
- idk a lot
- fix: use custom_route + mcp.run() for proper session lifecycle
- fire campaign page
- Add download URLs, output verification, and file serving to MCP server
- Bump PyJWT to >=2.10.1 for mcp compatibility
- Move Dockerfile to repo root for Render compatibility
- Fix Render dockerfilePath to use explicit relative path
- Add Render cloud deployment for MCP server
- added agent

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

### backend/requirements.txt

```
fastapi>=0.115.0
numpy==2.1.3
Pillow==11.0.0
uvicorn[standard]>=0.30.6
python-multipart==0.0.9
openai==1.55.3
python-dotenv==1.0.1
passlib==1.7.4
PyJWT>=2.10.1
mcp[cli]>=1.26.0

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "lucide-react": "^0.564.0",
    "next": "16.1.6",
    "react": "19.2.3",
    "react-dom": "19.2.3"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "20.19.33",
    "@types/react": "19.2.14",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "tailwindcss": "^4",
    "typescript": "5.9.3"
  }
}

```

### backend/ai_agents/generative/lucy_video_to_video/pyproject.toml

```
[project]
name = "runpod-flash-examples"
version = "1.0.0"
description = "A collection of example applications showcasing Runpod Flash - a framework for building production-ready AI applications with distributed GPU and CPU computing."
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "fastapi>=0.104.0",
    "numpy>=2.0.2",
    "pillow>=10.0.0",
    "python-multipart>=0.0.6",
    "runpod-flash",
    "structlog>=23.0.0",
    "uvicorn>=0.24.0",
]

[dependency-groups]
dev = [
    "ruff>=0.8.0",
    "mypy>=1.13.0",
    "pytest>=8.3.0",
    "pytest-asyncio>=0.25.0",
    "pytest-cov>=6.0.0",
    "tomli>=2.0.0",
    "tomli-w>=1.0.0",
    "packaging>=21.0.0",
]

[build-system]
requires = [
    "setuptools>=61.0",
]
build-backend = "setuptools.build_meta"

[tool.setuptools]
py-modules = []

[tool.ruff]
target-version = "py310"
line-length = 100
exclude = [
    ".git",
    ".venv",
    "__pycache__",
    "*.egg-info",
    ".ruff_cache",
    "dist",
    "build",
]

[tool.ruff.lint]
select = [
    "E",
    "W",
    "F",
    "I",
    "N",
    "UP",
    "B",
    "C4",
    "SIM",
    "RUF",
]
ignore = [
    "E501",
    "N999",
]

[tool.ruff.lint.isort]
known-first-party = [
    "runpod-flash",
]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"

[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
ignore_missing_imports = true
strict_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
check_untyped_defs = true

[tool.pytest.ini_options]
minversion = "8.0"
testpaths = [
    "tests",
]
pythonpath = [
    ".",
]
asyncio_mode = "auto"
addopts = [
    "--strict-markers",
    "--strict-config",
    "-ra",
]

[tool.coverage.run]
source = [
    ".",
]
omit = [
    ".venv/*",
    "tests/*",
    "*/site-packages/*",
]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
    "if __name__ == .__main__.:",
    "if TYPE_CHECKING:",
]

```

### backend/ai_agents/generative/lucy_video_to_video/requirements.txt

```
# This file was autogenerated by uv via the following command:
#    uv pip compile pyproject.toml -o requirements.txt
aiodns==4.0.0
    # via aiohttp
aiohappyeyeballs==2.6.1
    # via aiohttp
aiohttp==3.13.3
    # via
    #   aiohttp-retry
    #   runpod
aiohttp-retry==2.9.1
    # via runpod
aiosignal==1.4.0
    # via aiohttp
annotated-doc==0.0.4
    # via fastapi
annotated-types==0.7.0
    # via pydantic
anyio==4.12.1
    # via
    #   httpx
    #   starlette
    #   watchfiles
attrs==25.4.0
    # via aiohttp
backoff==2.2.1
    # via runpod
backports-zstd==1.3.0
    # via aiohttp
bcrypt==5.0.0
    # via paramiko
boto3==1.42.41
    # via runpod
botocore==1.42.41
    # via
    #   boto3
    #   s3transfer
brotli==1.2.0
    # via aiohttp
certifi==2026.1.4
    # via
    #   httpcore
    #   httpx
    #   requests
    #   sentry-sdk
cffi==2.0.0
    # via
    #   cryptography
    #   pycares
    #   pynacl
charset-normalizer==3.4.4
    # via requests
click==8.3.1
    # via
    #   rich-toolkit
    #   runpod
    #   typer
    #   uvicorn
cloudpickle==3.1.2
    # via runpod-flash
colorama==0.4.6
    # via runpod
cryptography==45.0.7
    # via
    #   paramiko
    #   runpod
dnspython==2.8.0
    # via email-validator
email-validator==2.3.0
    # via
    #   fastapi
    #   pydantic
fastapi==0.128.0
    # via runpod
fastapi-cli==0.0.20
    # via fastapi
fastapi-cloud-cli==0.11.0
    # via fastapi-cli
fastar==0.8.0
    # via fastapi-cloud-cli
filelock==3.20.3
    # via runpod
frozenlist==1.8.0
    # via
    #   aiohttp
    #   aiosignal
h11==0.16.0
    # via
    #   httpcore
    #   uvicorn
httpcore==1.0.9
    # via httpx
httptools==0.7.1
    # via uvicorn
httpx==0.28.1
    # via
    #   fastapi
    #   fastapi-cloud-cli
idna==3.11
    # via
    #   anyio
    #   email-validator
    #   httpx
    #   requests
    #   yarl
inquirerpy==0.3.4
    # via runpod
invoke==2.2.1
    # via paramiko
itsdangerous==2.2.0
    # via fastapi
jinja2==3.1.6
    # via fastapi
jmespath==1.1.0
    # via
    #   boto3
    #   botocore
markdown-it-py==4.0.0
    # via rich
markupsafe==3.0.3
    # via jinja2
mdurl==0.1.2
    # via markdown-it-py
multidict==6.7.1
    # via
    #   aiohttp
    #   yarl
numpy==2.4.2
    # via runpod-flash-examples (pyproject.toml)
orjson==3.11.7
    # via fastapi
paramiko==4.0.0
    # via runpod
pathspec==1.0.4
    # via runpod-flash
pfzy==0.3.4
    # via inquirerpy
pillow==12.1.0
    # via runpod-flash-examples (pyproject.toml)
prettytable==3.17.0
    # via runpod
prompt-toolkit==3.0.52
    # via
    #   inquirerpy
    #   questionary
propcache==0.4.1
    # via
    #   aiohttp
    #   yarl
py-cpuinfo==9.0.0
    # via runpod
pycares==5.0.1
    # via aiodns
pycparser==3.0
    # via cffi
pydantic==2.12.5
    # via
    #   fastapi
    #   fastapi-cloud-cli
    #   pydantic-extra-types
    #   pydantic-settings
    #   runpod-flash
pydantic-core==2.41.5
    # via pydantic
pydantic-extra-types==2.11.0
    # via fastapi
pydantic-settings==2.12.0
    # via fastapi
pygments==2.19.2
    # via rich
pynacl==1.6.2
    # via paramiko
python-dateutil==2.9.0.post0
    # via botocore
python-dotenv==1.2.1
    # via
    #   pydantic-settings
    #   runpod-flash
    #   uvicorn
python-multipart==0.0.22
    # via
    #   runpod-flash-examples (pyproject.toml)
    #   fastapi
pyyaml==6.0.3
    # via
    #   fastapi
    #   uvicorn
questionary==2.1.1
    # via runpod-flash
requests==2.32.5
    # via runpod
rich==14.3.2
    # via
    #   rich-toolkit
    #   runpod-flash
    #   typer
rich-toolkit==0.18.1
    # via
    #   fastapi-cli
    #   fastapi-cloud-cli
rignore==0.7.6
    # via fastapi-cloud-cli
runpod==1.8.1
    # via runpod-flash
runpod-flash==1.0.0
    # via runpod-flash-examples (pyproject.toml)
s3transfer==0.16.0
    # via boto3
sentry-sdk==2.51.0
    # via fastapi-cloud-cli
shellingham==1.5.4
    # via typer
six==1.17.0
    # via python-dateutil
starlette==0.50.0
    # via fastapi
structlog==25.5.0
    # via runpod-flash-examples (pyproject.toml)
tomli==2.4.0
    # via runpod
tomlkit==0.14.0
    # via runpod
tqdm==4.67.3
    # via tqdm-loggable
tqdm-loggable==0.2
    # via runpod
typer==0.21.1
    # via
    #   fastapi-cli
    #   fastapi-cloud-cli
    #   runpod-flash
typing-extensions==4.15.0
    # via
    #   fastapi
    #   pydantic
    #   pydantic-core
    #   pydantic-extra-types
    #   rich-toolkit
    #   typer
    #   typing-inspection
typing-inspection==0.4.2
    # via
    #   pydantic
    #   pydantic-settings
ujson==5.11.0
    # via fastapi
urllib3==2.6.3
    # via
    #   botocore
    #   requests
    #   runpod
    #   sentry-sdk
uvicorn==0.40.0
    # via
    #   fastapi
    #   fastapi-cli
    #   fastapi-cloud-cli
uvloop==0.22.1
    # via uvicorn
watchdog==6.0.0
    # via runpod
watchfiles==1.1.1
    # via uvicorn
wcwidth==0.5.3
    # via
    #   prettytable
    #   prompt-toolkit
websockets==16.0
    # via uvicorn
yarl==1.22.0
    # via aiohttp

```

### backend/ai_agents/generative/lucy_video_to_video/lucy_run/requirements.txt

```
runpod-flash
fastapi
uvicorn
python-dotenv

```

### backend/ai_agents/generative/lucy_video_to_video/lucy_run/pyproject.toml

```
[project]
name = "lucy_run"
version = "0.1.0"
description = "Runpod Flash app for decart-ai/Lucy-Edit-Dev"
requires-python = ">=3.10"

dependencies = [
  "runpod-flash",
  "fastapi",
  "uvicorn",
  "python-dotenv",
]

```

### frontend/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "ADAPT — AI-Powered Hyperlocal Ads",
  description:
    "Generate hyper-localized ad variants with AI. Adjust backgrounds, scenery, and elements to match any demographic.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
        {children}
      </body>
    </html>
  );
}

```

### backend/app.py

```python
import csv
import json
import os
import shutil
import traceback
from typing import Annotated
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4

from dotenv import load_dotenv
from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile, Query
from fastapi.concurrency import run_in_threadpool
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
import numpy as np
from openai import OpenAI

from ai_agents.action_timeline import analyze_video
from ai_agents.group_ads import generate_group_variants
from ai_agents.market_research import run_market_research_agent
from auth import create_access_token, decode_token, hash_password, verify_password
from db import (
    add_variant,
    create_user,
    create_video,
    delete_variants_by_prefix,
    delete_video,
    get_user_by_email,
    get_user_by_id,
    get_video_with_variants,
    init_db,
    list_videos_for_user,
    update_video_analysis_url,
    update_video_metadata,
)
from cluster_profiles import _embed_texts, _kmeans

load_dotenv()

DEBUG = os.getenv("DEBUG", "0") == "1"
EMBEDDINGS_INPUT_TYPE = os.getenv("EMBEDDINGS_INPUT_TYPE", "CLUSTERING")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-5")
OLDER_AUDIENCE_AGE = int(os.getenv("OLDER_AUDIENCE_AGE", "55") or "55")

openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY") or "")

BASE_DIR = Path(__file__).resolve().parent
STORAGE_DIR = BASE_DIR / "storage"
TMP_DIR = STORAGE_DIR / "tmp"
ORIGINAL_DIR = STORAGE_DIR / "original"
PROCESSED_DIR = STORAGE_DIR / "processed"
ANALYSIS_DIR = STORAGE_DIR / "analysis"
PROFILES_DIR = STORAGE_DIR / "profiles"

for folder in (TMP_DIR, ORIGINAL_DIR, PROCESSED_DIR, ANALYSIS_DIR, PROFILES_DIR):
    folder.mkdir(parents=True, exist_ok=True)

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.mount("/media/original", StaticFiles(directory=str(ORIGINAL_DIR)), name="original")
app.mount("/media/processed", StaticFiles(directory=str(PROCESSED_DIR)), name="processed")
app.mount("/media/analysis", StaticFiles(directory=str(ANALYSIS_DIR)), name="analysis")

security = HTTPBearer()


def _coerce_int(value) -> int | None:
    try:
        return int(value)
    except (TypeError, ValueError):
        return None


def _has_embedding_env() -> bool:
    return bool(
        os.getenv("ELASTICSEARCH_ENDPOINT")
        and os.getenv("ELASTIC_API_KEY")
        and os.getenv("ELASTIC_INFERENCE_ID")
    )


def _has_openai_env() -> bool:
    return bool(os.getenv("OPENAI_API_KEY"))


def _strip_code_fences(text: str) -> str:
    if not text:
        return ""
    cleaned = text.strip()
    if cleaned.startswith("```"):
        cleaned = cleaned.replace("```json", "", 1).replace("```", "", 1)
    return cleaned.strip()


def _extract_json(text: str) -> dict:
    cleaned = _strip_code_fences(text)
    if not cleaned:
        return {}
    try:
        return json.loads(cleaned)
    except json.JSONDecodeError:
        start = cleaned.find("{")
        end = cleaned.rfind("}")
        if start != -1 and end != -1 and end > start:
            try:
                return json.loads(cleaned[start : end + 1])
            except json.JSONDecodeError:
                return {}
    return {}


def _truncate(text: str, limit: int = 180) -> str:
    if not text:
        return ""
    cleaned = text.strip()
    if len(cleaned) <= limit:
        return cleaned
    trimmed = cleaned[:limit].rsplit(" ", 1)[0]
    return (trimmed or cleaned[:limit]).rstrip() + "..."


def _parse_age(value: str | None) -> int | None:
    if not value:
        return None
    try:
        return int(value)
    except ValueError:
        return None


def _top_terms(entries: list[str], limit: int = 4) -> list[str]:
    terms: list[str] = []
    for entry in entries:
        for chunk in entry.split(";"):
            cleaned = chunk.strip().lower()
            if cleaned:
                terms.append(cleaned)
    if not terms:
        return []
    counts = {}
    for term in terms:
        counts[term] = counts.get(term, 0) + 1
    ranked = sorted(counts.items(), key=lambda item: item[1], reverse=True)
    return [item for item, _ in ranked[:limit]]


def _format_example(row: dict[str, str]) -> str:
    age = row.get("age") or ""
    gender = row.get("gender") or ""
    demo = row.get("demographic_info") or ""
    history = row.get("previous_search_history") or ""
    parts = []
    if age or gender:
        parts.append(", ".join([p for p in [age, gender] if p]))
    if demo:
        parts.append(demo)
    if history:
        shortened = history.strip()
        if len(shortened) > 80:
            shortened = shortened[:77].rsplit(" ", 1)[0] + "..."
        parts.append(shortened)
    return " | ".join([p for p in parts if p])


def _summarize_group_heuristic(members: list[dict[str, str]]) -> dict[str, object]:
    ages = [age for age in (_parse_age(m.get("age")) for m in members) if age is not None]
    avg_age = round(sum(ages) / len(ages)) if ages else None
    is_older = avg_age is not None and avg_age >= OLDER_AUDIENCE_AGE

    gender_counts: dict[str, int] = {}
    for member in members:
        gender = (member.get("gender") or "").strip().lower()
        if not gender:
            continue
        gender_counts[gender] = gender_counts.get(gender, 0) + 1
    top_genders = [g for g, _ in sorted(gender_counts.items(), key=lambda item: item[1], reverse=True)[:2]]

    demo_samples = []
    for member in members:
        demo = (member.get("demographic_info") or "").strip()
        if demo and demo not in demo_samples:
            demo_samples.append(demo)
        if len(demo_samples) >= 2:
            break

    interests = _top_terms([m.get("previous_search_history") or "" for m in members], limit=3)

    summary_parts = []
 
[truncated — 19325 more characters]
```

### frontend/app/page.tsx

```typescript
"use client";

import { useEffect, useRef, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Pricing } from "@/components/landing/pricing";
import { Input } from "@/components/ui/input";
import { setToken } from "@/lib/auth";
import { api } from "@/lib/api";

const ads = [
  { title: "Doritos", market: "SF", year: "2026", video: "/ads/doritos_sb.webm" },
  { title: "Nike", market: "NYC", year: "2026", video: "/ads/nike_sb.webm" },
  { title: "Pepsi", market: "Miami", year: "2026", video: "/ads/pepsi_sb.webm" },
  { title: "OpenAI", market: "SF", year: "2026", video: "/ads/openai_sb.webm" },
  { title: "Taco Bell", market: "Austin", year: "2026", video: "/ads/taco_bell_sb.webm" },
  { title: "Nike", market: "London", year: "2026", video: "/ads/nike_sb.webm" },
  { title: "Pepsi", market: "Tokyo", year: "2026", video: "/ads/pepsi_sb.webm" },
  { title: "Doritos", market: "Berlin", year: "2026", video: "/ads/doritos_sb.webm" },
  { title: "OpenAI", market: "Dublin", year: "2026", video: "/ads/openai_sb.webm" },
  { title: "Taco Bell", market: "Seoul", year: "2026", video: "/ads/taco_bell_sb.webm" },
];

const CARD_WIDTH = 600;
const X_START = 50;   
const X_STEP = -16;
const Y_START = 0;
const Y_STEP = 8;
const BASE_Z = 336;

function getCardStyle(index: number, scrollOffset: number, isHovered: boolean, isScrolling: boolean) {
  const N = ads.length;
  let relativePos = (index + scrollOffset) % N;
  if (relativePos < 0) relativePos += N;
  
  const xOffset = X_START + relativePos * X_STEP;
  const yOffset = Y_START + relativePos * Y_STEP;
  
  const zIndex = BASE_Z + Math.floor(relativePos); 

  return {
    position: "absolute" as const,
    left: "50%",
    top: `calc(30% + ${yOffset}rem)`, 
    width: `${CARD_WIDTH}px`,
    aspectRatio: "16 / 10",
    zIndex,
    transform: `translateX(calc(-50% + ${xOffset}rem)) translateY(${isHovered ? "calc(-50% - 1.5rem)" : "-50%"}) skewY(10deg) scale(${isHovered ? 1.04 : 1}) translateZ(0)`,
    willChange: "transform" as const,
    pointerEvents: isScrolling ? "none" as const : "auto" as const,
    cursor: "pointer",
    backgroundColor: "transparent",
    transition: isHovered ? "transform 0.4s cubic-bezier(0.4, 0, 0.2, 1)" : "none",
  };
}

export default function Home() {
  const router = useRouter();
  const [hoveredAd, setHoveredAd] = useState<number | null>(null);
  const [scrollOffset, setScrollOffset] = useState(0); 
  const [isScrolling, setIsScrolling] = useState(false);
  const [hasLeftHero, setHasLeftHero] = useState(false);
  const [showLogin, setShowLogin] = useState(false);
  const [loginMode, setLoginMode] = useState<"login" | "register">("login");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [loginError, setLoginError] = useState("");
  const [loginLoading, setLoginLoading] = useState(false);
  const scrollAccum = useRef(0);
  const scrollTimeout = useRef<NodeJS.Timeout | null>(null);

  const handleLoginSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoginError("");
    setLoginLoading(true);
    try {
      const response =
        loginMode === "login"
          ? await api.auth.login(email, password)
          : await api.auth.register(email, password);
      setToken(response.token);
      router.push("/console");
    } catch (err) {
      setLoginError(err instanceof Error ? err.message : "Something went wrong");
    } finally {
      setLoginLoading(false);
    }
  };

  const openLogin = () => setShowLogin(true);
  const closeLogin = () => {
    setShowLogin(false);
    setLoginError("");
    setEmail("");
    setPassword("");
  };

  useEffect(() => {
    const timer = setTimeout(() => {
        if (window.scrollY > window.innerHeight / 2) {
            setHasLeftHero(true);
        }
    }, 0);
    return () => clearTimeout(timer);
  }, []);

  useEffect(() => {
    const handleWheel = (e: WheelEvent) => {
      if (!hasLeftHero) {
         e.preventDefault();
         
         if (!isScrolling) setIsScrolling(true);
         if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
         
         scrollTimeout.current = setTimeout(() => {
           setIsScrolling(false);
         }, 150);

         scrollAccum.current += e.deltaY * 0.005; 
         setScrollOffset(scrollAccum.current);
         return;
      }
      
      if (window.scrollY <= window.innerHeight + 50 && e.deltaY < 0) {
          e.preventDefault();
          window.scrollTo({ top: window.innerHeight, behavior: "auto" });
      }
    };

    window.addEventListener("wheel", handleWheel, { passive: false });
    return () => window.removeEventListener("wheel", handleWheel);
  }, [isScrolling, hasLeftHero]);

  const scrollToSection = (id: string) => {
    const el = document.getElementById(id);
    if (el) {
      setHasLeftHero(true);
      el.scrollIntoView({ behavior: "smooth" });
    }
  };

  const scrollToTop = () => {
    window.scrollTo({ top: 0, behavior: "smooth" });
    setTimeout(() => {
        setHasLeftHero(false);
    }, 500);
  };

  return (
    <div className="min-h-screen bg-background overflow-x-hidden relative">
      {/* --- login modal --- */}
      {showLogin && (
        <div className="fixed inset-0 z-99999 bg-background/95 backdrop-blur-sm flex items-center justify-center">
          <div className="animate-modal-in w-full max-w-sm px-6">
            <button
              onClick={closeLogin}
              className="absolute top-6 right-8 text-muted hover:text-foreground transition-colors cursor-pointer"
            >
              <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5">
                <path d="M4 4l12 12M16 4L4 16" />
              </svg>
            </button>

            <div className="flex flex-col items-center text-center mb-10">
              <span className="text-sm font-medium tracking-widest uppercase">ADAPT</sp
[truncated — 12970 more characters]
```

### frontend/app/console/layout.tsx

```typescript
"use client";

import { useRouter } from "next/navigation";
import Link from "next/link";
import { clearToken } from "@/lib/auth";

export default function ConsoleLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const router = useRouter();

  const handleSignOut = () => {
    clearToken();
    router.push("/");
  };

  return (
    <div className="h-screen w-full overflow-hidden bg-background flex flex-col">
      <nav className="shrink-0 z-50 bg-background">
        <div className="w-full px-8 flex items-center justify-between h-14">
          <Link
            href="/"
            className="cursor-pointer text-sm font-medium tracking-widest uppercase hover:opacity-70 transition-opacity"
          >
            ADAPT
          </Link>
          <button
            onClick={handleSignOut}
            className="cursor-pointer font-mono text-[11px] uppercase tracking-widest text-muted hover:text-foreground transition-colors"
          >
            Sign out
          </button>
        </div>
      </nav>

      <main className="flex-1 min-h-0 overflow-hidden">
        {children}
      </main>
    </div>
  );
}

```

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