diff --git a/.gitignore b/.gitignore index dd15ec4..5947ef2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,181 +1,29 @@ -# Byte-compiled / optimized / DLL files +# Python __pycache__/ *.py[cod] *$py.class - -# C extensions *.so - -# Distribution / packaging .Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ venv/ +env/ ENV/ -env.bak/ -venv.bak/ +.venv -# Spyder project settings -.spyderproject -.spyproject +# Environment variables +.env -# Rope project settings -.ropeproject +# IDE +.vscode/ +.idea/ +*.swp +*.swo -# mkdocs documentation -/site +# OS +.DS_Store +Thumbs.db -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - -# Cursor -# Cursor is an AI-powered code editor.`.cursorignore` specifies files/directories to -# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data -# refer to https://docs.cursor.com/context/ignore-files -.cursorignore -.cursorindexingignore \ No newline at end of file +# Project specific +*.png +*.jpg +*.jpeg +!*.example.* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b2aec0a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +# Use Python 3.11 slim image as base +FROM python:3.11-slim + +# ---- System setup ---- +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + FFMPEG_BIN=/usr/bin/ffmpeg \ + FFPROBE_BIN=/usr/bin/ffprobe + +# Set working directory +WORKDIR /app + +# Install system dependencies (ffmpeg + curl for healthcheck, build tools for pip) +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ffmpeg \ + curl \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt ./requirements.txt + +# Install Python dependencies +RUN python -m pip install --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Expose port +EXPOSE 8000 + +# Health check (uses curl we installed above) +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -fsS http://localhost:8000/health || exit 1 + +# Run the application +# For dev: keep --reload + volume mount +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + diff --git a/POSTMAN_SETUP.md b/POSTMAN_SETUP.md new file mode 100644 index 0000000..3cb1865 --- /dev/null +++ b/POSTMAN_SETUP.md @@ -0,0 +1,105 @@ +# Postman Setup Guide - Step by Step + +## Common Error: "Field required" for outfit_photo + +This error means Postman isn't sending the file correctly. Follow these exact steps: + +## ✅ Correct Setup in Postman + +### Step 1: Create the Request +1. Click **New** → **HTTP Request** +2. Set method to **POST** +3. Enter URL: `http://localhost:8000/api/try-on/` + +### Step 2: Configure Body (CRITICAL) +1. Click the **Body** tab +2. Select **form-data** (NOT raw, NOT x-www-form-urlencoded) +3. You should see a table with columns: Key | Value | Description + +### Step 3: Add First File Field +1. In the **Key** column, type exactly: `user_photo` +2. Click the dropdown on the right side of that row (it will say "Text" by default) +3. Change it to **File** (NOT Text!) +4. Click **Select Files** button that appears +5. Choose your user photo image file + +### Step 4: Add Second File Field +1. Click **Add Row** or just click in a new row +2. In the **Key** column, type exactly: `outfit_photo` +3. Click the dropdown on the right side (change from "Text" to **File**) +4. Click **Select Files** button +5. Choose your outfit photo image file + +### Step 5: Verify Setup +Your form-data should look like this: +``` +Key | Type | Value +-------------|------|---------- +user_photo | File | [your file name] +outfit_photo | File | [your file name] +``` + +### Step 6: Send Request +- Click **Send** +- You should receive a PNG image as the response + +## ❌ Common Mistakes + +1. **Using "Text" instead of "File"** - This is the #1 mistake! + - Wrong: Key = `outfit_photo`, Type = Text, Value = file path + - Right: Key = `outfit_photo`, Type = File, Value = [Select Files button] + +2. **Using wrong body type** + - Wrong: Body = raw (JSON) + - Wrong: Body = x-www-form-urlencoded + - Right: Body = form-data + +3. **Wrong field names** + - Must be exactly: `user_photo` and `outfit_photo` (case-sensitive) + +4. **Missing trailing slash** + - Wrong: `http://localhost:8000/api/try-on` + - Right: `http://localhost:8000/api/try-on/` + +## Quick Test: Health Check First + +Before testing try-on, verify the service is running: + +1. Create a GET request +2. URL: `http://localhost:8000/health` +3. Click Send +4. Should return: `{"status": "ok"}` + +If this fails, your service isn't running! + +## Visual Checklist + +When you look at your Postman request, you should see: + +- ✅ Method: POST +- ✅ URL: `http://localhost:8000/api/try-on/` +- ✅ Body tab selected +- ✅ "form-data" radio button selected (not raw, not x-www-form-urlencoded) +- ✅ Two rows in the form-data table +- ✅ First row: Key = `user_photo`, Type = **File** (dropdown shows "File") +- ✅ Second row: Key = `outfit_photo`, Type = **File** (dropdown shows "File") +- ✅ Both rows have file names in the Value column + +## Still Having Issues? + +1. **Check service is running:** + ```bash + # If using Docker: + docker-compose ps + + # If using Python directly: + # Make sure uvicorn is running + ``` + +2. **Check the service logs** for more details + +3. **Try the FastAPI docs:** + - Open browser: `http://localhost:8000/docs` + - Use the interactive Swagger UI to test the endpoint + - This will show you exactly what the API expects + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e14e970 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# AIFit Try-On Service + +A FastAPI microservice that uses Google's Gemini API to generate virtual try-on images. Takes a user mirror selfie and an outfit reference image, then returns an edited image with the outfit applied. + +## Setup + +1. **Install dependencies** (preferably in a virtual environment): + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + pip install -r requirements.txt + ``` + +2. **Set up environment variables**: + - Create a `.env` file in the project root (same directory as `main.py`) + - Add your Gemini API key with **NO spaces** around the `=` sign: + ``` + GEMINI_API_KEY=YOUR_REAL_KEY_HERE + ``` + - **Important**: + - No quotes around the value + - No spaces: `GEMINI_API_KEY=key` ✅ (correct) + - Wrong: `GEMINI_API_KEY = key` ❌ (has spaces) + - Wrong: `GEMINI_API_KEY="key"` ❌ (has quotes) + - The `.env` file should be in the same directory as `docker-compose.yml` + +## Running the Service + +### Option 1: Direct Python + +Start the FastAPI server: +```bash +uvicorn main:app --reload +``` + +### Option 2: Docker Compose + +Build and run with Docker: +```bash +docker-compose up --build +``` + +The API will be available at: +- API: http://localhost:8000 +- Interactive docs: http://localhost:8000/docs +- Alternative docs: http://localhost:8000/redoc +- Health check: http://localhost:8000/health + +## API Endpoints + +### GET `/health` + +Health check endpoint for monitoring and Docker healthchecks. + +**Response:** +```json +{"status": "ok"} +``` + +### POST `/api/try-on/` + +Generate a try-on image by combining a user photo with an outfit reference. + +**Parameters:** +- `user_photo` (file): User mirror/body photo (JPEG or PNG) +- `outfit_photo` (file): Collection/outfit photo (JPEG or PNG) + +**Response:** +- Returns the generated image as `image/png` + +**Example using curl:** +```bash +curl -X POST "http://localhost:8000/api/try-on/" \ + -F "user_photo=@user_selfie.jpg" \ + -F "outfit_photo=@outfit_reference.jpg" \ + --output result.png +``` + +**Testing in Postman:** + +1. **Health Check:** + - Method: `GET` + - URL: `http://localhost:8000/health` + - No body or headers needed + - Expected response: `{"status": "ok"}` + +2. **Try-On Endpoint:** + - Method: `POST` + - URL: `http://localhost:8000/api/try-on/` + - Body tab → Select `form-data` + - Add two fields: + - `user_photo` (type: **File**) → Select your user mirror/body photo + - `outfit_photo` (type: **File**) → Select your collection/outfit photo + - Click **Send** + - Response will be an image (PNG format) + - To save: Click "Save Response" → "Save to a file" + +**Import Postman Collection:** +You can import `postman_collection.json` into Postman for pre-configured requests. + +## Notes + +- The service uses `gemini-2.5-flash-image` model +- CORS is enabled for all origins (tighten in production) +- Images are validated before processing +- The service handles errors gracefully with appropriate HTTP status codes +- Docker healthcheck is configured to use the `/health` endpoint +- For production, remove `--reload` from the Dockerfile CMD and remove volume mounts from docker-compose.yml + diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..294680b --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,135 @@ +# Troubleshooting Guide + +## API Key Not Loading from .env + +### Step 1: Verify .env File Exists and Location + +The `.env` file must be in the **project root** (same directory as `docker-compose.yml` and `main.py`). + +Check if it exists: +```bash +# Windows PowerShell +Test-Path .env + +# Should return: True +``` + +### Step 2: Check .env File Format + +Your `.env` file should look exactly like this: + +``` +GEMINI_API_KEY=AIzaSyYourActualKeyHere123456789 +``` + +**Common mistakes:** +- ❌ `GEMINI_API_KEY = value` (spaces around =) +- ❌ `GEMINI_API_KEY="value"` (quotes) +- ❌ `GEMINI_API_KEY='value'` (single quotes) +- ❌ `GEMINI_API_KEY= value` (space after =) +- ✅ `GEMINI_API_KEY=value` (correct!) + +### Step 3: Test if API Key is Loaded + +1. **If using Docker:** + ```bash + docker-compose restart + docker-compose logs aifit-fastapi + ``` + Look for: `✓ GEMINI_API_KEY loaded: AIzaSy...` + +2. **Use the debug endpoint:** + - Open browser: `http://localhost:8000/debug/api-key-status` + - Or in Postman: GET `http://localhost:8000/debug/api-key-status` + - Should return: + ```json + { + "status": "ok", + "message": "API key is loaded", + "key_length": 39, + "key_preview": "AIzaSy...", + "key_ends_with": "xyz" + } + ``` + +### Step 4: If Using Docker - Restart After Creating .env + +If you created the `.env` file after starting Docker: + +1. Stop the container: + ```bash + docker-compose down + ``` + +2. Start again: + ```bash + docker-compose up --build + ``` + +Docker needs to be restarted to pick up new `.env` files. + +### Step 5: Alternative - Set Environment Variable Directly + +If `.env` still doesn't work, you can set it directly in `docker-compose.yml`: + +```yaml +environment: + - GEMINI_API_KEY=your_actual_key_here + # ... other vars +``` + +**⚠️ Warning:** Don't commit this to git! Use `.env` file instead. + +### Step 6: Verify API Key is Valid + +1. Check the key starts with `AIza` (most Gemini keys do) +2. Make sure there are no extra characters or line breaks +3. Try regenerating the key from Google AI Studio if needed + +### Step 7: Check Docker Logs + +```bash +docker-compose logs aifit-fastapi +``` + +Look for: +- `✓ GEMINI_API_KEY loaded: ...` = Good! +- `⚠ API key seems too short` = Key might be truncated +- `GEMINI_API_KEY not set` = File not found or wrong format + +## Quick Fix Checklist + +- [ ] `.env` file exists in project root +- [ ] `.env` file has exactly: `GEMINI_API_KEY=your_key` (no spaces, no quotes) +- [ ] Restarted Docker after creating/editing `.env` +- [ ] Checked `/debug/api-key-status` endpoint +- [ ] Verified API key is valid (starts with `AIza` typically) +- [ ] Checked Docker logs for loading confirmation + +## Still Not Working? + +1. **Test without Docker:** + ```bash + # Activate venv + python -m venv venv + venv\Scripts\activate # Windows + + # Install deps + pip install -r requirements.txt + + # Run directly (will use .env) + uvicorn main:app --reload + ``` + +2. **Check if python-dotenv is installed:** + ```bash + pip list | findstr dotenv + ``` + +3. **Manually set environment variable:** + ```bash + # Windows PowerShell + $env:GEMINI_API_KEY="your_key_here" + uvicorn main:app --reload + ``` + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ce78c2a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3.8' + +services: + aifit-fastapi: + build: + context: . + dockerfile: Dockerfile + container_name: aifit-fastapi + ports: + - "8000:8000" + volumes: + - .:/app # hot-reload for dev; remove in prod + env_file: + - .env # must contain GEMINI_API_KEY=... + environment: + - PYTHONPATH=/app + - FFMPEG_BIN=/usr/bin/ffmpeg + - FFPROBE_BIN=/usr/bin/ffprobe + # Explicitly pass GEMINI_API_KEY from .env (fallback if env_file doesn't work) + - GEMINI_API_KEY=${GEMINI_API_KEY} + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + diff --git a/main.py b/main.py new file mode 100644 index 0000000..df39ab5 --- /dev/null +++ b/main.py @@ -0,0 +1,261 @@ +import os +import json +from io import BytesIO +from pprint import pprint + +from fastapi import FastAPI, UploadFile, File, Form, HTTPException +from fastapi.responses import Response +from fastapi.middleware.cors import CORSMiddleware + +from dotenv import load_dotenv +from google import genai +from google.genai.types import Part +from PIL import Image + +# --- Load .env (for local development) --- +load_dotenv() + +# ---- Gemini SDK Setup ---- +GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") + +# Strip whitespace (common issue with .env files) +if GEMINI_API_KEY: + GEMINI_API_KEY = GEMINI_API_KEY.strip() + +if not GEMINI_API_KEY: + raise RuntimeError("GEMINI_API_KEY not set") + +client = genai.Client(api_key=GEMINI_API_KEY) +IMAGE_MODEL = "gemini-2.5-flash-image" + +app = FastAPI() + +# (Optional) CORS if you call it from frontend directly +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # tighten in production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +def find_first_image_part(resp): + """Return the actual output image part.""" + if not getattr(resp, "candidates", None): + return None + for cand in resp.candidates: + content = getattr(cand, "content", None) + if not content or not getattr(content, "parts", None): + continue + for p in content.parts: + if hasattr(p, "inline_data") and getattr(p.inline_data, "data", None): + return p.inline_data + if hasattr(p, "blob") and getattr(p.blob, "data", None): + return p.blob + return None + + +@app.get("/health") +def health(): + return {"status": "ok"} + + +@app.post("/api/try-on/", summary="Try-on (user_photo + outfit_photo + metadata)") +async def try_on( + user_photo: UploadFile = File(...), + outfit_photo: UploadFile = File(...), + metadata: str = Form("", description="JSON or text describing the outfit items"), +): + """ + metadata can be: + - free text: 'black hoodie, patterned shirt, white shorts' + - JSON: '{"outer": "hoodie", "inner": "shirt", "bottom": "shorts"}' + - any additional instructions + """ + + # ---- Read files ---- + user_bytes = await user_photo.read() + outfit_bytes = await outfit_photo.read() + + if not user_bytes: + raise HTTPException(status_code=400, detail="user_photo is empty") + if not outfit_bytes: + raise HTTPException(status_code=400, detail="outfit_photo is empty") + + # ---- Parse metadata if JSON ---- + metadata_text = "" + try: + meta = json.loads(metadata) + metadata_text = json.dumps(meta, indent=2) + except: + metadata_text = metadata.strip() + + # ---- Build Prompt ---- + prompt = f""" +EDIT TASK — FULL OUTFIT REPLACEMENT + +FIRST image = user body/mirror selfie +SECOND image = outfit reference + +Follow these rules: + +1. Edit ONLY the body/clothing of the FIRST image. + +2. Replace ALL clothing with the items visible in the SECOND image. + +3. Keep the user's: + - face + - skin + - hands + - phone + - pose + - environment + EXACTLY the same. + +4. IF a metadata description is given, use it to understand layers: + + Metadata: + {metadata_text} + +5. Ensure: + - realistic lighting + - correct fabric physics + - correct drape + - realism + - no blending of people + - no copying faces from the second image + +6. Output ONE final edited image. + +IMPORTANT: +- FIRST = user_photo +- SECOND = outfit_photo +""" + + # ---- Wrap image files ---- + user_part = Part.from_bytes(data=user_bytes, mime_type=user_photo.content_type or "image/jpeg") + outfit_part = Part.from_bytes(data=outfit_bytes, mime_type=outfit_photo.content_type or "image/jpeg") + + # ---- Call Gemini ---- + print(f"\n[DEBUG] Using model: {IMAGE_MODEL}") + print(f"[DEBUG] Metadata received: {metadata[:100] if metadata else '(empty)'}") + + try: + response = client.models.generate_content( + model=IMAGE_MODEL, + contents=[ + prompt, + user_part, + outfit_part, + "First image = user selfie. Second image = outfit reference.", + ], + ) + except Exception as e: + error_msg = str(e) + # Check if it's an API key error + if "API key" in error_msg or "INVALID_ARGUMENT" in error_msg or "API_KEY" in error_msg: + raise HTTPException( + status_code=401, + detail=( + f"Gemini API key error: {error_msg}. " + "Please verify your GEMINI_API_KEY in the .env file is correct." + ) + ) + # Log the full error for debugging + import traceback + print(f"[ERROR] Gemini API error: {error_msg}") + print(traceback.format_exc()) + raise HTTPException(status_code=502, detail=f"Gemini error: {error_msg}") + + # ---- Debug: Inspect response ---- + print("\n[DEBUG] === Gemini Response Analysis ===") + if not getattr(response, "candidates", None): + print("[DEBUG] No candidates at all. Full response:") + pprint(response) + else: + cand = response.candidates[0] + finish_reason = getattr(cand, "finish_reason", None) + print(f"[DEBUG] Finish reason: {finish_reason}") + print(f"[DEBUG] Safety ratings: {getattr(cand, 'safety_ratings', None)}") + + if hasattr(cand, "content") and getattr(cand.content, "parts", None): + print(f"[DEBUG] Parts in first candidate ({len(cand.content.parts)} total):") + for i, part in enumerate(cand.content.parts): + part_type = type(part).__name__ + print(f" Part {i}: type={part_type}") + + # Check for text + text = getattr(part, "text", None) + if text: + print(f" TEXT: {text[:300]}") + + # Check for inline_data + if hasattr(part, "inline_data"): + inline = part.inline_data + if inline: + data_size = len(getattr(inline, "data", b"")) + mime = getattr(inline, "mime_type", "unknown") + print(f" INLINE_DATA: mime={mime}, size={data_size} bytes") + + # Check for blob + if hasattr(part, "blob"): + blob = part.blob + if blob: + data_size = len(getattr(blob, "data", b"")) + mime = getattr(blob, "mime_type", "unknown") + print(f" BLOB: mime={mime}, size={data_size} bytes") + print("[DEBUG] === End Response Analysis ===\n") + + # ---- Extract output image ---- + img_part = find_first_image_part(response) + if not img_part or not getattr(img_part, "data", None): + # Try to extract any text message from Gemini + message = "No image returned from Gemini." + + if getattr(response, "candidates", None) and len(response.candidates) > 0: + cand = response.candidates[0] + finish_reason = getattr(cand, "finish_reason", "unknown") + + # Extract text explanation from Gemini + if hasattr(cand, "content") and getattr(cand.content, "parts", None): + for part in cand.content.parts: + txt = getattr(part, "text", None) + if txt: + message += f" Gemini says: {txt[:500]}" + break + + # Check safety ratings + if hasattr(cand, "safety_ratings"): + safety_ratings = cand.safety_ratings + if safety_ratings: + blocked = [ + f"{getattr(r, 'category', 'unknown')}" + for r in safety_ratings + if hasattr(r, "blocked") and getattr(r, "blocked", False) + ] + if blocked: + message += f" Safety blocked: {', '.join(blocked)}" + + raise HTTPException( + status_code=502, + detail=f"{message} Finish reason: {finish_reason}", + ) + else: + raise HTTPException( + status_code=502, + detail="No image returned from Gemini. No candidates in response.", + ) + + raw = img_part.data + + # Ensure valid image + try: + Image.open(BytesIO(raw)) + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Returned image is invalid: {e}. Image size: {len(raw)} bytes" + ) + + return Response(content=raw, media_type="image/png") diff --git a/postman_collection.json b/postman_collection.json new file mode 100644 index 0000000..b07e348 --- /dev/null +++ b/postman_collection.json @@ -0,0 +1,68 @@ +{ + "info": { + "name": "AIFit Try-On Service", + "description": "Collection for testing the AIFit FastAPI service", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Health Check", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/health", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "health" + ] + }, + "description": "Check if the service is running and healthy" + }, + "response": [] + }, + { + "name": "Try-On", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "user_photo", + "type": "file", + "src": [], + "description": "User mirror/body photo (JPEG or PNG)" + }, + { + "key": "outfit_photo", + "type": "file", + "src": [], + "description": "Collection/outfit photo (JPEG or PNG)" + } + ] + }, + "url": { + "raw": "http://localhost:8000/api/try-on/", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "api", + "try-on", + "" + ] + }, + "description": "User photo + outfit photo → edited image" + }, + "response": [] + } + ] +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..153b43f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn[standard] +google-genai +Pillow +python-multipart +python-dotenv + diff --git a/test_api_key.py b/test_api_key.py new file mode 100644 index 0000000..eaa95e7 --- /dev/null +++ b/test_api_key.py @@ -0,0 +1,59 @@ +""" +Simple script to test if your Gemini API key is working. +Run this to verify your API key before using it in the FastAPI service. +""" +import os +from dotenv import load_dotenv +from google import genai + +# Load .env +load_dotenv() + +# Get API key +api_key = os.getenv("GEMINI_API_KEY") + +if not api_key: + print("[ERROR] GEMINI_API_KEY not found in .env file or environment") + print("\nMake sure you have a .env file with:") + print("GEMINI_API_KEY=your_key_here") + exit(1) + +# Strip whitespace +api_key = api_key.strip() + +print(f"[OK] API Key found: {api_key[:10]}... (length: {len(api_key)})") + +# Validate format +if not api_key.startswith("AIza"): + print("[WARNING] API key doesn't start with 'AIza' - this might be incorrect") + +# Test the API key +print("\nTesting API key with Gemini...") +try: + client = genai.Client(api_key=api_key) + + # Try a simple test call + print("Making test API call...") + response = client.models.generate_content( + model="gemini-2.5-flash-image", + contents=["Generate a simple test image of a red circle."], + ) + + print("\n[SUCCESS] API key is valid and working!") + print("You can now use this key in your FastAPI service.") + +except Exception as e: + error_msg = str(e) + print(f"\n[ERROR] API key test failed") + print(f"Error: {error_msg}") + + if "API key" in error_msg or "INVALID_ARGUMENT" in error_msg: + print("\n[TROUBLESHOOTING]") + print("1. Check that your API key is correct (no typos)") + print("2. Verify the key in your .env file has no spaces or quotes") + print("3. Make sure the key is from Google AI Studio: https://aistudio.google.com/app/apikey") + print("4. Try regenerating a new API key") + print("5. Check if the API key has expired or been revoked") + else: + print(f"\nUnexpected error: {e}") +