Files
imagecreator/docs/superpowers/plans/2026-03-24-imagecreator-implementation.md
T
Claude ce6a775ffd docs: add imagecreator implementation plan
Detailed implementation plan covering:
- 17 tasks across 3 phases
- Backend: FastAPI, SQLAlchemy, JWT auth, image generation
- Frontend: Vue 3, Naive UI, TailwindCSS
- Docker deployment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 14:40:05 +08:00

68 KiB
Raw Blame History

ImageCreator Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a full-stack image generation website with user authentication, prompt optimization, and T2I/I2I generation capabilities.

Architecture: FastAPI backend with PostgreSQL, Vue 3 frontend with Naive UI, Docker Compose deployment. Backend handles all image generation logic via adapter pattern (referencing prompt-optimizer architecture).

Tech Stack: Python FastAPI, SQLAlchemy, PostgreSQL, JWT, Vue 3, TypeScript, Naive UI, TailwindCSS, Vite, Docker


File Structure

/opt/imagecreator/imagecreator/
├── backend/
│   ├── app/
│   │   ├── __init__.py
│   │   ├── main.py              # FastAPI app entry
│   │   ├── config.py            # Settings from env
│   │   ├── database.py           # SQLAlchemy engine/session
│   │   ├── api/
│   │   │   ├── __init__.py
│   │   │   ├── deps.py           # Dependency injection (get_db, get_current_user)
│   │   │   ├── auth.py           # /api/auth routes
│   │   │   ├── users.py          # /api/users routes
│   │   │   ├── images.py         # /api/images routes
│   │   │   └── history.py        # /api/history routes
│   │   ├── core/
│   │   │   ├── __init__.py
│   │   │   └── security.py       # JWT, password hashing
│   │   ├── models/
│   │   │   ├── __init__.py
│   │   │   ├── user.py           # User SQLAlchemy model
│   │   │   └── history.py        # GenerationHistory model
│   │   ├── schemas/
│   │   │   ├── __init__.py
│   │   │   ├── auth.py           # Login/Register schemas
│   │   │   ├── user.py           # User schemas
│   │   │   ├── image.py           # Image generate/optimize schemas
│   │   │   └── history.py        # History schemas
│   │   └── services/
│   │       ├── __init__.py
│   │       ├── auth.py            # AuthService (register, login)
│   │       ├── image.py           # ImageService (generate, optimize)
│   │       └── llm.py             # LLM client for prompt optimization
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── test_auth.py
│   │   ├── test_images.py
│   │   └── test_history.py
│   ├── requirements.txt
│   └── Dockerfile
├── frontend/
│   ├── src/
│   │   ├── api/
│   │   │   ├── index.ts           # Axios instance
│   │   │   ├── auth.ts            # Auth API calls
│   │   │   ├── images.ts          # Image generation API calls
│   │   │   └── history.ts         # History API calls
│   │   ├── components/
│   │   │   ├── ImageGenerator.vue
│   │   │   ├── PromptOptimizer.vue
│   │   │   ├── ImageHistory.vue
│   │   │   ├── ImagePreview.vue
│   │   │   └── ...
│   │   ├── views/
│   │   │   ├── Login.vue
│   │   │   ├── Register.vue
│   │   │   ├── Home.vue
│   │   │   └── History.vue
│   │   ├── stores/
│   │   │   └── auth.ts            # Pinia auth store
│   │   ├── router/
│   │   │   └── index.ts
│   │   ├── App.vue
│   │   └── main.ts
│   ├── package.json
│   ├── vite.config.ts
│   ├── tailwind.config.js
│   ├── index.html
│   └── Dockerfile
├── docker-compose.yml
└── README.md

Phase 1: Project Structure & Backend Foundation

Task 1: Create project directory structure

Files:

  • Create: /opt/imagecreator/imagecreator/backend/app/__init__.py

  • Create: /opt/imagecreator/imagecreator/backend/app/main.py

  • Create: /opt/imagecreator/imagecreator/backend/app/config.py

  • Create: /opt/imagecreator/imagecreator/backend/app/database.py

  • Create: /opt/imagecreator/imagecreator/backend/app/api/__init__.py

  • Create: /opt/imagecreator/imagecreator/backend/app/api/deps.py

  • Create: /opt/imagecreator/imagecreator/backend/app/core/__init__.py

  • Create: /opt/imagecreator/imagecreator/backend/app/core/security.py

  • Create: /opt/imagecreator/imagecreator/backend/app/models/__init__.py

  • Create: /opt/imagecreator/imagecreator/backend/app/models/user.py

  • Create: /opt/imagecreator/imagecreator/backend/app/models/history.py

  • Create: /opt/imagecreator/imagecreator/backend/app/schemas/__init__.py

  • Create: /opt/imagecreator/imagecreator/backend/app/schemas/auth.py

  • Create: /opt/imagecreator/imagecreator/backend/app/schemas/user.py

  • Create: /opt/imagecreator/imagecreator/backend/app/schemas/image.py

  • Create: /opt/imagecreator/imagecreator/backend/app/schemas/history.py

  • Create: /opt/imagecreator/imagecreator/backend/app/services/__init__.py

  • Create: /opt/imagecreator/imagecreator/backend/app/services/auth.py

  • Create: /opt/imagecreator/imagecreator/backend/app/services/image.py

  • Create: /opt/imagecreator/imagecreator/backend/app/services/llm.py

  • Create: /opt/imagecreator/imagecreator/backend/requirements.txt

  • Create: /opt/imagecreator/imagecreator/backend/Dockerfile

  • Create: /opt/imagecreator/imagecreator/frontend/ (Vue project structure)

  • Step 1: Create backend directory structure

Run: mkdir -p /opt/imagecreator/imagecreator/backend/app/{api,core,models,schemas,services} /opt/imagecreator/imagecreator/backend/tests

  • Step 2: Create backend requirements.txt
fastapi==0.109.0
uvicorn[standard]==0.27.0
sqlalchemy==2.0.25
psycopg2-binary==2.9.9
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6
pydantic==2.5.3
pydantic-settings==2.1.0
google-genai==0.8.0
openai==1.12.0
aiofiles==23.2.1
httpx==0.26.0
pytest==8.0.0
pytest-asyncio==0.23.4
  • Step 3: Create backend/app/__init__.py and main.py
# app/__init__.py
# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.api import auth, users, images, history
from app.config import settings
from app.database import engine
from app.models import user, history as history_model

app = FastAPI(title="ImageCreator API", version="1.0.0")

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.CORS_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(users.router, prefix="/api/users", tags=["users"])
app.include_router(images.router, prefix="/api/images", tags=["images"])
app.include_router(history.router, prefix="/api/history", tags=["history"])

@app.get("/health")
def health_check():
    return {"status": "ok"}
  • Step 4: Create app/config.py
# app/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache

class Settings(BaseSettings):
    # Database
    DATABASE_URL: str = "postgresql://user:pass@localhost:5432/imagecreator"

    # JWT
    SECRET_KEY: str = "your-secret-key-change-in-production"
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
    REFRESH_TOKEN_EXPIRE_DAYS: int = 7

    # CORS
    CORS_ORIGINS: list[str] = ["http://localhost:3000"]

    # LLM API Keys
    GEMINI_API_KEY: str = ""
    OPENAI_API_KEY: str = ""

    # Image storage
    IMAGE_STORAGE_PATH: str = "/app/uploads"

    class Config:
        env_file = ".env"

