do lots of things
This commit is contained in:
parent
b33fa28c68
commit
207a4ae834
188
.gitignore
vendored
188
.gitignore
vendored
@ -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
|
||||
# Project specific
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
!*.example.*
|
||||
|
||||
42
Dockerfile
Normal file
42
Dockerfile
Normal file
@ -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"]
|
||||
|
||||
105
POSTMAN_SETUP.md
Normal file
105
POSTMAN_SETUP.md
Normal file
@ -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
|
||||
|
||||
109
README.md
Normal file
109
README.md
Normal file
@ -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
|
||||
|
||||
135
TROUBLESHOOTING.md
Normal file
135
TROUBLESHOOTING.md
Normal file
@ -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
|
||||
```
|
||||
|
||||
28
docker-compose.yml
Normal file
28
docker-compose.yml
Normal file
@ -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
|
||||
|
||||
261
main.py
Normal file
261
main.py
Normal file
@ -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")
|
||||
68
postman_collection.json
Normal file
68
postman_collection.json
Normal file
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
google-genai
|
||||
Pillow
|
||||
python-multipart
|
||||
python-dotenv
|
||||
|
||||
59
test_api_key.py
Normal file
59
test_api_key.py
Normal file
@ -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}")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user