Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Deploy API to Cloud Run

on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:

env:
PROJECT_ID: jing-264314
REGION: europe-west1
API_SERVICE_NAME: video-logo-api
ARTIFACT_REGISTRY: europe-west1-docker.pkg.dev

jobs:
deploy-api:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker ${{ env.ARTIFACT_REGISTRY }}

- name: Build Docker image
working-directory: ./api
run: |
docker build -t ${{ env.ARTIFACT_REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_SERVICE_NAME }}/${{ env.API_SERVICE_NAME }}:${{ github.sha }} .
docker tag ${{ env.ARTIFACT_REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_SERVICE_NAME }}/${{ env.API_SERVICE_NAME }}:${{ github.sha }} \
${{ env.ARTIFACT_REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_SERVICE_NAME }}/${{ env.API_SERVICE_NAME }}:latest

- name: Push to Artifact Registry
run: |
docker push ${{ env.ARTIFACT_REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_SERVICE_NAME }}/${{ env.API_SERVICE_NAME }}:${{ github.sha }}
docker push ${{ env.ARTIFACT_REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_SERVICE_NAME }}/${{ env.API_SERVICE_NAME }}:latest

- name: Deploy to Cloud Run
run: |
gcloud run deploy ${{ env.API_SERVICE_NAME }} \
--image ${{ env.ARTIFACT_REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_SERVICE_NAME }}/${{ env.API_SERVICE_NAME }}:${{ github.sha }} \
--region ${{ env.REGION }} \
--allow-unauthenticated \
--service-account neurons-cloud-function@jing-264314.iam.gserviceaccount.com \
--set-secrets=/app/credentials/gcp-key.json=neurons-cloud-function-service-account-key:latest \
--set-env-vars GOOGLE_APPLICATION_CREDENTIALS=/app/credentials/gcp-key.json \
--max-instances 1

- name: Show deployment URL
run: |
echo "Service URL: $(gcloud run services describe ${{ env.API_SERVICE_NAME }} \
--region ${{ env.REGION }} \
--format 'value(status.url)')"

deploy-cloud-function-worker:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Deploy Cloud Function
working-directory: ./worker
run: |
gcloud functions deploy video-logo-worker \
--gen2 \
--runtime python312 \
--entry-point process_video \
--source . \
--memory 16GB \
--cpu 8 \
--max-instances 4 \
--service-account neurons-cloud-function@jing-264314.iam.gserviceaccount.com \
--region europe-west1 \
--timeout 60m \
--trigger-http \
--allow-unauthenticated
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
api/__pycache__/*
worker/__pycache__
senior_ai_engineer_assignment/*
results/*
test/*
.env
.envrc
adc.json
.DS_Store
logo_detection.db
*.pyc
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
# video-logo-tracker
# video-logo-tracker (code challenge)

This is a code challenge for a video logo tracker.

It consists of two parts:

1. API
2. Worker

## API

The API is a FastAPI application deployed on GCP cloud run that allows you to upload a video and a logo, and then process the video to detect the logo.

## Worker

The Worker is a Cloud Function deployed on GCP that processes the video to detect the logo using the SIFT algorithm.

Visit the APP
21 changes: 21 additions & 0 deletions api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM python:3.13-slim
EXPOSE 8080

WORKDIR /app

COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

ENV PYTHONUNBUFFERED=1 \
# Keeps Python from generating .pyc files in the container
PYTHONDONTWRITEBYTECODE=1 \
# Turns off buffering for easier container logging
UV_SYSTEM_PYTHON=1

COPY requirements.txt .

RUN uv pip install --system --no-cache -r requirements.txt

COPY . .


CMD ["uvicorn", "main:api", "--host", "0.0.0.0", "--port", "8080"]
26 changes: 26 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Video Logo Tracker API

FastAPI application for video logo processing with GCP integration.

## Run API Locally with Docker

1. **Download GCP credentials** and save as `adc.json` in `/api` directory.

```bash
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/adc.json
```

2. **Build and run**:

```bash
cd api
docker build -t api .
docker run -v $(pwd)/adc.json:/app/credentials/gcp-key.json:ro \
-e GOOGLE_APPLICATION_CREDENTIALS=/app/credentials/gcp-key.json \
-p 8000:8080 api
```

3. **Access API**:

-Production Docs: https://video-logo-api-606507129150.europe-west1.run.app/docs#
-Local Docs: http://localhost:8000/docs#
49 changes: 49 additions & 0 deletions api/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from fastapi import HTTPException


class InvalidVideoFileExtensionException(HTTPException):
def __init__(self, file_name: str):
super().__init__(
status_code=400,
detail=f"Invalid video file extension, must be .mp4 got {file_name}",
)


class InvalidImageFileExtensionException(HTTPException):
def __init__(self, file_name: str):
super().__init__(
status_code=400,
detail=f"Invalid image file extension, must be .jpg got {file_name}",
)


class MissingVideoFileException(HTTPException):
def __init__(self):
super().__init__(
status_code=400,
detail="Missing video file",
)


class MissingLogoFileException(HTTPException):
def __init__(self):
super().__init__(
status_code=400,
detail="Missing logo file",
)


class JobNotFoundException(HTTPException):
def __init__(self, job_id: str):
super().__init__(
status_code=404,
detail=f"Job not found with id {job_id}",
)


class InvalidGCSURLException(HTTPException):
def __init__(self, url: str):
super().__init__(
status_code=400,
detail=f"Invalid GCS URL: {url}",
)
29 changes: 29 additions & 0 deletions api/input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from pydantic import BaseModel

from typing import Optional

from models import JobStatus
from services.db import JobDB


class UploadInput(BaseModel):
video_file_name: str
logo_file_name: str


class JobPatchInput(BaseModel):
output_gcs_url: Optional[str] = None
status: JobStatus
error: Optional[str] = None

def to_job_db(self, job: JobDB) -> JobDB:
job.status = self.status
if self.output_gcs_url:
job.output_gcs_url = self.output_gcs_url
if self.error:
job.error = self.error

return job

def is_gcs_url(self, url: str) -> bool:
return url.startswith("gs://")
Loading