@lru_cache()
def get_settings():
    return Settings()

settings = get_settings()
  • Step 5: Create app/database.py
# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base

from app.config import settings

engine = create_engine(settings.DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
  • Step 6: Commit
git add -A && git commit -m "feat: create backend project structure"

Task 2: Implement database models

Files:

  • Modify: /opt/imagecreator/imagecreator/backend/app/models/user.py

  • Modify: /opt/imagecreator/imagecreator/backend/app/models/history.py

  • Create: /opt/imagecreator/imagecreator/backend/tests/test_models.py

  • Step 1: Create User model

# app/models/user.py
import uuid
from datetime import datetime
from sqlalchemy import Column, String, DateTime
from sqlalchemy.dialects.postgresql import UUID

from app.database import Base

class User(Base):
    __tablename__ = "users"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    username = Column(String(32), unique=True, nullable=False, index=True)
    email = Column(String(255), unique=True, nullable=False, index=True)
    hashed_password = Column(String(255), nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# app/models/history.py
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Text, DateTime, ForeignKey, Enum as SQLEnum
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
import enum

from app.database import Base

class GenerationMode(str, enum.Enum):
    TEXT2IMAGE = "text2image"
    IMAGE2IMAGE = "image2image"

class GenerationHistory(Base):
    __tablename__ = "generation_history"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
    original_prompt = Column(Text, nullable=False)
    optimized_prompt = Column(Text, nullable=False)
    mode = Column(SQLEnum(GenerationMode), nullable=False)
    model = Column(String(32), nullable=False)
    image_url = Column(String(512), nullable=False)
    input_image_url = Column(String(512), nullable=True)  # For I2I
    created_at = Column(DateTime, default=datetime.utcnow)

    user = relationship("User", backref="generation_history")
  • Step 2: Create test for models
# tests/test_models.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base
from app.models.user import User
from app.models.history import GenerationHistory, GenerationMode

engine = create_engine("sqlite:///:memory:")
TestSessionLocal = sessionmaker(bind=engine)

def test_user_model():
    Base.metadata.create_all(bind=engine)
    db = TestSessionLocal()

    user = User(username="testuser", email="test@example.com", hashed_password="hashed")
    db.add(user)
    db.commit()

    assert user.id is not None
    assert user.username == "testuser"
    assert user.email == "test@example.com"

    db.close()

def test_generation_history_model():
    Base.metadata.create_all(bind=engine)
    db = TestSessionLocal()

    user = User(username="testuser2", email="test2@example.com", hashed_password="hashed")
    db.add(user)
    db.commit()

    history = GenerationHistory(
        user_id=user.id,
        original_prompt="a cat",
        optimized_prompt="a fluffy orange cat sitting on a windowsill",
        mode=GenerationMode.TEXT2IMAGE,
        model="gemini-2.5-flash-image",
        image_url="/images/cat.png"
    )
    db.add(history)
    db.commit()

    assert history.id is not None
    assert history.user_id == user.id
    assert history.mode == GenerationMode.TEXT2IMAGE

    db.close()
  • Step 3: Run tests

Run: cd /opt/imagecreator/imagecreator/backend && pytest tests/test_models.py -v Expected: PASS

  • Step 4: Commit
git add -A && git commit -m "feat: add database models (User, GenerationHistory)"

Task 3: Implement security module (JWT & password)

Files:

  • Modify: /opt/imagecreator/imagecreator/backend/app/core/security.py

  • Create: /opt/imagecreator/imagecreator/backend/tests/test_security.py

  • Step 1: Write failing test

# tests/test_security.py
import pytest
from app.core.security import verify_password, hash_password, create_access_token, decode_access_token

def test_password_hash_and_verify():
    password = "testpassword123"
    hashed = hash_password(password)

    assert hashed != password
    assert verify_password(password, hashed) is True
    assert verify_password("wrongpassword", hashed) is False

def test_access_token_creation_and_decode():
    token = create_access_token({"sub": "testuser"})

    assert token is not None
    payload = decode_access_token(token)

    assert payload is not None
    assert payload.sub == "testuser"
  • Step 2: Run test to verify it fails

Run: cd /opt/imagecreator/imagecreator/backend && pytest tests/test_security.py -v Expected: FAIL - verify_password not defined

  • Step 3: Write minimal implementation
# app/core/security.py
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext

from app.config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(password: str) -> str:
    return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> Optional[dict]:
    try:
        payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
        return payload
    except JWTError:
        return None
  • Step 4: Run tests

Run: cd /opt/imagecreator/imagecreator/backend && pytest tests/test_security.py -v Expected: PASS

  • Step 5: Commit
git add -A && git commit -m "feat: add JWT and password security module"

Task 4: Implement auth API & service

Files:

  • Modify: /opt/imagecreator/imagecreator/backend/app/schemas/auth.py

  • Modify: /opt/imagecreator/imagecreator/backend/app/services/auth.py

  • Modify: /opt/imagecreator/imagecreator/backend/app/api/auth.py

  • Create: /opt/imagecreator/imagecreator/backend/tests/test_auth_api.py

  • Step 1: Write failing test

# tests/test_auth_api.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db
from app.main import app
from app.models.user import User
from app.core.security import hash_password

engine = create_engine("sqlite:///:memory:")
TestSessionLocal = sessionmaker(bind=engine)

def override_get_db():
    db = TestSessionLocal()
    try:
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)

def test_register_success():
    Base.metadata.create_all(bind=engine)
    response = client.post("/api/auth/register", json={
        "username": "newuser",
        "email": "new@example.com",
        "password": "password123"
    })
    assert response.status_code == 201
    data = response.json()
    assert data["username"] == "newuser"
    assert "id" in data

def test_register_duplicate_username():
    Base.metadata.create_all(bind=engine)
    db = TestSessionLocal()
    user = User(username="existing", email="existing@example.com", hashed_password=hash_password("pass"))
    db.add(user)
    db.commit()
    db.close()

    response = client.post("/api/auth/register", json={
        "username": "existing",
        "email": "new@example.com",
        "password": "password123"
    })
    assert response.status_code == 400

def test_login_success():
    Base.metadata.create_all(bind=engine)
    db = TestSessionLocal()
    user = User(username="loginuser", email="login@example.com", hashed_password=hash_password("correctpassword"))
    db.add(user)
    db.commit()
    db.close()

    response = client.post("/api/auth/login", json={
        "username": "loginuser",
        "password": "correctpassword"
    })
    assert response.status_code == 200
    data = response.json()
    assert "access_token" in data
    assert "token_type" in data

def test_login_wrong_password():
    Base.metadata.create_all(bind=engine)
    db = TestSessionLocal()
    user = User(username="wrongpass", email="wrongpass@example.com", hashed_password=hash_password("correctpassword"))
    db.add(user)
    db.commit()
    db.close()

    response = client.post("/api/auth/login", json={
        "username": "wrongpass",
        "password": "wrongpassword"
    })
    assert response.status_code == 401
  • Step 2: Run test to verify it fails

Run: cd /opt/imagecreator/imagecreator/backend && pytest tests/test_auth_api.py -v Expected: FAIL - modules not found

  • Step 3: Write schemas
# app/schemas/auth.py
from pydantic import BaseModel, EmailStr

class UserRegister(BaseModel):
    username: str
    email: EmailStr
    password: str

class UserLogin(BaseModel):
    username: str
    password: str

class Token(BaseModel):
    access_token: str
    refresh_token: str
    token_type: str = "bearer"

class TokenRefresh(BaseModel):
    refresh_token: str
# app/schemas/user.py
from pydantic import BaseModel, EmailStr
from typing import Optional
from datetime import datetime
from uuid import UUID

class UserBase(BaseModel):
    username: str
    email: EmailStr

class UserCreate(UserBase):
    password: str

class UserResponse(UserBase):
    id: UUID
    created_at: datetime

    class Config:
        from_attributes = True

class UserUpdate(BaseModel):
    email: Optional[EmailStr] = None
  • Step 4: Write auth service
# app/services/auth.py
from typing import Optional
from sqlalchemy.orm import Session
from uuid import UUID

from app.models.user import User
from app.core.security import hash_password, verify_password

class AuthService:
    def __init__(self, db: Session):
        self.db = db

    def register(self, username: str, email: str, password: str) -> User:
        # Check if user exists
        existing = self.db.query(User).filter(
            (User.username == username) | (User.email == email)
        ).first()
        if existing:
            raise ValueError("Username or email already registered")

        hashed_password = hash_password(password)
        user = User(username=username, email=email, hashed_password=hashed_password)
        self.db.add(user)
        self.db.commit()
        self.db.refresh(user)
        return user

    def authenticate(self, username: str, password: str) -> Optional[User]:
        user = self.db.query(User).filter(User.username == username).first()
        if not user:
            return None
        if not verify_password(password, user.hashed_password):
            return None
        return user

    def get_user_by_id(self, user_id: UUID) -> Optional[User]:
        return self.db.query(User).filter(User.id == user_id).first()
  • Step 5: Write auth API routes
# app/api/auth.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from app.database import get_db
from app.schemas.auth import UserRegister, UserLogin, Token, TokenRefresh
from app.schemas.user import UserResponse
from app.services.auth import AuthService
from app.core.security import create_access_token

router = APIRouter()

@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def register(data: UserRegister, db: Session = Depends(get_db)):
    service = AuthService(db)
    try:
        user = service.register(data.username, data.email, data.password)
        return user
    except ValueError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))

