CoolFace
Apppublic

rubenppombo/birds-app

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
batch1.json2 linesDownload Raw Back to root
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