rubenppombo/birds-app
0
1[{"path": ".gitignore", "content": "# Python\n__pycache__/\n*.pyc\n*.pyo\n*.pyd\n.Python\nenv/\nvenv/\n.env\n.venv/\n.vscode/\n.idea/\n\n# Backend\nbackend/static/uploads/\n*.db\n*.sqlite3\n\n# Node\nnode_modules/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n# Frontend\nfrontend/dist/\nfrontend/.env\nfrontend/.env.local\nfrontend/.env.development.local\nfrontend/.env.test.local\nfrontend/.env.production.local\n\n# Logs\n*.log\nbackend.log\nfrontend.log\nfrontend_debug.log\nfrontend_out.log\nserver.log\n\n# OS\n.DS_Store\nThumbs.db\n\n# Other\n.gemini/"}, {"path": "ARCHITECTURE.md", "content": "# Architecture & Design\n\n## System Overview\n**Birds App** (physically located in `devops_birds`) is a web application designed to detect and classify Iberian bird species using a YOLO11 model. The system features a modern, nature-themed UI, user authentication, and a history of past analyses.\n\n## Tech Stack\n- **Frontend**: \n - React 19 (Vite 7)\n - Tailwind CSS 3.4 (Styling)\n - Lucide React (Icons)\n - Axios (API Communication)\n - React Router DOM (Routing)\n- **Backend**: \n - Python 3.x (FastAPI)\n - Ultralytics YOLO (Inference)\n - Pillow (Image Processing)\n - SQLite (Persistence for users, history, and shared links)\n - SQLAlchemy (ORM)\n - Passlib & JWT (Authentication)\n - Uvicorn (ASGI Server)\n\n## Data Flow\n1. **Auth**: User registers/logs in. JWT token returned and stored in client.\n2. **Upload**: User uploads an image via the React frontend.\n3. **Inference**: Image is sent to the FastAPI backend (`POST /api/detect`).\n4. **Processing**: \n - Backend loads the YOLO model.\n - Performs inference.\n - Generates visualization.\n5. **Persistence**:\n - Result is saved to `history` table linked to `user_id`.\n - Image stored in `static/uploads`.\n6. **Result**: Backend returns processed image and metadata.\n7. **History**: Sidebar fetches user's past queries from `GET /api/history`.\n\n## Database Schema (SQLite)\n- **User**\n - `id` (UUID/Integer)\n - `username` (String, Unique)\n - `password_hash` (String)\n - `created_at` (DateTime)\n- **SharedResult/History**\n - `id` (UUID)\n - `user_id` (ForeignKey -> User.id, nullable for anonymous shares?)\n - `image_filename` (String)\n - `metadata_json` (Text)\n - `created_at` (DateTime)\n\n## Directory Structure\n```\n/\n\u251c\u2500\u2500 backend/\n\u2502 \u251c\u2500\u2500 app/\n\u2502 \u2502 \u251c\u2500\u2500 main.py\n\u2502 \u2502 \u251c\u2500\u2500 model.py\n\u2502 \u2502 \u251c\u2500\u2500 database.py\n\u2502 \u2502 \u251c\u2500\u2500 models.py # DB Models (User, History)\n\u2502 \u2502 \u251c\u2500\u2500 auth.py # Auth logic\n\u2502 \u2502 \u2514\u2500\u2500 api.py\n\u2502 \u251c\u2500\u2500 static/\n\u2502 \u251c\u2500\u2500 iberbirds.db\n\u2502 \u2514\u2500\u2500 requirements.txt\n\u251c\u2500\u2500 frontend/\n\u2502 \u251c\u2500\u2500 src/\n\u2502 \u2502 \u251c\u2500\u2500 context/ # AuthContext\n\u2502 \u2502 \u251c\u2500\u2500 components/ # Sidebar, Navbar\n\u2502 \u2502 \u251c\u2500\u2500 pages/ # Login, Register, Home\n\u2502 \u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 models/\n\u2502 \u2514\u2500\u2500 best.pt\n\u251c\u2500\u2500 ARCHITECTURE.md\n\u251c\u2500\u2500 PROGRESS.md\n\u2514\u2500\u2500 README.md\n```\n\n## Future Roadmap\n- **Containerization**: Dockerize Backend and Frontend.\n- **CI/CD**: GitHub Actions.\n- **Monitoring**: Prometheus/Grafana."}, {"path": "PROGRESS.md", "content": "# Development Roadmap\n\n## Phase 1: Foundation & Setup\n- [x] **Project Initialization**: Set up directory structure, create Python virtual environment, install backend dependencies (FastAPI, Ultralytics), and initialize React app.\n- [x] **Backend - Model Integration**: Implement `ModelService` to load `models/best.pt` and run inference.\n- [x] **Backend - API Basic**: Create `POST /predict` endpoint to accept images and return raw detection data.\n\n## Phase 2: Core Functionality (The Loop)\n- [x] **Frontend - Basic UI**: Create a minimalist, nature-themed landing page with a drag-and-drop file upload.\n- [x] **Integration - Inference**: Connect the Frontend upload to the Backend API and display the result.\n- [x] **Visualization**: Render bounding boxes/processed image.\n- [x] **Feature - Download**: Implement download functionality.\n- [x] **Feature - Sharing (Backend)**: Create database models and endpoints to save results.\n- [x] **Feature - Sharing (Frontend)**: Add \"Share\" button and route.\n\n## Phase 3: UI/UX & Redesign (Completed)\n- [x] **Landing Page Redesign**: Implemented a cinematic \"Glassmorphism\" landing page with a separate Dashboard.\n- [x] **Species Showcase**: Created a detailed card grid for the 10 detected species using custom illustrations.\n- [x] **Assets Integration**: Migrated high-quality images and bird illustrations to the frontend assets.\n- [x] **Internationalization (i18n)**: Implemented full English/Spanish translation with a minimalist toggle.\n- [x] **Theme Consistency**: Unified the \"Earthy Organic\" design system across all pages (Landing, Dashboard, Login, Register, SharedResult).\n\n## Phase 4: Authentication & User Experience\n- [x] **Backend - Auth Foundation**:\n - [x] Update `requirements.txt` (passlib, python-jose, python-multipart).\n - [x] Create `User` model in `database.py`.\n - [x] Implement password hashing and JWT token generation in `auth.py`.\n - [x] Create API routes: `POST /api/register`, `POST /api/token` (login).\n- [x] **Backend - History Feature**:\n - [x] Update `SharedResult` model to link to `User` (foreign key).\n - [x] Create `GET /api/history` endpoint to fetch user's past detections.\n- [x] **Frontend - Auth Pages**:\n - [x] Create `Login.jsx` and `Register.jsx`.\n - [x] Implement `AuthContext` to manage login state and token storage.\n- [x] **Frontend - Sidebar & Navigation**:\n - [x] Create a `Sidebar` component to display history.\n - [x] Update `Home.jsx` to show \"Log in to save\" if guest.\n - [x] Connect Sidebar to `GET /api/history`.\n\n## Phase 5: DevOps & Production Readiness (Current)\n- [ ] **Containerization Assessment**: Evaluate Dockerization strategy for Backend (FastAPI + YOLO) and Frontend (Vite).\n- [ ] **Docker Implementation**: Create Dockerfiles and docker-compose.yml.\n- [ ] **Cloud Deployment**: Deploy the full stack (likely Vercel for Frontend, Render/Railway for Backend due to GPU/Model requirements).\n- [ ] **CI/CD Pipeline**: GitHub Actions for automated testing and deployment.\n- [ ] **Observability**: Prometheus & Grafana integration.\n"}, {"path": "README.md", "content": "# IBERBIRDS\n\nIBERBIRDS is a web application that uses a custom-trained YOLO11 model to detect and classify 10 specific bird species found in the Iberian Peninsula.\n\n## Features\n- **Bird Detection**: Detects 10 Iberian bird species using YOLO11.\n- **Modern UI**: Nature-themed, minimalist interface with drag-and-drop upload.\n- **Visualization**: Displays bounding boxes and confidence scores directly on the image.\n- **Sharing**: Generate unique shareable links for your discoveries.\n- **Download**: Download analyzed images with results.\n- **Responsive**: Optimized for Desktop and Mobile.\n\n## Bird Species Detected\n1. Ciconia ciconia\n2. Ciconia nigra\n3. Aegypius monachus\n4. Gyps fulvus\n5. Milvus milvus\n6. Milvus migrans\n7. Neophron percnopterus\n8. Falco peregrinus\n9. Aquila chrysaetos\n10. Aquila adalberti\n\n## Project Structure\n- `backend/`: FastAPI application and YOLO inference logic.\n- `frontend/`: React + Vite + Tailwind CSS application.\n- `models/`: Contains the trained YOLO model (`best.pt`).\n\n## Setup & Running\n\n### Prerequisites\n- Python 3.10+\n- Node.js 18+\n\n### Quick Start\n\n1. **Backend**:\n ```bash\n # Create virtual environment if not exists\n python3 -m venv .venv\n source .venv/bin/activate\n pip install -r backend/requirements.txt\n \n # Run server\n chmod +x run_backend.sh\n ./run_backend.sh\n ```\n The backend API runs at `http://localhost:8000`.\n\n2. **Frontend**:\n ```bash\n cd frontend\n npm install\n npm run dev\n ```\n Access the app at `http://localhost:5173`."}, {"path": "api/index.py", "content": "import sys\nimport os\n\n# Add the project root to sys.path\nsys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n\nfrom backend.app.main import app\n"}, {"path": "backend/app/api.py", "content": "from fastapi import APIRouter, UploadFile, File, HTTPException, Depends, status\nfrom fastapi.security import OAuth2PasswordRequestForm\nfrom PIL import Image\nimport io\nimport base64\nimport os\nimport uuid\nimport json\nfrom pydantic import BaseModel\nfrom sqlalchemy.orm import Session\nfrom .model import model_service\nfrom .database import get_db, SharedResult, User\nfrom . import auth\n\nrouter = APIRouter()\n\nclass ShareRequest(BaseModel):\n image_base64: str\n detections: list\n\nclass UserCreate(BaseModel):\n username: str\n password: str\n\nclass Token(BaseModel):\n access_token: str\n token_type: str\n\n@router.post(\"/register\", response_model=Token)\ndef register(user_data: UserCreate, db: Session = Depends(get_db)):\n # Check if user exists\n existing_user = db.query(User).filter(User.username == user_data.username).first()\n if existing_user:\n raise HTTPException(status_code=400, detail=\"Username already registered\")\n \n # Create user\n user_id = str(uuid.uuid4())\n hashed_password = auth.get_password_hash(user_data.password)\n new_user = User(\n id=user_id,\n username=user_data.username,\n password_hash=hashed_password\n )\n db.add(new_user)\n db.commit()\n db.refresh(new_user)\n \n access_token = auth.create_access_token(data={\"sub\": new_user.username})\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\n@router.post(\"/token\", response_model=Token)\ndef login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):\n user = db.query(User).filter(User.username == form_data.username).first()\n if not user or not auth.verify_password(form_data.password, user.password_hash):\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Incorrect username or password\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n \n access_token = auth.create_access_token(data={\"sub\": user.username})\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\n@router.post(\"/detect\")\nasync def detect(file: UploadFile = File(...)):\n if not model_service:\n raise HTTPException(status_code=500, detail=\"Model not loaded\")\n\n # Validate image\n if file.content_type not in [\"image/jpeg\", \"image/png\", \"image/webp\", \"image/jpg\"]:\n raise HTTPException(status_code=400, detail=f\"Invalid image type: {file.content_type}\")\n\n try:\n contents = await file.read()\n image = Image.open(io.BytesIO(contents)).convert(\"RGB\")\n \n result = model_service.predict(image)\n \n # Generate plotted image (BGR -> RGB -> Base64)\n im_array = result.plot() # Returns BGR numpy array\n im_pil = Image.fromarray(im_array[..., ::-1]) # Convert BGR to RGB\n \n buffered = io.BytesIO()\n im_pil.save(buffered, format=\"JPEG\")\n img_str = base64.b64encode(buffered.getvalue()).decode(\"utf-8\")\n \n # Parse results\n detections = []\n # result.names is a dict mapping class_id to name\n names = result.names\n \n for box in result.boxes:\n cls_id = int(box.cls[0])\n detections.append({\n \"class_id\": cls_id,\n \"class_name\": names[cls_id],\n \"confidence\": float(box.conf[0]),\n \"bbox\": box.xyxy[0].tolist() # [x1, y1, x2, y2]\n })\n \n return {\n \"detections\": detections,\n \"image_base64\": img_str\n }\n\n except Exception as e:\n print(f\"Error processing image: {e}\")\n raise HTTPException(status_code=500, detail=str(e))\n\n@router.post(\"/share\")\ndef share_result(request: ShareRequest, db: Session = Depends(get_db), current_user: User = Depends(auth.get_current_user)):\n # Note: I changed this to require login because otherwise it's not \"SharedResult/History\" linked to User\n # But for public sharing we might want a different endpoint or handle None user.\n # Given requirements say \"Update SharedResult model to link to User\", and Sidebar needs History.\n # I'll make it optionally authenticated for public shares, but if auth is present, link it.\n # Actually, the requirement says \"sidebar fetches user's past queries\".\n # Let's check how auth.get_current_user behaves. It raises if token is missing.\n # I'll use a wrapper or manual check if I want it optional.\n # For now, let's strictly follow the \"authenticated\" path for history.\n \n try:\n # Generate ID\n share_id = str(uuid.uuid4())\n filename = f\"{share_id}.jpg\"\n \n # Path logic\n base_dir = os.path.dirname(os.path.abspath(__file__))\n upload_dir = os.path.join(base_dir, \"..\", \"static\", \"uploads\")\n if not os.path.exists(upload_dir):\n os.makedirs(upload_dir)\n \n filepath = os.path.join(upload_dir, filename)\n \n # Save image\n img_data_str = request.image_base64\n if \",\" in img_data_str:\n img_data_str = img_data_str.split(\",\")[1]\n \n img_data = base64.b64decode(img_data_str)\n \n with open(filepath, \"wb\") as f:\n f.write(img_data)\n \n # Save to DB\n db_entry = SharedResult(\n id=share_id,\n user_id=current_user.id if current_user else None,\n image_filename=f\"uploads/{filename}\",\n metadata_json=json.dumps(request.detections)\n )\n db.add(db_entry)\n db.commit()\n db.refresh(db_entry)\n \n return {\"id\": share_id, \"url\": f\"/static/uploads/{filename}\"}\n \n except Exception as e:\n print(f\"Share error: {e}\")\n raise HTTPException(status_code=500, detail=\"Failed to share result\")\n\n@router.get(\"/history\")\ndef get_history(db: Session = Depends(get_db), current_user: User = Depends(auth.get_current_user)):\n results = db.query(SharedResult).filter(SharedResult.user_id == current_user.id).order_by(SharedResult.created_at.desc()).all()\n \n return [\n {\n \"id\": r.id,\n \"image_url\": f\"/static/{r.image_filename}\",\n \"detections\": json.loads(r.metadata_json),\n \"created_at\": r.created_at\n } for r in results\n ]\n\n@router.get(\"/share/{share_id}\")\ndef get_shared_result(share_id: str, db: Session = Depends(get_db)):\n result = db.query(SharedResult).filter(SharedResult.id == share_id).first()\n if not result:\n raise HTTPException(status_code=404, detail=\"Shared result not found\")\n \n return {\n \"id\": result.id,\n \"image_url\": f\"/static/{result.image_filename}\",\n \"detections\": json.loads(result.metadata_json),\n \"created_at\": result.created_at\n }"}, {"path": "backend/app/auth.py", "content": "from datetime import datetime, timedelta\nfrom typing import Optional\nfrom jose import JWTError, jwt\nfrom passlib.context import CryptContext\nfrom fastapi import Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer\nfrom sqlalchemy.orm import Session\nfrom . import database\n\n# Configuration (In a real app, use environment variables)\nSECRET_KEY = \"birds-app-secret-key-replace-me\"\nALGORITHM = \"HS256\"\nACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # 24 hours\n\n# Use argon2 instead of bcrypt to avoid version conflicts\npwd_context = CryptContext(schemes=[\"argon2\"], deprecated=\"auto\")\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"api/token\")\n\ndef verify_password(plain_password, hashed_password):\n return pwd_context.verify(plain_password, hashed_password)\n\ndef get_password_hash(password):\n return pwd_context.hash(password)\n\ndef create_access_token(data: dict, expires_delta: Optional[timedelta] = None):\n to_encode = data.copy()\n if expires_delta:\n expire = datetime.utcnow() + expires_delta\n else:\n expire = datetime.utcnow() + timedelta(minutes=15)\n to_encode.update({\"exp\": expire})\n encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)\n return encoded_jwt\n\ndef get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(database.get_db)):\n credentials_exception = HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n try:\n payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])\n username: str = payload.get(\"sub\")\n if username is None:\n raise credentials_exception\n except JWTError:\n raise credentials_exception\n user = db.query(database.User).filter(database.User.username == username).first()\n if user is None:\n raise credentials_exception\n return user\n"}, {"path": "backend/app/database.py", "content": "from sqlalchemy import create_engine, Column, String, Text, DateTime, ForeignKey\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import sessionmaker, relationship\nimport datetime\nimport os\n\nBase = declarative_base()\n\nclass User(Base):\n __tablename__ = 'users'\n \n id = Column(String, primary_key=True) # UUID\n username = Column(String, unique=True, index=True, nullable=False)\n password_hash = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.datetime.utcnow)\n \n results = relationship(\"SharedResult\", back_populates=\"user\")\n\nclass SharedResult(Base):\n __tablename__ = 'shared_results'\n \n id = Column(String, primary_key=True) # UUID\n user_id = Column(String, ForeignKey('users.id'), nullable=True) # Optional for now\n image_filename = Column(String, nullable=False)\n metadata_json = Column(Text, nullable=False) # JSON string of detections\n created_at = Column(DateTime, default=datetime.datetime.utcnow)\n \n user = relationship(\"User\", back_populates=\"results\")\n\n# Database setup\n# Store db in backend directory (parent of app)\nBASE_DIR = os.path.dirname(os.path.abspath(__file__))\nDB_PATH = os.path.join(BASE_DIR, \"..\", \"iberbirds.db\")\nSQLALCHEMY_DATABASE_URL = f\"sqlite:///{DB_PATH}\"\n\nengine = create_engine(\n SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False}\n)\nSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\ndef init_db():\n Base.metadata.create_all(bind=engine)\n\ndef get_db():\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n"}, {"path": "backend/app/main.py", "content": "from fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.staticfiles import StaticFiles\nfrom .api import router\nfrom .database import init_db\nimport os\n\napp = FastAPI(title=\"IberBirds API\")\n\n# Initialize DB\ninit_db()\n\n# Mount Static\n# Resolve backend/static relative to backend/app/main.py\nbase_dir = os.path.dirname(os.path.abspath(__file__))\nstatic_dir = os.path.join(base_dir, \"..\", \"static\")\nif not os.path.exists(static_dir):\n os.makedirs(static_dir)\n\napp.mount(\"/static\", StaticFiles(directory=static_dir), name=\"static\")\n\n# Configure CORS\napp.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"], # Allow all for development\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n\napp.include_router(router, prefix=\"/api\")\n\n@app.get(\"/health\")\ndef health_check():\n return {\"status\": \"ok\"}"}, {"path": "backend/app/model.py", "content": "import os\nfrom ultralytics import YOLO\nfrom PIL import Image\nfrom typing import Any\n\nclass ModelService:\n def __init__(self, model_path: str = None):\n if model_path is None:\n # Resolve path relative to this file: ../../models/best.pt\n base_dir = os.path.dirname(os.path.abspath(__file__))\n # Adjust path traversal based on project structure\n # app/model.py -> app/ -> backend/ -> root/ -> models/best.pt\n model_path = os.path.join(base_dir, \"..\", \"..\", \"models\", \"best.pt\")\n \n self.model_path = os.path.abspath(model_path)\n \n if not os.path.exists(self.model_path):\n raise FileNotFoundError(f\"Model file not found at: {self.model_path}\")\n \n print(f\"Loading model from: {self.model_path}\")\n self.model = YOLO(self.model_path)\n\n def predict(self, image: Image.Image) -> Any:\n \"\"\"\n Run inference on a PIL Image.\n Returns the Ultralytics Results object.\n \"\"\"\n # Run inference\n # conf=0.25 is a standard default, can be adjusted\n results = self.model(image, conf=0.25) \n return results[0] # Return the first result (single image)\n\n# Singleton instance\ntry:\n model_service = ModelService()\nexcept Exception as e:\n print(f\"Failed to initialize ModelService: {e}\")\n model_service = None\n"}, {"path": "backend/requirements.txt", "content": "fastapi\nuvicorn\nultralytics\npillow\npython-multipart\nsqlalchemy\npasslib[bcrypt]\npython-jose[cryptography]\n"}, {"path": "backend/test_api_local.py", "content": "from fastapi.testclient import TestClient\nfrom app.main import app\nfrom PIL import Image\nimport io\nimport numpy as np\n\nclient = TestClient(app)\n\ndef test_health():\n response = client.get(\"/health\")\n assert response.status_code == 200\n assert response.json() == {\"status\": \"ok\"}\n print(\"Health check passed.\")\n\ndef test_predict_dummy():\n # Create dummy image\n img = Image.fromarray(np.zeros((640, 640, 3), dtype=np.uint8))\n buf = io.BytesIO()\n img.save(buf, format=\"JPEG\")\n buf.seek(0)\n \n response = client.post(\n \"/api/detect\",\n files={\"file\": (\"dummy.jpg\", buf, \"image/jpeg\")}\n )\n \n if response.status_code != 200:\n print(f\"Prediction failed: {response.text}\")\n \n assert response.status_code == 200\n data = response.json()\n assert \"detections\" in data\n assert isinstance(data[\"detections\"], list)\n print(\"Prediction test passed.\")\n\nif __name__ == \"__main__\":\n try:\n test_health()\n test_predict_dummy()\n print(\"ALL API TESTS PASSED\")\n except ImportError:\n print(\"httpx not installed, cannot use TestClient. Install httpx or use manual curl.\")\n # Fallback to check if we can import httpx\n import httpx\n except Exception as e:\n print(f\"Test failed: {e}\")\n import traceback\n traceback.print_exc()\n"}, {"path": "backend/test_auth.py", "content": "from fastapi.testclient import TestClient\nfrom app.main import app\nfrom app.database import Base, engine, SessionLocal\nimport uuid\n\n# Use the same DB for testing or a separate one? \n# For simplicity, we use the current one but with unique usernames.\n\nclient = TestClient(app)\n\ndef test_auth_flow():\n username = f\"testuser_{uuid.uuid4().hex[:8]}\"\n password = \"testpassword\"\n \n # 1. Register\n response = client.post(\n \"/api/register\",\n json={\"username\": username, \"password\": password}\n )\n assert response.status_code == 200\n data = response.json()\n assert \"access_token\" in data\n token = data[\"access_token\"]\n print(f\"Registration successful for {username}\")\n \n # 2. Login\n response = client.post(\n \"/api/token\",\n data={\"username\": username, \"password\": password}\n )\n assert response.status_code == 200\n assert \"access_token\" in response.json()\n print(\"Login successful\")\n \n # 3. Access protected history\n headers = {\"Authorization\": f\"Bearer {token}\"}\n response = client.get(\"/api/history\", headers=headers)\n assert response.status_code == 200\n assert isinstance(response.json(), list)\n print(\"History access successful\")\n\ndef test_invalid_login():\n response = client.post(\n \"/api/token\",\n data={\"username\": \"nonexistent\", \"password\": \"wrong\"}\n )\n assert response.status_code == 401\n print(\"Invalid login handled correctly\")\n\nif __name__ == \"__main__\":\n try:\n test_auth_flow()\n test_invalid_login()\n print(\"AUTH TESTS PASSED\")\n except Exception as e:\n print(f\"Auth test failed: {e}\")\n import traceback\n traceback.print_exc()\n"}, {"path": "backend/test_model_loading.py", "content": "import sys\nimport os\n\n# Add backend directory to sys.path so we can import app.model\nsys.path.append(os.path.join(os.path.dirname(__file__)))\n\ntry:\n from app.model import model_service\n from PIL import Image\n import numpy as np\n\n if model_service and model_service.model:\n print(\"SUCCESS: Model loaded successfully.\")\n \n # Create a dummy black image\n dummy_image = Image.fromarray(np.zeros((640, 640, 3), dtype=np.uint8))\n print(\"Running dummy prediction...\")\n result = model_service.predict(dummy_image)\n print(\"SUCCESS: Prediction ran without errors.\")\n print(f\"Result boxes: {len(result.boxes)}\")\n \n else:\n print(\"FAILURE: Model service initialized but model is None.\")\n sys.exit(1)\nexcept Exception as e:\n print(f\"FAILURE: Exception occurred: {e}\")\n sys.exit(1)\n"}, {"path": "backend/test_share_feature.py", "content": "from fastapi.testclient import TestClient\nimport sys\nimport os\n\n# Add backend directory to sys.path\nsys.path.append(os.path.join(os.path.dirname(__file__)))\n\nfrom app.main import app\n\nclient = TestClient(app)\n\ndef test_share_flow():\n # 1. Share dummy data\n # 1x1 pixel png\n dummy_base64 = \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=\" \n payload = {\n \"image_base64\": dummy_base64,\n \"detections\": [{\"class_name\": \"TestBird\", \"confidence\": 0.99}]\n }\n \n print(\"Sending POST /api/share...\")\n response = client.post(\"/api/share\", json=payload)\n if response.status_code != 200:\n print(f\"Share failed: {response.text}\")\n sys.exit(1)\n \n assert response.status_code == 200\n data = response.json()\n assert \"id\" in data\n share_id = data[\"id\"]\n print(f\"Shared ID: {share_id}\")\n \n # 2. Get shared data\n print(f\"Sending GET /api/share/{share_id}...\")\n response = client.get(f\"/api/share/{share_id}\")\n assert response.status_code == 200\n get_data = response.json()\n assert get_data[\"id\"] == share_id\n assert get_data[\"detections\"][0][\"class_name\"] == \"TestBird\"\n print(\"Share flow success!\")\n\nif __name__ == \"__main__\":\n test_share_flow()\n"}, {"path": "requirements.txt", "content": "fastapi\nuvicorn\nultralytics\npillow\npython-multipart\nsqlalchemy\npasslib[bcrypt]\npython-jose[cryptography]\n"}, {"path": "run_backend.sh", "content": "#!/bin/bash\n# Ensure we are in the project root\ncd \"$(dirname \"$0\")\"\n\n# Activate venv and run uvicorn\n./.venv/bin/uvicorn backend.app.main:app --host 0.0.0.0 --port 8000 --reload\n"}, {"path": "vercel.json", "content": "{\n \"rewrites\": [\n {\n \"source\": \"/api/(.*)\",\n \"destination\": \"/api/index.py\"\n }\n ]\n}"}]2 