@router.post("/login", response_model=Token)
def login(data: UserLogin, db: Session = Depends(get_db)):
    service = AuthService(db)
    user = service.authenticate(data.username, data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password"
        )

    access_token = create_access_token(data={"sub": str(user.id)})
    # For simplicity, using same token as refresh - in production, use separate refresh token
    return Token(access_token=access_token, refresh_token=access_token)

@router.post("/refresh", response_model=Token)
def refresh(data: TokenRefresh, db: Session = Depends(get_db)):
    # Simplified: in production, validate refresh token properly
    from app.core.security import decode_access_token
    payload = decode_access_token(data.refresh_token)
    if not payload:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")

    access_token = create_access_token(data={"sub": payload.get("sub")})
    return Token(access_token=access_token, refresh_token=data.refresh_token)
  • Step 6: Write api/deps.py
# app/api/deps.py
from typing import Generator
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session

from app.database import get_db
from app.core.security import decode_access_token
from app.services.auth import AuthService

security = HTTPBearer()

def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security),
    db: Session = Depends(get_db)
):
    token = credentials.credentials
    payload = decode_access_token(token)
    if not payload:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired token"
        )

    user_id_str = payload.get("sub")
    if not user_id_str:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")

    from uuid import UUID
    try:
        user_id = UUID(user_id_str)
    except ValueError:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid user ID")

    service = AuthService(db)
    user = service.get_user_by_id(user_id)
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")

    return user
  • Step 7: Run tests

Run: cd /opt/imagecreator/imagecreator/backend && pytest tests/test_auth_api.py -v Expected: PASS

  • Step 8: Commit
git add -A && git commit -m "feat: implement auth API (register, login, JWT)"

Task 5: Implement users API

Files:

  • Modify: /opt/imagecreator/imagecreator/backend/app/api/users.py

  • Create: /opt/imagecreator/imagecreator/backend/tests/test_users_api.py

  • Step 1: Write failing test

# tests/test_users_api.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db
from app.main import app
from app.models.user import User
from app.core.security import hash_password, create_access_token

engine = create_engine("sqlite:///:memory:")
TestSessionLocal = sessionmaker(bind=engine)

def override_get_db():
    db = TestSessionLocal()
    try:
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)

def get_auth_header(user_id):
    token = create_access_token({"sub": str(user_id)})
    return {"Authorization": f"Bearer {token}"}

def test_get_current_user():
    Base.metadata.create_all(bind=engine)
    db = TestSessionLocal()
    user = User(username="testuser", email="test@example.com", hashed_password=hash_password("pass"))
    db.add(user)
    db.commit()
    db.refresh(user)
    db.close()

    response = client.get("/api/users/me", headers=get_auth_header(user.id))
    assert response.status_code == 200
    data = response.json()
    assert data["username"] == "testuser"
    assert data["email"] == "test@example.com"
    assert "password" not in data
  • Step 2: Run test to verify it fails

Run: cd /opt/imagecreator/imagecreator/backend && pytest tests/test_users_api.py -v Expected: FAIL

  • Step 3: Write users API
# app/api/users.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from app.database import get_db
from app.api.deps import get_current_user
from app.models.user import User
from app.schemas.user import UserResponse, UserUpdate
from app.services.auth import AuthService

router = APIRouter()

@router.get("/me", response_model=UserResponse)
def get_current_user_info(current_user: User = Depends(get_current_user)):
    return current_user

