# Fansifter Clipper Demo Application

A full-stack web application that exposes the `fansifter_clipper` library as a multi-user project management UI.

## What it does

Users create a **project** by supplying a video (local file upload, Google Drive link, or a qualified Artist Social video). The app then runs a two-phase pipeline:

1. **Analyze** — a Celery worker calls the library's `VideoProcessor.analyze()`, which uploads the video to Gemini AI and returns ranked segment suggestions (start time, end time, relevance score). Results appear as a draggable timeline the user can review and adjust.
2. **Generate clips** — once the user confirms the segments, a second Celery task calls `VideoProcessor.reframe_and_render()`, which crops each segment to 9:16 using MediaPipe face detection and the scene-equilibrium algorithm. The finished clips are playable and downloadable directly from the UI.

Each phase is handled asynchronously; the frontend polls the project status every 5 seconds and advances through the states `uploading → analyzing → analysis_complete → processing → completed` (or `failed`).

A **mock mode** is available at the analyze step to generate random test segments without consuming Gemini API quota — useful for demoing the UI or testing the cropping pipeline in isolation.

A separate **Artist Socials** section manages a library of TikTok-style videos stored in a local folder tree (`TIKTOK_VIDEOS_DIR`). Videos are auto-discovered on first browse, analyzed by Gemini for qualification criteria, and surfaced in the New Project form as ready-to-clip sources. See [Artist Socials](#artist-socials) below for details.

## Technology Stack

- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) for the Python backend API
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com) for database ORM
- 💾 [PostgreSQL](https://www.postgresql.org) as the SQL database
- 📦 [Redis](https://redis.io) for Celery message broker
- ⚙️ [Celery](https://docs.celeryq.dev/) for async video processing tasks
- 🚀 [React](https://react.dev) with TypeScript for the frontend
- 🎨 [Tailwind CSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com) for UI components
- 🐋 [Docker Compose](https://www.docker.com) for containerized services
- 🎥 **fansifter-clipper** - Custom video processing library (OpenCV + MediaPipe + PyTorch)

## Running with Docker (Recommended for Video Processing)

Docker provides a stable Linux environment that fixes SIGSEGV crashes during video processing.

### Prerequisites

- Docker Desktop for Mac (or Docker Engine + Docker Compose on Linux)
- PostgreSQL running natively (or use a managed service)
  - The demo expects PostgreSQL on port 5433
  - Update `POSTGRES_SERVER` in `.env` if using a different setup

### Quick Start - Full Docker Mode

Run all services (backend API + Celery worker + Redis) in Docker:

```bash
cd demo-app

# Copy Docker environment config
cp .env.docker .env

# Update GEMINI_API_KEY in .env with your key

# Build and start all services
docker-compose up --build

# Or run in detached mode
docker-compose up --build -d
```

Services will be available at:
- **Backend API**: http://localhost:8000
- **API Docs**: http://localhost:8000/docs
- **Redis**: localhost:6379

To view logs:
```bash
# All services
docker-compose logs -f

# Worker only (to monitor video processing)
docker-compose logs -f celery_worker

# Backend only
docker-compose logs -f backend
```

To stop services:
```bash
docker-compose down

# Remove volumes as well
docker-compose down -v
```

### Hybrid Mode - Development Workflow (Recommended)

For the best development experience, run the backend API natively (for hot reload) but use Docker for the Celery worker (to fix SIGSEGV):

```bash
# Terminal 1: Start Redis and Worker in Docker
cd demo-app
docker-compose up redis celery_worker

# Terminal 2: Run Backend API natively with hot reload
cd demo-app/backend
uv run uvicorn app.main:app --reload --port 8000

# Terminal 3: Run Frontend natively
cd demo-app/frontend
npm install  # first time only
npm run dev
```

**Advantages of Hybrid Mode:**
- ✅ Backend changes reload instantly (no Docker rebuild)
- ✅ Video processing runs in stable Docker environment (no SIGSEGV)
- ✅ Frontend hot reload works as usual
- ✅ Easy debugging of API code

### Environment Configuration

The app uses `.env` file in the `demo-app/` directory.

**For Docker mode**, copy `.env.docker`:
```bash
cp .env.docker .env
```

**For Hybrid mode**, use the default `.env` but ensure:
```bash
# .env
REDIS_HOST=localhost  # Redis accessible from native backend
POSTGRES_SERVER=localhost
POSTGRES_PORT=5433
```

Key environment variables:
- `GEMINI_API_KEY` - Required for video analysis and Artist Socials qualification (get from Google AI Studio)
- `POSTGRES_SERVER` - Use `host.docker.internal` in Docker mode, `localhost` in hybrid mode
- `REDIS_HOST` - Use `redis` in full Docker mode, `localhost` in hybrid mode
- `TIKTOK_VIDEOS_DIR` - Optional; path to your Artist Socials folder (defaults to `demo-app/tiktok-videos`)

## Running Natively (Without Docker)

### Prerequisites

- Python 3.10+
- PostgreSQL 15+ (running on port 5433)
- Redis (running on port 6379)
- Node.js 18+ (for frontend)
- FFmpeg installed via homebrew: `brew install ffmpeg`

**Warning:** Native mode may experience SIGSEGV crashes on macOS during video processing for segments longer than 10-15 seconds. Use Docker mode if you encounter crashes.

### Backend Setup

```bash
cd demo-app/backend

# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install dependencies
uv sync

# Run database migrations
uv run alembic upgrade head

# Start the backend
uv run uvicorn app.main:app --reload --port 8000
```

### Celery Worker Setup (Separate Terminal)

```bash
cd demo-app/backend

# Start Celery worker
uv run celery -A app.core.celery_app worker --loglevel=info --concurrency=2
```

### Frontend Setup

```bash
cd demo-app/frontend

# Install dependencies
npm install

# Start development server
npm run dev
```

Frontend will be available at http://localhost:5173

### Redis Setup

```bash
# macOS
brew install redis
brew services start redis

# Or run in foreground
redis-server
```

## Using the Application

1. **Create a project**: Upload a video file (MP4, MOV, AVI, MKV, WEBM)
2. **Analyze video**: The app will detect faces and analyze segments
3. **Create segments**: Define time ranges for clips
4. **Generate clips**: Process segments into final cropped videos
   - For segments >15 seconds, use Docker mode to avoid crashes
5. **Download clips**: Get the final MP4 files

## Troubleshooting

### SIGSEGV Crashes During Video Processing

**Symptoms:**
```
[2026-02-09 13:42:37,210] Progress: 20% (54/272 frames)
[2026-02-09 13:42:40,167] Process 'ForkPoolWorker-11' pid:3216 exited with 'signal 11 (SIGSEGV)'
```

**Solutions:**

1. **Use Docker mode** (primary fix):
   ```bash
   cd demo-app
   docker-compose up celery_worker
   ```

2. **Increase memory limits** in `compose.yml`:
   ```yaml
   celery_worker:
     deploy:
       resources:
         limits:
           memory: 8G  # increase from 4G
   ```

3. **Decrease concurrency**:
   ```yaml
   celery_worker:
     command: >
       celery -A app.core.celery_app worker
       --concurrency=1  # decrease from 2
   ```

4. **Check logs**:
   ```bash
   docker-compose logs -f celery_worker
   ```

### Worker Not Processing Tasks

**Check Redis connection:**
```bash
docker-compose logs redis
docker-compose exec redis redis-cli ping  # should return PONG
```

**Check worker is running:**
```bash
docker-compose ps
docker-compose logs celery_worker
```

**Restart worker:**
```bash
docker-compose restart celery_worker

# Or rebuild if code changed
docker-compose up --build celery_worker
```

### Database Connection Issues

**In Docker mode**, ensure `POSTGRES_SERVER=host.docker.internal` in `.env`

**In hybrid mode**, ensure `POSTGRES_SERVER=localhost` in `.env`

**Verify PostgreSQL is running:**
```bash
psql -h localhost -p 5433 -U postgres -d app
```

### Frontend Can't Connect to Backend

- Ensure backend is running on port 8000
- Check CORS settings in `.env`: `BACKEND_CORS_ORIGINS` includes `http://localhost:5173`
- Verify `FRONTEND_HOST=http://localhost:5173` in `.env`

## Development

### Rebuilding Docker Images

After changing dependencies or Dockerfile:
```bash
docker-compose build --no-cache celery_worker
docker-compose up celery_worker
```

### Running Tests

```bash
cd demo-app/backend
uv run pytest
```

### Database Migrations

```bash
cd demo-app/backend

# Create new migration
uv run alembic revision --autogenerate -m "description"

# Apply migrations
uv run alembic upgrade head

# Rollback
uv run alembic downgrade -1
```

## Architecture

### Video Processing Flow

1. **Upload** → Video stored in `media/videos/`
2. **Analysis** → Celery task runs `fansifter-clipper` to detect faces and segments
3. **Segment Creation** → User defines time ranges in UI
4. **Clip Generation** → Celery task processes video frame-by-frame with crop interpolation
5. **Download** → Final clip stored in `media/clips/`


### Celery Worker Configuration

The worker in `compose.yml` has critical settings:
- `--concurrency=2` - Limit parallel video processing
- `--max-tasks-per-child=10` - Restart worker after 10 tasks (clears memory)
- `memory: 4G` - Hard memory limit
- `task_time_limit=1800` - 30 min timeout (in `celery_app.py`)

---

## Artist Socials

Artist Socials is a second video source alongside project uploads. TikTok-style videos are organized in a local folder tree and qualified by Gemini before they can be used as clip sources.

### Folder structure

```
tiktok-videos/          # TIKTOK_VIDEOS_DIR (default: demo-app/tiktok-videos)
├── artist-slug-1/
│   ├── video1.mp4
│   ├── video1.json     # optional TikTok metadata sidecar
│   ├── video2.mp4
│   └── video2.json
└── artist-slug-2/
    └── video3.mp4
```

Each `.json` sidecar may contain: `video_id`, `duration`, `create_time` (Unix timestamp), `author.nickname`, `author.unique_id`, `desc`, and `statistics.play_count`. The backend reads these fields to populate the `SocialVideo` database record; fields that are absent are stored as `null`.

### Auto-discovery and qualification flow

1. **Browse** — the frontend requests `GET /api/v1/social/artists/{slug}/videos/`. For each MP4 that has no database record yet, the backend creates a `SocialVideo` row (status `PROCESSING`) and enqueues an `analyze_social_video_task` Celery task.
2. **Gemini qualification** — the task downsamples the video and uploads it to the Gemini Files API. The model checks two criteria:
   - No on-screen text, captions, subtitles, lyrics, watermarks, or TikTok stickers burned into the frame.
   - At least one scene showing a person from head to knees (3/4 body or fuller).
   Both must pass. The result (`QUALIFIED` or `DISQUALIFIED`) and an optional one-sentence disqualification reason are written back to the database.
3. **Retry** — if a task crashes before writing a result, the record stays in `PENDING`. Re-fetching the artist's video list automatically re-enqueues the task.
4. **Frontend polling** — the artist detail page polls every 3 seconds while any video is in `PENDING` or `PROCESSING` state.

### Using qualified videos in a project

On the New Project form, the **Artist Socials** tab lists qualified videos grouped by artist in a collapsible tree. Selecting a video sets `source_type: ARTIST_SOCIAL` and `social_video_id` on the project; no file upload is needed. From that point, the project runs the same two-phase analyze → clip pipeline as any other source.

### API endpoints

| Method | Path | Description |
|---|---|---|
| GET | `/api/v1/social/artists/` | List all artists found in `TIKTOK_VIDEOS_DIR` |
| GET | `/api/v1/social/artists/{slug}/videos/` | List videos for an artist; triggers discovery |
| GET | `/api/v1/social/videos/qualified/` | All qualified videos across all artists |
| GET | `/api/v1/social/videos/{id}/stream` | Stream MP4 with HTTP range request support |

---

## ECS Fargate Deployment

`demo-app/Dockerfile.fargate` builds a single container that runs nginx (port 8080), the FastAPI backend, and the Celery worker together via supervisord. Build context is the project root.

### Build

```bash
# From project root
docker build -f demo-app/Dockerfile.fargate -t fansifter-fargate .
```

### Infrastructure assumptions

- **PostgreSQL** — provisioned separately (e.g. RDS). The container does not run its own database.
- **Redis** — provisioned separately (e.g. ElastiCache). Used as Celery broker and result backend.
- **Amazon EFS** — provisioned separately and mounted into the ECS task at a path of your choice (e.g. `/mnt/efs`). Set `MEDIA_ROOT` to that path so all video uploads and generated clips land on EFS instead of ephemeral container storage.

### Environment variables for the ECS task definition

All configuration is injected via environment variables. Define these in your Terraform `aws_ecs_task_definition` `container_definitions`.

#### Required

| Variable | Example value | Description |
|---|---|---|
| `POSTGRES_SERVER` | `mydb.cluster.us-east-1.rds.amazonaws.com` | RDS hostname |
| `POSTGRES_PORT` | `5432` | RDS port (default 5432) |
| `POSTGRES_USER` | `postgres` | Database user |
| `POSTGRES_PASSWORD` | `<secret>` | Database password |
| `POSTGRES_DB` | `app` | Database name |
| `REDIS_HOST` | `myredis.cache.amazonaws.com` | ElastiCache primary endpoint |
| `REDIS_PORT` | `6379` | Redis port (default 6379) |
| `SECRET_KEY` | `<random 32+ char string>` | JWT signing key — generate with `openssl rand -hex 32` |
| `FIRST_SUPERUSER` | `admin@example.com` | Admin account created on first boot |
| `FIRST_SUPERUSER_PASSWORD` | `<secret>` | Admin account password |
| `PROJECT_NAME` | `Fansifter Clipper` | Displayed in API docs and emails |
| `ENVIRONMENT` | `prod` | One of `dev`, `qa`, `prod` in deployed environments |
| `GEMINI_API_KEY` | `AIza...` | Google Gemini API key (from Google AI Studio) |
| `MEDIA_ROOT` | `/mnt/efs` | **Must match the EFS container mount path.** All uploaded videos and generated clips are stored here. |
| `FRONTEND_HOST` | `https://app.example.com` | Public URL of the app — used in CORS and email links |

#### Optional

| Variable | Default | Description |
|---|---|---|
| `GEMINI_MODEL` | `gemini-3-flash-preview` | Gemini model used for video analysis and social qualification |
| `TIKTOK_VIDEOS_DIR` | `demo-app/tiktok-videos` | Path to folder containing per-artist subfolders of MP4s for Artist Socials |
| `BACKEND_CORS_ORIGINS` | `""` | Comma-separated extra CORS origins |
| `SENTRY_DSN` | `""` | Sentry error-tracking DSN |
| `FFMPEG_PATH` | `/usr/bin/ffmpeg` | Already correct for the Linux image |
| `FFPROBE_PATH` | `/usr/bin/ffprobe` | Already correct for the Linux image |
| `MAX_UPLOAD_SIZE` | `2147483648` | Max video upload size in bytes (default 2 GB) |

### EFS volume wiring (Terraform example)

```hcl
resource "aws_ecs_task_definition" "fansifter" {
  # ...
  volume {
    name = "efs-media"
    efs_volume_configuration {
      file_system_id = aws_efs_file_system.media.id
      root_directory = "/"
    }
  }

  container_definitions = jsonencode([{
    name  = "fansifter"
    image = "..."
    portMappings = [{ containerPort = 8080 }]
    mountPoints = [{
      sourceVolume  = "efs-media"
      containerPath = "/mnt/efs"
      readOnly      = false
    }]
    environment = [
      { name = "MEDIA_ROOT", value = "/mnt/efs" },
      # ... other vars
    ]
  }])
}
```

### Push image to Amazon ECR

```bash
# Set to match your AWS account and preferred region
export AWS_ACCOUNT_ID=103233932089
export AWS_REGION=us-east-1
export ECR_REPO=fansifter-video-clipper

# Log Docker in to ECR
aws ecr get-login-password --region $AWS_REGION | \
  docker login --username AWS --password-stdin \
  $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com

# Build with both tags at once (layers uploaded once, two manifests written)
IMAGE_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO
SHA=$(git rev-parse --short HEAD)
docker build --platform linux/amd64 \
  -f demo-app/Dockerfile.fargate \
  -t ${IMAGE_URI}:${SHA} \
  -t ${IMAGE_URI}:latest \
  .

# Push all tags in a single command
docker push ${IMAGE_URI} --all-tags
```

Use `$IMAGE_URI:latest` (or the SHA tag for pinned deploys) as the `image` value in your Terraform ECS task definition.

### RDS database setup

Run the following SQL once against the fresh RDS instance using the **master credentials** you set when provisioning it (e.g. via `psql` or the RDS query editor).

```sql
-- 1. Create the application database
CREATE DATABASE app;

-- 2. Create the app user
--    Skip this step if your RDS master user is already named 'postgres'.
CREATE USER postgres WITH PASSWORD 'your-strong-password';

-- 3. Grant full access to the database
GRANT ALL PRIVILEGES ON DATABASE app TO postgres;

-- 4. Switch into the app database, then grant schema-level privileges
--    (required so Alembic can create and alter tables)
\c app
GRANT ALL ON SCHEMA public TO postgres;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO postgres;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres;
```

After this, set the ECS env vars:
```
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your-strong-password
POSTGRES_DB=app
```

Alembic migrations (`alembic upgrade head`) and initial data creation run automatically each time the container starts, so no manual schema setup is needed beyond this.

### Health check

The ALB target group health check should point to:
```
GET /api/v1/utils/health-check/
```
Port 8080, expected HTTP 200.