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__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
.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/
|
venv/
|
||||||
|
env/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
.venv
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
# Environment variables
|
||||||
.spyderproject
|
.env
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
# IDE
|
||||||
.ropeproject
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
# mkdocs documentation
|
# OS
|
||||||
/site
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
# mypy
|
# Project specific
|
||||||
.mypy_cache/
|
*.png
|
||||||
.dmypy.json
|
*.jpg
|
||||||
dmypy.json
|
*.jpeg
|
||||||
|
!*.example.*
|
||||||
# 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
|
|
||||||
|
|||||||
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