@router.put("/me", response_model=UserResponse)
def update_current_user(
    data: UserUpdate,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    service = AuthService(db)
    if data.email and data.email != current_user.email:
        # Check if email is taken
        existing = db.query(User).filter(User.email == data.email, User.id != current_user.id).first()
        if existing:
            raise HTTPException(status_code=400, detail="Email already registered")
        current_user.email = data.email

    db.commit()
    db.refresh(current_user)
    return current_user
  • Step 4: Run tests

Run: cd /opt/imagecreator/imagecreator/backend && pytest tests/test_users_api.py -v Expected: PASS

  • Step 5: Commit
git add -A && git commit -m "feat: implement users API"

Task 6: Implement LLM and prompt optimization service

Files:

  • Modify: /opt/imagecreator/imagecreator/backend/app/services/llm.py

  • Create: /opt/imagecreator/imagecreator/backend/app/services/prompt.py

  • Create: /opt/imagecreator/imagecreator/backend/tests/test_prompt.py

  • Step 1: Write failing test

# tests/test_prompt.py
import pytest
from app.services.prompt import PromptOptimizationService

def test_optimize_text2image_prompt():
    service = PromptOptimizationService()
    result = service.optimize_text2image("a cat")

    assert result is not None
    assert len(result) > len("a cat")
    assert "cat" in result.lower()

def test_optimize_image2image_prompt():
    service = PromptOptimizationService()
    result = service.optimize_image2image("make it colorful", "a cat image")

    assert result is not None
    assert len(result) > 0
  • Step 2: Run test to verify it fails

Run: cd /opt/imagecreator/imagecreator/backend && pytest tests/test_prompt.py -v Expected: FAIL - module not found

  • Step 3: Write LLM service
# app/services/llm.py
from typing import Optional
import google.genai as genai
from app.config import settings

class LLMService:
    def __init__(self):
        genai.configure(api_key=settings.GEMINI_API_KEY)

    def generate_text(self, prompt: str, system_instruction: Optional[str] = None) -> str:
        client = genai.GenerativeModel("gemini-2.5-flash")
        response = client.generate_content(prompt)
        return response.text
# app/services/prompt.py
from app.services.llm import LLMService

TEXT2IMAGE_SYSTEM_PROMPT = """You are an expert at optimizing text-to-image prompts.
Your task is to take user's simple or vague prompt and enhance it with detailed, vivid descriptions.
Include details about: subject, setting, lighting, mood, style, composition, and quality modifiers.
Output ONLY the optimized prompt in English, no explanations."""

IMAGE2IMAGE_SYSTEM_PROMPT = """You are an expert at optimizing image-to-image prompts.
The user wants to modify an existing image. Your task is to clearly describe what should be changed.

Important rules:
1. Identify if user wants to ADD, REMOVE, REPLACE, or ENHANCE something
2. Be specific about what to keep vs what to change
3. Describe the desired style, mood, and quality
4. Output ONLY the optimized prompt in English, no explanations."""

class PromptOptimizationService:
    def __init__(self):
        self.llm = LLMService()

    def optimize_text2image(self, user_prompt: str) -> str:
        full_prompt = f"{TEXT2IMAGE_SYSTEM_PROMPT}\n\nUser's original prompt: {user_prompt}"
        return self.llm.generate_text(full_prompt)

    def optimize_image2image(self, user_prompt: str, original_description: str = "") -> str:
        context = f"Original image description: {original_description}\n\n" if original_description else ""
        full_prompt = f"{IMAGE2IMAGE_SYSTEM_PROMPT}\n\n{context}User's modification request: {user_prompt}"
        return self.llm.generate_text(full_prompt)
  • Step 4: Run tests (skipped if no API key)

Run: cd /opt/imagecreator/imagecreator/backend && pytest tests/test_prompt.py -v Expected: SKIP or FAIL (if no API key)

  • Step 5: Commit
git add -A && git commit -m "feat: implement LLM and prompt optimization service"

Task 7: Implement image generation service

Files:

  • Modify: /opt/imagecreator/imagecreator/backend/app/services/image.py

  • Create: /opt/imagecreator/imagecreator/backend/tests/test_image_service.py

  • Step 1: Write failing test

# tests/test_image_service.py
import pytest
from app.services.image import ImageGenerationService

def test_generate_text2image_request_structure():
    # Test that the service creates proper request structure
    # This is a unit test - actual generation would require API key
    service = ImageGenerationService()
    # Just verify service initializes
    assert service is not None
  • Step 2: Write image service
# app/services/image.py
import base64
import os
import uuid
from typing import Optional
import google.genai as genai
from app.config import settings

class ImageGenerationService:
    def __init__(self):
        genai.configure(api_key=settings.GEMINI_API_KEY)

    async def generate_text2image(
        self,
        prompt: str,
        model: str = "gemini-2.5-flash-image"
    ) -> dict:
        client = genai.GenerativeModel(model)

        response = client.generate_content(
            prompt,
            generation_config={"response_modalities": ["TEXT", "IMAGE"]}
        )

        image_data = None
        for part in response.candidates[0].content.parts:
            if part.inline_data:
                image_data = base64.b64encode(part.inline_data.data).decode('utf-8')
                break

        return {
            "image_b64": image_data,
            "model": model,
            "finish_reason": response.candidates[0].finish_reason
        }

    async def generate_image2image(
        self,
        prompt: str,
        input_image_b64: str,
        input_mime_type: str = "image/png",
        model: str = "gemini-2.5-flash-image"
    ) -> dict:
        client = genai.GenerativeModel(model)

        response = client.generate_content(
            [
                {"text": prompt},
                {"inline_data": {"mime_type": input_mime_type, "data": input_image_b64}}
            ],
            generation_config={"response_modalities": ["TEXT", "IMAGE"]}
        )

        image_data = None
        for part in response.candidates[0].content.parts:
            if part.inline_data:
                image_data = base64.b64encode(part.inline_data.data).decode('utf-8')
                break

        return {
            "image_b64": image_data,
            "model": model,
            "finish_reason": response.candidates[0].finish_reason
        }

    def save_image(self, image_b64: str, user_id: str) -> str:
        os.makedirs(settings.IMAGE_STORAGE_PATH, exist_ok=True)
        filename = f"{user_id}/{uuid.uuid4()}.png"
        filepath = os.path.join(settings.IMAGE_STORAGE_PATH, filename)
        os.makedirs(os.path.dirname(filepath), exist_ok=True)

        image_data = base64.b64decode(image_b64)
        with open(filepath, "wb") as f:
            f.write(image_data)

        return f"/uploads/{filename}"
  • Step 3: Commit
git add -A && git commit -m "feat: implement image generation service"

Task 8: Implement images API

Files:

  • Modify: /opt/imagecreator/imagecreator/backend/app/schemas/image.py

  • Modify: /opt/imagecreator/imagecreator/backend/app/api/images.py

  • Create: /opt/imagecreator/imagecreator/backend/tests/test_images_api.py

  • Step 1: Write failing test

# tests/test_images_api.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db
from app.main import app
from app.models.user import User
from app.core.security import hash_password, create_access_token

engine = create_engine("sqlite:///:memory:")
TestSessionLocal = sessionmaker(bind=engine)

def override_get_db():
    db = TestSessionLocal()
    try:
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)

def get_auth_header(user_id):
    token = create_access_token({"sub": str(user_id)})
    return {"Authorization": f"Bearer {token}"}

def test_optimize_prompt_requires_auth():
    response = client.post("/api/images/optimize", json={"prompt": "a cat"})
    assert response.status_code == 403  # No auth header

def test_generate_requires_auth():
    response = client.post("/api/images/generate", json={
        "prompt": "a cat",
        "mode": "text2image",
        "model": "gemini-2.5-flash-image"
    })
    assert response.status_code == 403
  • Step 2: Write schemas
# app/schemas/image.py
from pydantic import BaseModel
from typing import Optional
from enum import Enum

class GenerationMode(str, Enum):
    TEXT2IMAGE = "text2image"
    IMAGE2IMAGE = "image2image"

class PromptOptimizeRequest(BaseModel):
    prompt: str
    mode: GenerationMode = GenerationMode.TEXT2IMAGE
    original_description: Optional[str] = None

