- Add history saving to generate_image endpoint - Remove DALL-E from spec (Gemini only for now) - Add mkdir commands for frontend directories - Add fetchCurrentUser to auth store Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
69 KiB
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))
# Save to history
from app.models.history import GenerationHistory
history_item = GenerationHistory(
user_id=current_user.id,
original_prompt=data.prompt,
optimized_prompt=data.optimized_prompt,
mode=data.mode,
model=result["model"],
image_url=image_url,
input_image_url=f"/uploads/{input_image_b64.split('/')[0]}.png" if data.mode == "image2image" and data.input_image_b64 else None
)
db.add(history_item)
db.commit()
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/style.css -
Create:
/opt/imagecreator/imagecreator/frontend/src/router/index.ts -
Create:
/opt/imagecreator/imagecreator/frontend/src/api/index.ts -
Step 0: Create frontend directory structure
Run: mkdir -p /opt/imagecreator/imagecreator/frontend/src/{api,components,views,stores,router}
- 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 9: 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'
import api from '../api/index'
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)
await fetchCurrentUser()
}
const fetchCurrentUser = async () => {
if (!token.value) return
try {
const response = await api.get('/users/me')
user.value = response.data
} catch {
// Token might be expired
logout()
}
}
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, fetchCurrentUser }
})
- 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
- Edit
.envand add your API keys:GEMINI_API_KEY=your_gemini_api_key SECRET_KEY=your-secret-key
Running with Docker Compose
docker-compose up -d
- Frontend: http://localhost:3000
- Backend API: http://localhost:8000
- API Docs: http://localhost:8000/docs
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