class PromptOptimizeResponse(BaseModel):
    optimized_prompt: str

class ImageGenerateRequest(BaseModel):
    prompt: str
    optimized_prompt: str
    mode: GenerationMode
    model: str = "gemini-2.5-flash-image"
    input_image_b64: Optional[str] = None  # For I2I

class ImageGenerateResponse(BaseModel):
    image_url: str
    model: str
    optimized_prompt: str
  • Step 3: Write images API
# app/api/images.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from app.database import get_db
from app.api.deps import get_current_user
from app.models.user import User
from app.schemas.image import (
    PromptOptimizeRequest,
    PromptOptimizeResponse,
    ImageGenerateRequest,
    ImageGenerateResponse,
)
from app.services.prompt import PromptOptimizationService
from app.services.image import ImageGenerationService
from app.services.auth import AuthService

router = APIRouter()

@router.post("/optimize", response_model=PromptOptimizeResponse)
async def optimize_prompt(
    data: PromptOptimizeRequest,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    service = PromptOptimizationService()
    try:
        if data.mode == "text2image":
            optimized = service.optimize_text2image(data.prompt)
        else:
            optimized = service.optimize_image2image(data.prompt, data.original_description or "")

        return PromptOptimizeResponse(optimized_prompt=optimized)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Optimization failed: {str(e)}")

@router.post("/generate", response_model=ImageGenerateResponse)
async def generate_image(
    data: ImageGenerateRequest,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    service = ImageGenerationService()
    try:
        if data.mode == "text2image":
            result = await service.generate_text2image(data.optimized_prompt, data.model)
        else:
            if not data.input_image_b64:
                raise HTTPException(status_code=400, detail="input_image_b64 required for image2image")
            result = await service.generate_image2image(
                data.optimized_prompt,
                data.input_image_b64,
                "image/png",
                data.model
            )

        # Save image
        image_url = service.save_image(result["image_b64"], str(current_user.id))

        return ImageGenerateResponse(
            image_url=image_url,
            model=result["model"],
            optimized_prompt=data.optimized_prompt
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
  • Step 4: Run tests

Run: cd /opt/imagecreator/imagecreator/backend && pytest tests/test_images_api.py -v Expected: PASS (auth tests)

  • Step 5: Commit
git add -A && git commit -m "feat: implement images API (optimize, generate)"

Task 9: Implement history API

Files:

  • Modify: /opt/imagecreator/imagecreator/backend/app/api/history.py

  • Create: /opt/imagecreator/imagecreator/backend/tests/test_history_api.py

  • Step 1: Write failing test

# tests/test_history_api.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db
from app.main import app
from app.models.user import User
from app.models.history import GenerationHistory, GenerationMode
from app.core.security import hash_password, create_access_token

engine = create_engine("sqlite:///:memory:")
TestSessionLocal = sessionmaker(bind=engine)

def override_get_db():
    db = TestSessionLocal()
    try:
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)

def get_auth_header(user_id):
    token = create_access_token({"sub": str(user_id)})
    return {"Authorization": f"Bearer {token}"}

def test_get_history_empty():
    Base.metadata.create_all(bind=engine)
    db = TestSessionLocal()
    user = User(username="histtest", email="hist@example.com", hashed_password=hash_password("pass"))
    db.add(user)
    db.commit()
    db.refresh(user)
    db.close()

    response = client.get("/api/history", headers=get_auth_header(user.id))
    assert response.status_code == 200
    assert response.json() == []

def test_get_history_with_items():
    Base.metadata.create_all(bind=engine)
    db = TestSessionLocal()
    user = User(username="histtest2", email="hist2@example.com", hashed_password=hash_password("pass"))
    db.add(user)
    db.commit()
    db.refresh(user)

    history = GenerationHistory(
        user_id=user.id,
        original_prompt="a cat",
        optimized_prompt="a fluffy orange cat",
        mode=GenerationMode.TEXT2IMAGE,
        model="gemini",
        image_url="/images/cat.png"
    )
    db.add(history)
    db.commit()
    db.close()

    response = client.get("/api/history", headers=get_auth_header(user.id))
    assert response.status_code == 200
    data = response.json()
    assert len(data) == 1
    assert data[0]["original_prompt"] == "a cat"
  • Step 2: Write history API
# app/api/history.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from uuid import UUID

from app.database import get_db
from app.api.deps import get_current_user
from app.models.user import User
from app.models.history import GenerationHistory
from app.schemas.history import HistoryResponse

router = APIRouter()

@router.get("", response_model=List[HistoryResponse])
def get_history(
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    items = db.query(GenerationHistory).filter(
        GenerationHistory.user_id == current_user.id
    ).order_by(GenerationHistory.created_at.desc()).all()
    return items

@router.get("/{history_id}", response_model=HistoryResponse)
def get_history_item(
    history_id: UUID,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    item = db.query(GenerationHistory).filter(
        GenerationHistory.id == history_id,
        GenerationHistory.user_id == current_user.id
    ).first()

    if not item:
        raise HTTPException(status_code=404, detail="History item not found")
    return item

@router.delete("/{history_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_history_item(
    history_id: UUID,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    item = db.query(GenerationHistory).filter(
        GenerationHistory.id == history_id,
        GenerationHistory.user_id == current_user.id
    ).first()

    if not item:
        raise HTTPException(status_code=404, detail="History item not found")

    db.delete(item)
    db.commit()
  • Step 3: Write history schema
# app/schemas/history.py
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
from uuid import UUID

class HistoryResponse(BaseModel):
    id: UUID
    original_prompt: str
    optimized_prompt: str
    mode: str
    model: str
    image_url: str
    input_image_url: Optional[str] = None
    created_at: datetime

    class Config:
        from_attributes = True
  • Step 4: Run tests

Run: cd /opt/imagecreator/imagecreator/backend && pytest tests/test_history_api.py -v Expected: PASS

  • Step 5: Commit
git add -A && git commit -m "feat: implement history API"

Task 10: Backend Docker setup

Files:

  • Create: /opt/imagecreator/imagecreator/backend/Dockerfile

  • Create: /opt/imagecreator/imagecreator/docker-compose.yml

  • Step 1: Create backend Dockerfile

# backend/Dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app/ ./app/

RUN apt-get update && apt-get install -y --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

# Create uploads directory
RUN mkdir -p /app/uploads

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
  • Step 2: Create docker-compose.yml
# docker-compose.yml
version: '3.8'

services:
  backend:
    build: ./backend
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/imagecreator
      - SECRET_KEY=change-this-secret-key-in-production
      - GEMINI_API_KEY=${GEMINI_API_KEY}
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    volumes:
      - uploads:/app/uploads
    depends_on:
      - db
    restart: unless-stopped

  frontend:
    build: ./frontend
    ports:
      - "3000:80"
    depends_on:
      - backend

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=imagecreator
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  postgres_data:
  uploads:
  • Step 3: Create .env.example
# .env.example
SECRET_KEY=your-secret-key-here
GEMINI_API_KEY=your-gemini-api-key
OPENAI_API_KEY=your-openai-api-key
DATABASE_URL=postgresql://user:pass@localhost:5432/imagecreator
  • Step 4: Commit
git add -A && git commit -m "feat: add backend Dockerfile and docker-compose"

Phase 2: Frontend Development

Task 11: Initialize Vue 3 frontend project

Files:

  • Create: /opt/imagecreator/imagecreator/frontend/package.json

  • Create: /opt/imagecreator/imagecreator/frontend/vite.config.ts

  • Create: /opt/imagecreator/imagecreator/frontend/index.html

  • Create: /opt/imagecreator/imagecreator/frontend/tailwind.config.js

  • Create: /opt/imagecreator/imagecreator/frontend/src/main.ts

  • Create: /opt/imagecreator/imagecreator/frontend/src/App.vue

  • Create: /opt/imagecreator/imagecreator/frontend/src/router/index.ts

  • Create: /opt/imagecreator/imagecreator/frontend/src/api/index.ts

  • Step 1: Create package.json

{
  "name": "imagecreator-frontend",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.4.0",
    "vue-router": "^4.2.0",
    "pinia": "^2.1.0",
    "axios": "^1.6.0",
    "naive-ui": "^2.38.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.0",
    "typescript": "^5.3.0",
    "vite": "^5.0.0",
    "vue-tsc": "^1.8.0",
    "tailwindcss": "^3.4.0",
    "autoprefixer": "^10.4.0",
    "postcss": "^8.4.0"
  }
}
  • Step 2: Create vite.config.ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true
      },
      '/uploads': {
        target: 'http://localhost:8000',
        changeOrigin: true
      }
    }
  }
})
  • Step 3: Create tailwind.config.js
// tailwind.config.js
export default {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
  • Step 4: Create main.ts
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import naive from 'naive-ui'
import App from './App.vue'
import router from './router'
import './style.css'

const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(naive)
app.mount('#app')
  • Step 5: Create api/index.ts
// src/api/index.ts
import axios from 'axios'

const api = axios.create({
  baseURL: '/api',
  timeout: 30000,
})

api.interceptors.request.use((config) => {
  const token = localStorage.getItem('access_token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      localStorage.removeItem('access_token')
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

export default api
  • Step 6: Create App.vue
<!-- src/App.vue -->
<template>
  <n-config-provider :theme-overlord="themeOverrides">
    <n-message-provider>
      <router-view />
    </n-message-provider>
  </n-config-provider>
</template>

<script setup lang="ts">
import { NConfigProvider, NMessageProvider } from 'naive-ui'

const themeOverrides = {}
</script>
  • Step 7: Create router
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  { path: '/login', name: 'Login', component: () => import('../views/Login.vue') },
  { path: '/register', name: 'Register', component: () => import('../views/Register.vue') },
  { path: '/', name: 'Home', component: () => import('../views/Home.vue'), meta: { requiresAuth: true } },
  { path: '/history', name: 'History', component: () => import('../views/History.vue'), meta: { requiresAuth: true } },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('access_token')
  if (to.meta.requiresAuth && !token) {
    next('/login')
  } else if ((to.path === '/login' || to.path === '/register') && token) {
    next('/')
  } else {
    next()
  }
})

export default router
  • Step 8: Create style.css
/* src/style.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  margin: 0;
  padding: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
  • Step 9: Commit
git add -A && git commit -m "feat: initialize Vue 3 frontend project"

Task 12: Implement auth stores and API

Files:

  • Create: /opt/imagecreator/imagecreator/frontend/src/stores/auth.ts

  • Create: /opt/imagecreator/imagecreator/frontend/src/api/auth.ts

  • Step 1: Write auth API

// src/api/auth.ts
import api from './index'

export interface LoginRequest {
  username: string
  password: string
}

export interface RegisterRequest {
  username: string
  email: string
  password: string
}

export interface AuthResponse {
  access_token: string
  refresh_token: string
  token_type: string
}

export const authApi = {
  login(data: LoginRequest) {
    return api.post<AuthResponse>('/auth/login', data)
  },
  register(data: RegisterRequest) {
    return api.post<any>('/auth/register', data)
  },
}
  • Step 2: Write auth store
// src/stores/auth.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { authApi } from '../api/auth'

export const useAuthStore = defineStore('auth', () => {
  const token = ref<string | null>(localStorage.getItem('access_token'))
  const user = ref<{ id: string; username: string; email: string } | null>(null)

  const login = async (username: string, password: string) => {
    const response = await authApi.login({ username, password })
    token.value = response.data.access_token
    localStorage.setItem('access_token', response.data.access_token)
  }

  const register = async (username: string, email: string, password: string) => {
    await authApi.register({ username, email, password })
  }

  const logout = () => {
    token.value = null
    user.value = null
    localStorage.removeItem('access_token')
  }

  return { token, user, login, register, logout }
})
  • Step 3: Commit
git add -A && git commit -m "feat: implement frontend auth store and API"

Task 13: Implement Login and Register views

Files:

  • Create: /opt/imagecreator/imagecreator/frontend/src/views/Login.vue

  • Create: /opt/imagecreator/imagecreator/frontend/src/views/Register.vue

  • Step 1: Write Login.vue

<!-- src/views/Login.vue -->
<template>
  <div class="min-h-screen flex items-center justify-center bg-gray-100">
    <n-card class="w-96" title="登录">
      <n-form ref="formRef" :model="form" :rules="rules">
        <n-form-item path="username" label="用户名">
          <n-input v-model:value="form.username" placeholder="输入用户名" />
        </n-form-item>
        <n-form-item path="password" label="密码">
          <n-input v-model:value="form.password" type="password" placeholder="输入密码" />
        </n-form-item>
        <n-button type="primary" block :loading="loading" @click="handleLogin">
          登录
        </n-button>
      </n-form>
      <div class="mt-4 text-center">
        还没有账号<router-link to="/register" class="text-blue-500">注册</router-link>
      </div>
    </n-card>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { NCard, NForm, NFormItem, NInput, NButton, useMessage } from 'naive-ui'
import { useAuthStore } from '../stores/auth'

const router = useRouter()
const message = useMessage()
const authStore = useAuthStore()

const form = ref({ username: '', password: '' })
const loading = ref(false)

const rules = {
  username: [{ required: true, message: '请输入用户名' }],
  password: [{ required: true, message: '请输入密码' }],
}

const handleLogin = async () => {
  loading.value = true
  try {
    await authStore.login(form.value.username, form.value.password)
    message.success('登录成功')
    router.push('/')
  } catch (error: any) {
    message.error(error.response?.data?.detail || '登录失败')
  } finally {
    loading.value = false
  }
}
</script>
  • Step 2: Write Register.vue
<!-- src/views/Register.vue -->
<template>
  <div class="min-h-screen flex items-center justify-center bg-gray-100">
    <n-card class="w-96" title="注册">
      <n-form ref="formRef" :model="form" :rules="rules">
        <n-form-item path="username" label="用户名">
          <n-input v-model:value="form.username" placeholder="输入用户名" />
        </n-form-item>
        <n-form-item path="email" label="邮箱">
          <n-input v-model:value="form.email" placeholder="输入邮箱" />
        </n-form-item>
        <n-form-item path="password" label="密码">
          <n-input v-model:value="form.password" type="password" placeholder="输入密码" />
        </n-form-item>
        <n-button type="primary" block :loading="loading" @click="handleRegister">
          注册
        </n-button>
      </n-form>
      <div class="mt-4 text-center">
        已有账号<router-link to="/login" class="text-blue-500">登录</router-link>
      </div>
    </n-card>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { NCard, NForm, NFormItem, NInput, NButton, useMessage } from 'naive-ui'
import { useAuthStore } from '../stores/auth'

const router = useRouter()
const message = useMessage()
const authStore = useAuthStore()

const form = ref({ username: '', email: '', password: '' })
const loading = ref(false)

const rules = {
  username: [{ required: true, message: '请输入用户名' }],
  email: [
    { required: true, message: '请输入邮箱' },
    { type: 'email', message: '请输入有效邮箱' }
  ],
  password: [
    { required: true, message: '请输入密码' },
    { min: 6, message: '密码至少6位' }
  ],
}

const handleRegister = async () => {
  loading.value = true
  try {
    await authStore.register(form.value.username, form.value.email, form.value.password)
    message.success('注册成功,请登录')
    router.push('/login')
  } catch (error: any) {
    message.error(error.response?.data?.detail || '注册失败')
  } finally {
    loading.value = false
  }
}
</script>
  • Step 3: Commit
git add -A && git commit -m "feat: implement Login and Register views"

Task 14: Implement Home view with image generation

Files:

  • Create: /opt/imagecreator/imagecreator/frontend/src/views/Home.vue

  • Create: /opt/imagecreator/imagecreator/frontend/src/api/images.ts

  • Create: /opt/imagecreator/imagecreator/frontend/src/components/ImageGenerator.vue

  • Create: /opt/imagecreator/imagecreator/frontend/src/components/PromptOptimizer.vue

  • Step 1: Write images API

// src/api/images.ts
import api from './index'

export interface PromptOptimizeRequest {
  prompt: string
  mode: 'text2image' | 'image2image'
  original_description?: string
}

export interface ImageGenerateRequest {
  prompt: string
  optimized_prompt: string
  mode: 'text2image' | 'image2image'
  model: string
  input_image_b64?: string
}

export const imagesApi = {
  optimizePrompt(data: PromptOptimizeRequest) {
    return api.post<{ optimized_prompt: string }>('/images/optimize', data)
  },
  generateImage(data: ImageGenerateRequest) {
    return api.post<{ image_url: string; model: string }>('/images/generate', data)
  },
}
  • Step 2: Write Home.vue
<!-- src/views/Home.vue -->
<template>
  <div class="min-h-screen bg-gray-50">
    <nav class="bg-white shadow">
      <div class="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
        <h1 class="text-xl font-bold">ImageCreator</h1>
        <div class="flex gap-4">
          <router-link to="/" class="text-blue-500">生成</router-link>
          <router-link to="/history" class="text-blue-500">历史</router-link>
          <n-button size="small" @click="handleLogout">退出</n-button>
        </div>
      </div>
    </nav>

    <div class="max-w-4xl mx-auto px-4 py-8">
      <n-tabs v-model:value="activeMode" type="segment">
        <n-tab-pane name="text2image" tab="文生图" />
        <n-tab-pane name="image2image" tab="图生图" />
      </n-tabs>

      <div class="mt-6 grid grid-cols-2 gap-6">
        <div>
          <n-card title="提示词输入">
            <n-input
              v-model:value="originalPrompt"
              type="textarea"
              placeholder="描述你想要生成的图片..."
              :rows="4"
            />
            <div class="mt-4">
              <n-button type="primary" block @click="handleOptimize" :loading="optimizing">
                优化提示词
              </n-button>
            </div>
            <div v-if="optimizedPrompt" class="mt-4">
              <n-divider>优化后</n-divider>
              <p class="text-gray-700 whitespace-pre-wrap">{{ optimizedPrompt }}</p>
            </div>
          </n-card>

          <n-card v-if="activeMode === 'image2image'" title="上传参考图片" class="mt-4">
            <n-upload
              accept="image/*"
              :max-size="10 * 1024 * 1024"
              @change="handleImageUpload"
              @error="handleUploadError"
            >
              <n-button>选择图片</n-button>
            </n-upload>
          </n-card>
        </div>

        <div>
          <n-card title="生成结果">
            <div v-if="generating" class="flex justify-center py-8">
              <n-spin size="large" />
            </div>
            <div v-else-if="generatedImageUrl" class="text-center">
              <img :src="generatedImageUrl" class="max-w-full rounded" alt="Generated" />
              <n-button type="primary" class="mt-4" tag="a" :href="generatedImageUrl" download>
                下载图片
              </n-button>
            </div>
            <div v-else class="text-center text-gray-400 py-8">
              生成的图片将显示在这里
            </div>
          </n-card>

          <div class="mt-4">
            <n-button
              type="primary"
              block
              size="large"
              @click="handleGenerate"
              :disabled="!optimizedPrompt"
              :loading="generating"
            >
              生成图片
            </n-button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { NCard, NInput, NButton, NTabs, NTabPane, NDivider, NSpin, NUpload, useMessage, useDialog } from 'naive-ui'
import { useAuthStore } from '../stores/auth'
import { imagesApi } from '../api/images'

const router = useRouter()
const message = useMessage()
const authStore = useAuthStore()

const activeMode = ref<'text2image' | 'image2image'>('text2image')
const originalPrompt = ref('')
const optimizedPrompt = ref('')
const generatedImageUrl = ref('')
const inputImageB64 = ref('')
const optimizing = ref(false)
const generating = ref(false)

const handleLogout = () => {
  authStore.logout()
  router.push('/login')
}

const handleOptimize = async () => {
  if (!originalPrompt.value.trim()) {
    message.warning('请输入提示词')
    return
  }
  optimizing.value = true
  try {
    const response = await imagesApi.optimizePrompt({
      prompt: originalPrompt.value,
      mode: activeMode.value,
    })
    optimizedPrompt.value = response.data.optimized_prompt
  } catch (error) {
    message.error('优化失败')
  } finally {
    optimizing.value = false
  }
}

const handleImageUpload = (options: any) => {
  const file = options.file.file
  if (!file) return

  const reader = new FileReader()
  reader.onload = (e) => {
    const result = e.target?.result as string
    inputImageB64.value = result.split(',')[1] // Remove data URL prefix
    message.success('图片上传成功')
  }
  reader.readAsDataURL(file)
}

const handleUploadError = () => {
  message.error('图片上传失败')
}

const handleGenerate = async () => {
  if (!optimizedPrompt.value) {
    message.warning('请先优化提示词')
    return
  }

  generating.value = true
  generatedImageUrl.value = ''

  try {
    const response = await imagesApi.generateImage({
      prompt: originalPrompt.value,
      optimized_prompt: optimizedPrompt.value,
      mode: activeMode.value,
      model: 'gemini-2.5-flash-image',
      input_image_b64: activeMode.value === 'image2image' ? inputImageB64.value : undefined,
    })

    // Construct full URL for image
    generatedImageUrl.value = `/uploads/${response.data.image_url.split('/uploads/')[1]}`
  } catch (error) {
    message.error('生成失败')
  } finally {
    generating.value = false
  }
}
</script>
  • Step 3: Commit
git add -A && git commit -m "feat: implement Home view with image generation"

Task 15: Implement History view

Files:

  • Create: /opt/imagecreator/imagecreator/frontend/src/views/History.vue

  • Create: /opt/imagecreator/imagecreator/frontend/src/api/history.ts

  • Step 1: Write history API

// src/api/history.ts
import api from './index'

export interface HistoryItem {
  id: string
  original_prompt: string
  optimized_prompt: string
  mode: string
  model: string
  image_url: string
  input_image_url: string | null
  created_at: string
}

export const historyApi = {
  getHistory() {
    return api.get<HistoryItem[]>('/history')
  },
  deleteHistory(id: string) {
    return api.delete(`/history/${id}`)
  },
}
  • Step 2: Write History.vue
<!-- src/views/History.vue -->
<template>
  <div class="min-h-screen bg-gray-50">
    <nav class="bg-white shadow">
      <div class="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
        <h1 class="text-xl font-bold">ImageCreator</h1>
        <div class="flex gap-4">
          <router-link to="/" class="text-blue-500">生成</router-link>
          <router-link to="/history" class="text-blue-500">历史</router-link>
          <n-button size="small" @click="handleLogout">退出</n-button>
        </div>
      </div>
    </nav>

    <div class="max-w-6xl mx-auto px-4 py-8">
      <h2 class="text-2xl font-bold mb-6">生成历史</h2>

      <div v-if="loading" class="flex justify-center py-8">
        <n-spin size="large" />
      </div>

      <div v-else-if="history.length === 0" class="text-center text-gray-400 py-8">
        暂无历史记录
      </div>

      <n-grid v-else :cols="3" :x-gap="16" :y-gap="16">
        <n-gi v-for="item in history" :key="item.id">
          <n-card class="h-full">
            <template #header>
              <div class="flex justify-between items-center">
                <span class="text-sm text-gray-500">{{ item.mode === 'text2image' ? '文生图' : '图生图' }}</span>
                <n-button text type="error" size="small" @click="handleDelete(item.id)">
                  删除
                </n-button>
              </div>
            </template>

            <img
              :src="item.image_url"
              class="w-full h-48 object-cover rounded"
              alt="Generated"
            />

            <div class="mt-4">
              <p class="text-sm text-gray-500 mb-1">原始提示词</p>
              <p class="text-sm">{{ item.original_prompt }}</p>
            </div>

            <div class="mt-2">
              <p class="text-sm text-gray-500 mb-1">优化后</p>
              <p class="text-sm">{{ item.optimized_prompt }}</p>
            </div>

            <template #footer>
              <div class="text-xs text-gray-400">
                {{ formatDate(item.created_at) }} | {{ item.model }}
              </div>
            </template>
          </n-card>
        </n-gi>
      </n-grid>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { NCard, NGrid, NGi, NButton, NSpin, useMessage, useDialog } from 'naive-ui'
import { useAuthStore } from '../stores/auth'
import { historyApi, HistoryItem } from '../api/history'

const router = useRouter()
const message = useMessage()
const dialog = useDialog()
const authStore = useAuthStore()

const history = ref<HistoryItem[]>([])
const loading = ref(false)

const fetchHistory = async () => {
  loading.value = true
  try {
    const response = await historyApi.getHistory()
    history.value = response.data
  } catch (error) {
    message.error('获取历史失败')
  } finally {
    loading.value = false
  }
}

const handleDelete = (id: string) => {
  dialog.warning({
    title: '确认删除',
    content: '确定要删除这条记录吗?',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
      try {
        await historyApi.deleteHistory(id)
        message.success('删除成功')
        fetchHistory()
      } catch (error) {
        message.error('删除失败')
      }
    }
  })
}

const formatDate = (dateStr: string) => {
  return new Date(dateStr).toLocaleString('zh-CN')
}

const handleLogout = () => {
  authStore.logout()
  router.push('/login')
}

onMounted(fetchHistory)
</script>
  • Step 3: Commit
git add -A && git commit -m "feat: implement History view"

Task 16: Frontend Dockerfile and nginx config

Files:

  • Create: /opt/imagecreator/imagecreator/frontend/Dockerfile

  • Create: /opt/imagecreator/imagecreator/frontend/nginx.conf

  • Step 1: Create Dockerfile

# frontend/Dockerfile
FROM node:20-alpine as builder

WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
  • Step 2: Create nginx.conf
# frontend/nginx.conf
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://backend:8000/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /uploads/ {
        proxy_pass http://backend:8000/uploads/;
        proxy_set_header Host $host;
    }
}
  • Step 3: Commit
git add -A && git commit -m "feat: add frontend Dockerfile and nginx config"

Phase 3: Final Integration

Task 17: Create README

Files:

  • Create: /opt/imagecreator/imagecreator/README.md

  • Step 1: Write README

# ImageCreator

A full-stack image generation website with user authentication, prompt optimization, and T2I/I2I generation.

## Features

- User authentication (register/login)
- Prompt optimization using LLM
- Text-to-Image generation
- Image-to-Image generation
- Generation history

## Tech Stack

- **Backend**: FastAPI, SQLAlchemy, PostgreSQL
- **Frontend**: Vue 3, Naive UI, TailwindCSS
- **Deployment**: Docker Compose

## Quick Start

### Prerequisites

- Docker & Docker Compose
- Gemini API key (for image generation)

### Environment Setup

1. Copy `.env.example` to `.env` and configure:
   ```bash
   cp .env.example .env
  1. Edit .env and add your API keys:
    GEMINI_API_KEY=your_gemini_api_key
    SECRET_KEY=your-secret-key
    

Running with Docker Compose

docker-compose up -d

Development

Backend

cd backend
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000

Frontend

cd frontend
npm install
npm run dev

Project Structure

imagecreator/
├── backend/           # FastAPI backend
│   ├── app/
│   │   ├── api/      # API routes
│   │   ├── core/     # Security, config
│   │   ├── models/    # Database models
│   │   ├── schemas/   # Pydantic schemas
│   │   └── services/   # Business logic
│   └── tests/
├── frontend/          # Vue 3 frontend
│   └── src/
│       ├── api/      # API clients
│       ├── views/    # Page components
│       ├── stores/   # Pinia stores
│       └── router/   # Vue Router
└── docker-compose.yml

License

AGPL-3.0


- [ ] **Step 2: Commit**

```bash
git add -A && git commit -m "docs: add README"

Summary

Total Tasks: 17

Phase 1: Backend (Tasks 1-10)

  • Project structure, config, database
  • User & History models
  • Security (JWT, password)
  • Auth API & service
  • Users API
  • LLM & Prompt optimization service
  • Image generation service
  • Images API
  • History API
  • Backend Docker

Phase 2: Frontend (Tasks 11-16)

  • Vue 3 project init
  • Auth stores and API
  • Login/Register views
  • Home view with generation
  • History view
  • Frontend Docker

Phase 3: Final (Task 17)

  • README documentation