💪

AIフル活用でゼロから作るフルスタック画像生成アプリ - RapidGenをOSS化した理由と全技術解説

に公開

はじめに

「AIを使えばコードが書けるようになる」という話を聞いて、半信半疑だった方も多いのではないでしょうか。本記事では、ChatGPTとCursor AIを駆使して、フロントエンドからバックエンドまで完全なフルスタックアプリケーションを構築し、OSS化するまでの全プロセスを包み隠さず共有します。

開発したのは、Stable Diffusion XLとLCM(Latent Consistency Model)を使用したAI画像生成アプリ「RapidGen」です。このアプリは、アップロードした画像に様々なスタイル(アニメ調、ジブリ風、3Dレンダリングなど)を適用し、数秒で高品質な画像を生成します。

公開リポジトリ

  • フロントエンド: YuuOhnuki/RapidGen
    • Next.js 16 + React 19 + TypeScript
    • モダンなUI/UX、レスポンシブデザイン
  • バックエンドAPI: YuuOhnuki/rapidgen-api
    • FastAPI + Stable Diffusion XL + LCM
    • 非同期タスク処理、GPU最適化

本記事は、以下のような方々に向けて書かれています。

  • フルスタック開発に挑戦したいエンジニア
  • AIツールを活用した開発に興味がある方
  • OSSプロジェクトの立ち上げを検討している方
  • 画像生成AIのアプリケーション開発を学びたい方

なぜOSS化するのか - 有料化を断念した3つの理由

実は、RapidGenは当初、有料サービスとして運用する予定でした。しかし、開発を進める中で3つの大きな壁に直面し、最終的にOSS化の道を選びました。

理由1: 無料で使えるバックエンドAPIの限界

Stable Diffusion XLのような大規模なAIモデルを動かすには、強力なGPU環境が必要です。無料で利用できるAPI(Replicate、Hugging Face Inferenceなど)は存在しますが、以下の制約がありました。

  • レート制限: 無料枠では1日あたりの生成回数に厳しい制限
  • 処理速度: 共有GPUのため、ピーク時は処理が遅延
  • カスタマイズ性: モデルの細かいチューニングができない

結果として、自前でGPUサーバーを立てる必要があると判明しました。

理由2: デプロイの壁 - 容量制限との戦い

次に、バックエンドAPIをクラウドサービスにデプロイしようと試みました。

試したサービスと結果:

  1. Hugging Face Spaces(GPU)

    • 申請したが審査に時間がかかる
    • 無料枠では制限が厳しい
  2. Render

    • Dockerイメージが512MBの制限を大幅に超過
    • Stable Diffusion XLのモデルだけで数GB必要
    • デプロイ失敗
  3. Vercel(フロントエンド)

    • ✅ フロントエンドはデプロイ成功
    • ❌ バックエンドAPIは対応不可(サーバーレス関数の制約)

理由3: 自宅サーバーという選択肢の限界

最終的に、自宅のGPU搭載PCでAPIサーバーを運用することにしましたが、これには大きな問題がありました。

  • 高性能PCが必須: RTX 3060以上のGPUが推奨
  • 電気代: 24時間稼働させるとコストが膨大
  • ネットワーク: 固定IPや動的DNSの設定が必要
  • セキュリティ: 自宅サーバーのリスク管理

一般のユーザーが気軽に試せる環境ではありませんでした。

OSS化の決断 - 知見を共有し、コミュニティで育てる

これらの課題を経験して気づいたことは、「同じ壁に直面している開発者は他にもたくさんいるはず」ということでした。

そこで方針を転換し、以下の理念でOSS化することを決意しました。

OSS化の目的:

  1. スターターテンプレートとして活用

    • フルスタック構成の実装例を提供
    • 各自の環境でカスタマイズ可能な設計
  2. 知見の共有

    • AI画像生成アプリの実装方法
    • FastAPIとNext.jsの連携パターン
    • GPU最適化のテクニック
  3. コミュニティでの改善

    • バグ修正や機能追加をコミュニティで協力
    • より多くの環境での動作検証

このリポジトリを使えば、誰でも自分の環境で画像生成アプリを立ち上げ、カスタマイズできます。 ローカル環境でも、自前のクラウドサーバーでも動作します。

技術スタックの全体像

フロントエンド技術

Framework: Next.js 16 (App Router)
Language: TypeScript
UI Library: React 19
Styling: Tailwind CSS 4
Component Library: Radix UI
Icons: Lucide React
State Management: React Hooks

選定理由:

  • Next.js 16 App Router: サーバーコンポーネントによるパフォーマンス最適化
  • TypeScript: 型安全性による開発時のバグ削減
  • Tailwind CSS 4: ユーティリティファーストによる高速スタイリング
  • Radix UI: アクセシビリティを考慮した堅牢なコンポーネント

バックエンド技術

Framework: FastAPI
Language: Python 3.10+
AI Model: Stable Diffusion XL + LCM LoRA
Library: Diffusers, Torch
Validation: Pydantic
Async: asyncio, BackgroundTasks

選定理由:

  • FastAPI: 高速な非同期処理と自動ドキュメント生成(OpenAPI)
  • Stable Diffusion XL: 高品質な画像生成
  • LCM(Latent Consistency Model): 少ないステップ数で高速生成
  • Pydantic: 堅牢なデータバリデーション

全体アーキテクチャ

┌─────────────────────────────────────┐
│   ユーザー (ブラウザ)                │
└─────────────┬───────────────────────┘
              │
              ↓
┌─────────────────────────────────────┐
│   Next.js Frontend (Vercel)         │
│   - 画像アップロードUI               │
│   - スタイル選択                     │
│   - 結果表示                         │
│   - API Routes (プロキシ)           │
└─────────────┬───────────────────────┘
              │ HTTP/REST API
              ↓
┌─────────────────────────────────────┐
│   FastAPI Backend (自宅/クラウド)    │
│   - 画像生成エンドポイント           │
│   - 非同期タスク管理                 │
│   - Stable Diffusion XL + LCM       │
└─────────────┬───────────────────────┘
              │
              ↓
┌─────────────────────────────────────┐
│   GPU (CUDA)                        │
│   - モデル推論                       │
│   - 画像生成処理                     │
└─────────────────────────────────────┘

プロジェクト構造の詳細

フロントエンド構造

rapidgen/
├── app/
│   ├── api/                    # Next.js API Routes
│   │   ├── generate/
│   │   │   └── route.ts        # 画像生成プロキシ
│   │   └── status/
│   │       └── route.ts        # ステータス確認プロキシ
│   ├── editor/                 # エディター画面
│   │   └── page.tsx
│   ├── layout.tsx              # ルートレイアウト
│   ├── page.tsx                # ランディングページ
│   └── globals.css             # グローバルスタイル
├── components/
│   ├── editor/
│   │   ├── ImageUploadBox.tsx      # 画像アップロード
│   │   ├── StyleSelector.tsx       # スタイル選択
│   │   ├── ControlPanel.tsx        # 詳細設定パネル
│   │   └── ResultDisplay.tsx       # 結果表示
│   └── ui/                     # 再利用可能UIコンポーネント
│       ├── button.tsx
│       ├── slider.tsx
│       └── select.tsx
├── lib/
│   ├── api.ts                  # API通信ユーティリティ
│   ├── image-utils.ts          # 画像処理
│   └── utils.ts                # 汎用ユーティリティ
└── public/
    └── presets/                # スタイルプリセット画像

バックエンド構造

rapidgen-api/
├── app/
│   ├── main.py                 # FastAPIアプリケーション
│   └── routers/
│       ├── image_generation.py # 画像生成ルーター
│       └── health.py           # ヘルスチェック
├── config/
│   └── settings.py             # 環境設定
├── models/
│   └── schemas.py              # Pydanticモデル定義
├── services/
│   ├── image_generation.py     # 画像生成サービス
│   └── task_manager.py         # タスク管理
├── utils/
│   ├── logging_config.py       # ログ設定
│   └── exceptions.py           # カスタム例外
├── tests/
│   ├── test_generation.py
│   └── test_health.py
└── requirements.txt

バックエンドの核心実装 - コード詳細解説

1. FastAPIアプリケーションのエントリーポイント

まずは、app/main.pyの実装を見ていきましょう。この部分がバックエンド全体の起点となります。

# app/main.py
"""
メインアプリケーションモジュール

Stable Diffusion XL (SDXL) + LCM を使用したimg2img生成APIの
FastAPIアプリケーションエントリーポイントです。
"""

import logging
from contextlib import asynccontextmanager
from typing import AsyncGenerator

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware

from config.settings import settings
from utils.logging_config import setup_logging
from utils.exceptions import BaseImageGenerationError
from app.routers import image_generation, health
from services.task_manager import task_manager

# ログ設定の初期化
setup_logging()
logger = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
    """
    アプリケーションのライフサイクル管理
    
    起動時と終了時の処理を定義します:
    - 起動時: サービスの初期化とヘルスチェック
    - 終了時: リソースのクリーンアップ
    """
    # === 起動時処理 ===
    logger.info("=" * 60)
    logger.info("Stable Diffusion XL LCM API を起動中...")
    logger.info(f"デバイス: {settings.device}")
    logger.info(f"モデル: {settings.model_id}")
    logger.info(f"LoRA: {settings.lora_id}")
    logger.info("=" * 60)
    
    try:
        # 画像生成サービスの初期化確認
        from services.image_generation import image_generation_service
        if not image_generation_service.is_available:
            logger.error("画像生成サービスの初期化に失敗しました")
            raise RuntimeError("重要なサービスの初期化に失敗しました")
        
        logger.info("すべてのサービスが正常に初期化されました")
        logger.info("API サーバーが利用可能になりました")
        
    except Exception as e:
        logger.error(f"アプリケーション起動エラー: {e}")
        raise
    
    # アプリケーション実行
    yield
    
    # === 終了時処理 ===
    logger.info("アプリケーションを終了中...")
    
    try:
        # タスクマネージャーのシャットダウン
        task_manager.shutdown()
        
        # 古いタスクのクリーンアップ
        cleaned_count = task_manager.cleanup_old_tasks(max_age_hours=1)
        if cleaned_count > 0:
            logger.info(f"終了時に {cleaned_count} 個の古いタスクをクリーンアップしました")
        
        logger.info("アプリケーションが正常に終了しました")
        
    except Exception as e:
        logger.error(f"終了時エラー: {e}")


# FastAPIアプリケーションインスタンスの作成
app = FastAPI(
    title=settings.app_title,
    description="""
    Stable Diffusion XL (SDXL) + Latent Consistency Model (LCM) を使用した
    高品質なimg2img画像生成APIです。
    
    ## 主な機能
    
    * **高速画像生成**: LCMにより少ないステップで高品質な画像を生成
    * **非同期処理**: 長時間の生成処理を背景で実行し、進捗を追跡可能
    * **柔軟なパラメータ**: プロンプト、強度、ガイダンス等を細かく調整
    * **GPU最適化**: CUDAが利用可能な環境でのメモリ効率的な処理
    """,
    version="1.0.0",
    lifespan=lifespan,
    debug=settings.debug,
)

# CORS設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 本番環境では適切なオリジンを設定
    allow_credentials=True,
    allow_methods=["GET", "POST"],
    allow_headers=["*"],
)

# ルーターの登録
app.include_router(
    image_generation.router,
    tags=["画像生成"]
)

app.include_router(
    health.router,
    tags=["システム"]
)

このコードのポイント:

  1. lifespanコンテキストマネージャー: アプリケーションの起動・終了時の処理を一元管理
  2. サービス初期化の確認: Stable Diffusion XLモデルが正しくロードされているか起動時にチェック
  3. リソースのクリーンアップ: 終了時に古いタスクを自動削除
  4. CORS設定: フロントエンドからのAPIアクセスを許可
  5. 自動ドキュメント生成: FastAPIが自動的にOpenAPI仕様書を生成(/docsでアクセス可能)

2. 画像生成APIエンドポイント

次に、実際の画像生成リクエストを処理するapp/routers/image_generation.pyを見ていきます。

# app/routers/image_generation.py
"""
画像生成API ルーターモジュール

img2img画像生成に関連するAPIエンドポイントを定義します。
非同期タスクの作成と状態確認のエンドポイントを提供します。
"""

import logging
from typing import Union
from fastapi import APIRouter, HTTPException, status
from fastapi.responses import JSONResponse

from models.schemas import (
    ImageGenerationRequest,
    TaskCreationResponse,
    TaskStatusResponse,
    TaskCompletedResponse,
    TaskStatus,
    ErrorResponse
)
from services.task_manager import task_manager

logger = logging.getLogger(__name__)

router = APIRouter(
    prefix="/api/v1/generate",
    tags=["画像生成"],
    responses={
        400: {"model": ErrorResponse, "description": "不正なリクエスト"},
        404: {"model": ErrorResponse, "description": "タスクが見つかりません"},
        500: {"model": ErrorResponse, "description": "内部サーバーエラー"},
    }
)


@router.post(
    "/",
    response_model=TaskCreationResponse,
    status_code=status.HTTP_201_CREATED,
    summary="画像生成タスクの作成",
    description="""
    Stable Diffusion XL (SDXL) + LCM を使用したimg2img画像生成タスクを作成します。
    
    **処理の流れ:**
    1. リクエストパラメータの検証
    2. 非同期タスクの作成とキューへの追加
    3. タスクIDを即座に返却
    4. バックグラウンドで画像生成を実行
    """
)
async def create_generation_task(request: ImageGenerationRequest) -> TaskCreationResponse:
    """
    画像生成タスクを作成します
    
    Args:
        request: 画像生成リクエストパラメータ
        
    Returns:
        TaskCreationResponse: 作成されたタスクのID
    """
    try:
        # 基本的なバリデーション
        if not request.prompt or not request.prompt.strip():
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="プロンプトは必須です"
            )
        
        if not request.init_image:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="元画像(init_image)は必須です"
            )
        
        logger.info(f"画像生成タスクを作成中 - プロンプト: {request.prompt[:50]}...")
        
        # タスクの作成
        task_id = task_manager.create_task(request)
        
        logger.info(f"タスクが作成されました: {task_id}")
        
        return TaskCreationResponse(task_id=task_id)
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"タスク作成エラー: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="タスクの作成に失敗しました"
        )


@router.get(
    "/tasks/{task_id}",
    response_model=Union[TaskStatusResponse, TaskCompletedResponse],
    summary="タスク状態の確認",
    description="""
    指定されたタスクIDの実行状態と進捗を確認します。
    
    **返却される状態:**
    - `PENDING`: 実行待機中
    - `IN_PROGRESS`: 実行中(progress フィールドで進捗確認可能)
    - `COMPLETED`: 完了(dataUrl フィールドで結果画像を取得可能)
    - `FAILED`: 失敗(エラー詳細がHTTPエラーレスポンスで返却)
    """
)
async def get_task_status(task_id: str) -> Union[TaskStatusResponse, TaskCompletedResponse]:
    """
    タスクの実行状態を取得します
    """
    try:
        # タスク情報の取得
        task_info = task_manager.get_task_status(task_id)
        
        if not task_info:
            logger.warning(f"存在しないタスクIDが指定されました: {task_id}")
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="指定されたタスクが見つかりません"
            )
        
        # 失敗した場合の処理
        if task_info.status == TaskStatus.FAILED:
            error_detail = task_info.error or "不明なエラーが発生しました"
            logger.error(f"タスク失敗: {task_id} - {error_detail}")
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail=f"タスクが失敗しました: {error_detail}"
            )
        
        # 完了した場合の処理
        if task_info.status == TaskStatus.COMPLETED:
            if not task_info.result:
                logger.error(f"完了タスクの結果が存在しません: {task_id}")
                raise HTTPException(
                    status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                    detail="タスクは完了しましたが、結果データが見つかりません"
                )
            
            logger.info(f"タスク完了結果を返却: {task_id}")
            return TaskCompletedResponse(
                status=task_info.status,
                progress=task_info.progress,
                dataUrl=task_info.result
            )
        
        # 実行中または待機中の場合
        return TaskStatusResponse(
            status=task_info.status,
            progress=task_info.progress
        )
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"タスク状態取得エラー: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="タスク状態の取得に失敗しました"
        )

このコードのポイント:

  1. 非同期タスクパターン: 生成リクエストは即座にタスクIDを返し、処理はバックグラウンドで実行
  2. ポーリング設計: フロントエンドは定期的に/tasks/{task_id}をポーリングして進捗確認
  3. 詳細なエラーハンドリング: バリデーションエラー、タスク未検出、実行エラーを適切に分類
  4. 型安全な設計: Pydanticモデルによる入出力の検証
  5. ログ記録: すべての重要な処理にログを記録してデバッグを容易化

3. エラーハンドリングとカスタム例外

バックエンドの品質を支えるエラー処理も見ていきます。

# app/main.py(エラーハンドラー部分)

@app.exception_handler(BaseImageGenerationError)
async def handle_image_generation_error(
    request: Request, 
    exc: BaseImageGenerationError
) -> JSONResponse:
    """
    画像生成関連エラーのハンドラー
    
    カスタム例外を適切なHTTPレスポンスに変換します。
    """
    logger.error(
        f"画像生成エラー: {exc.message} "
        f"(コード: {exc.error_code}, 詳細: {exc.details})"
    )
    
    # エラーコードに応じてHTTPステータスを決定
    status_code = 500  # デフォルト
    if exc.error_code in ["VALIDATION_FAILED"]:
        status_code = 400
    elif exc.error_code in ["SERVICE_UNAVAILABLE"]:
        status_code = 503
    elif exc.error_code in ["RESOURCE_EXHAUSTED"]:
        status_code = 429
    
    return JSONResponse(
        status_code=status_code,
        content={
            "detail": exc.message,
            "error_code": exc.error_code,
            "timestamp": exc.details.get("timestamp") if exc.details else None
        }
    )


@app.exception_handler(HTTPException)
async def handle_http_exception(
    request: Request, 
    exc: HTTPException
) -> JSONResponse:
    """FastAPI HTTPExceptionのハンドラー"""
    logger.warning(
        f"HTTP例外: {exc.status_code} - {exc.detail} "
        f"(パス: {request.url.path})"
    )
    
    return JSONResponse(
        status_code=exc.status_code,
        content={"detail": exc.detail}
    )


@app.exception_handler(500)
async def handle_internal_server_error(
    request: Request, 
    exc: Exception
) -> JSONResponse:
    """予期しない内部サーバーエラーのハンドラー"""
    logger.error(
        f"予期しないエラー: {str(exc)} (パス: {request.url.path})",
        exc_info=True
    )
    
    return JSONResponse(
        status_code=500,
        content={
            "detail": "内部サーバーエラーが発生しました",
            "error_code": "INTERNAL_SERVER_ERROR"
        }
    )

エラーハンドリングのメリット:

  • クライアントに適切なHTTPステータスコードを返却
  • エラー原因を明確に伝えるメッセージ
  • すべてのエラーをログに記録してデバッグを支援
  • 本番環境で予期しないエラーが発生しても適切にハンドリング

フロントエンドの核心実装 - コード詳細解説

1. 画像エディター画面

メインの画像編集画面app/editor/page.tsxの実装を見ていきます。

// app/editor/page.tsx
'use client';

import { useState } from 'react';
import { ImageUploadBox } from '@/components/editor/ImageUploadBox';
import { StyleSelector } from '@/components/editor/StyleSelector';
import { ControlPanel } from '@/components/editor/ControlPanel';
import { ResultDisplay } from '@/components/editor/ResultDisplay';
import { useImageGeneration } from '@/hooks/useImageGeneration';
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';

export default function EditorPage() {
  const [uploadedImage, setUploadedImage] = useState<string | null>(null);
  const [selectedStyle, setSelectedStyle] = useState('anime');
  const [options, setOptions] = useState({
    strength: 0.8,
    guidance: 7.5,
    steps: 4,
  });

  const { 
    generate, 
    result, 
    loading, 
    error, 
    progress 
  } = useImageGeneration();

  const handleGenerate = async () => {
    if (!uploadedImage) return;

    await generate({
      image: uploadedImage,
      style: selectedStyle,
      options,
    });
  };

  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold text-center mb-8 bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">
        RapidGen - AI画像生成
      </h1>

      <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
        {/* 左側: コントロールパネル */}
        <div className="space-y-6">
          <div className="glass-effect p-6 rounded-xl">
            <h2 className="text-xl font-semibold mb-4">画像をアップロード</h2>
            <ImageUploadBox
              onUpload={setUploadedImage}
              value={uploadedImage}
            />
          </div>

          <div className="glass-effect p-6 rounded-xl">
            <h2 className="text-xl font-semibold mb-4">スタイルを選択</h2>
            <StyleSelector
              value={selectedStyle}
              onChange={setSelectedStyle}
            />
          </div>

          <div className="glass-effect p-6 rounded-xl">
            <h2 className="text-xl font-semibold mb-4">詳細設定</h2>
            <ControlPanel
              options={options}
              onChange={setOptions}
            />
          </div>

          <Button
            onClick={handleGenerate}
            disabled={!uploadedImage || loading}
            className="w-full py-6 text-lg"
            size="lg"
          >
            {loading ? (
              <>
                <Loader2 className="mr-2 h-5 w-5 animate-spin" />
                生成中... {progress}%
              </>
            ) : (
              '画像を生成'
            )}
          </Button>

          {error && (
            <div className="p-4 bg-red-500/10 border border-red-500 rounded-lg text-red-500">
              エラー: {error}
            </div>
          )}
        </div>

        {/* 右側: 結果表示 */}
        <div className="glass-effect p-6 rounded-xl">
          <h2 className="text-xl font-semibold mb-4">生成結果</h2>
          <ResultDisplay
            original={uploadedImage}
            generated={result}
            loading={loading}
            progress={progress}
          />
        </div>
      </div>
    </div>
  );
}

このコードのポイント:

  1. 状態管理: useStateで画像、スタイル、オプションを管理
  2. レスポンシブデザイン: grid-cols-1 lg:grid-cols-2で画面サイズに応じたレイアウト
  3. リアルタイムフィードバック: 生成中は進捗率を表示
  4. エラー表示: エラーが発生した場合は赤枠で明示

2. Next.js API Routes - プロキシの実装

フロントエンドからバックエンドAPIへのプロキシを実装します。

// app/api/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';

const API_BASE_URL = process.env.API_URL || 'http://localhost:8000';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();

    // バックエンドAPIへプロキシ
    const response = await fetch(`${API_BASE_URL}/api/v1/generate/`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });

    if (!response.ok) {
      const errorData = await response.json();
      return NextResponse.json(
        { error: errorData.detail || 'API request failed' },
        { status: response.status }
      );
    }

    const data = await response.json();
    return NextResponse.json(data);

  } catch (error) {
    console.error('API proxy error:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}
// app/api/generate/tasks/[taskId]/route.ts
import { NextRequest, NextResponse } from 'next/server';

const API_BASE_URL = process.env.API_URL || 'http://localhost:8000';

export async function GET(
  request: NextRequest,
  { params }: { params: { taskId: string } }
) {
  try {
    const { taskId } = params;

    const response = await fetch(
      `${API_BASE_URL}/api/v1/generate/tasks/${taskId}`,
      {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      }
    );

    if (!response.ok) {
      const errorData = await response.json();
      return NextResponse.json(
        { error: errorData.detail || 'Failed to get task status' },
        { status: response.status }
      );
    }

    const data = await response.json();
    return NextResponse.json(data);

  } catch (error) {
    console.error('API proxy error:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

プロキシを使う理由:

  1. CORS問題の回避: ブラウザから直接バックエンドにアクセスする際のCORS制約を回避
  2. APIキーの隠蔽: 将来的にAPIキーが必要になった場合、サーバーサイドで管理
  3. レート制限の実装: プロキシ層でリクエスト制限を追加可能
  4. 環境変数の活用: バックエンドURLを環境変数で管理

AI活用による開発効率化の実践

1. ChatGPTでのコード生成

RapidGenの開発では、バックエンドのPythonコードも含め、ほぼすべてAIで生成しました。

具体例1: FastAPIルーターの生成

ChatGPTへのプロンプト:

FastAPIで画像生成APIを作成してください。
要件:
- img2imgの画像生成
- 非同期タスク処理(即座にタスクIDを返す)
- タスクステータスの確認エンドポイント
- Pydanticでのバリデーション
- 詳細なログ記録
- エラーハンドリング

生成されたコードをベースに、Stable Diffusion XLとの連携部分を追加するだけで、本番レベルのAPIが完成しました。

具体例2: タスクマネージャーの実装

複雑な非同期タスク管理も、ChatGPTに以下のように依頼:

Pythonで非同期タスク管理システムを作成してください。
要件:
- タスクの作成、実行、ステータス確認
- スレッドセーフな実装
- 古いタスクの自動クリーンアップ
- 進捗率の追跡

結果、以下のような高品質なコードが生成されました:

# services/task_manager.py(簡略版)
import uuid
import threading
from typing import Dict, Optional
from datetime import datetime, timedelta
from models.schemas import ImageGenerationRequest, TaskInfo, TaskStatus

class TaskManager:
    def __init__(self):
        self.tasks: Dict[str, TaskInfo] = {}
        self.lock = threading.Lock()
        
    def create_task(self, request: ImageGenerationRequest) -> str:
        """新しいタスクを作成"""
        task_id = str(uuid.uuid4())
        
        with self.lock:
            self.tasks[task_id] = TaskInfo(
                id=task_id,
                status=TaskStatus.PENDING,
                progress=0,
                created_at=datetime.now()
            )
        
        # バックグラウンドで実行
        threading.Thread(
            target=self._execute_task,
            args=(task_id, request)
        ).start()
        
        return task_id
    
    def get_task_status(self, task_id: str) -> Optional[TaskInfo]:
        """タスクの状態を取得"""
        with self.lock:
            return self.tasks.get(task_id)
    
    def cleanup_old_tasks(self, max_age_hours: int = 24) -> int:
        """古いタスクをクリーンアップ"""
        cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
        cleaned = 0
        
        with self.lock:
            old_tasks = [
                task_id for task_id, task in self.tasks.items()
                if task.created_at < cutoff_time
            ]
            for task_id in old_tasks:
                del self.tasks[task_id]
                cleaned += 1
        
        return cleaned

task_manager = TaskManager()

2. Cursor AIでのリファクタリング

Cursor AIは、既存コードの改善に特に優れています。

実例: コードの最適化

初期実装では、画像のBase64エンコード/デコードを各所で行っていましたが、Cursor AIに「このコードを共通ユーティリティに抽出してください」と指示するだけで、以下のように整理されました:

# utils/image_utils.py
import base64
from io import BytesIO
from PIL import Image
from typing import Union

def base64_to_image(base64_string: str) -> Image.Image:
    """Base64文字列をPIL Imageに変換"""
    # data:image/png;base64, のプレフィックスを除去
    if ',' in base64_string:
        base64_string = base64_string.split(',')[1]
    
    image_data = base64.b64decode(base64_string)
    return Image.open(BytesIO(image_data))

def image_to_base64(image: Image.Image, format: str = 'PNG') -> str:
    """PIL ImageをBase64文字列に変換"""
    buffered = BytesIO()
    image.save(buffered, format=format)
    img_str = base64.b64encode(buffered.getvalue()).decode()
    return f"data:image/{format.lower()};base64,{img_str}"

3. ドキュメント生成の自動化

README、APIドキュメント、コメントもAIで効率的に作成しました。

具体例: OpenAPI仕様書の自動生成

FastAPIは自動的にOpenAPI仕様を生成しますが、より詳細な説明をChatGPTで生成:

@router.post(
    "/",
    response_model=TaskCreationResponse,
    summary="画像生成タスクの作成",
    description="""
    Stable Diffusion XL (SDXL) + LCM を使用したimg2img画像生成タスクを作成します。
    
    **処理の流れ:**
    1. リクエストパラメータの検証
    2. 非同期タスクの作成とキューへの追加
    3. タスクIDを即座に返却
    4. バックグラウンドで画像生成を実行
    
    **注意事項:**
    - `init_image`は有効なBase64エンコードされた画像データである必要があります
    - 生成には時間がかかるため、返されたタスクIDで進捗を確認してください
    
    **使用例:**
    ```json
    {
      "init_image": "data:image/png;base64,iVBORw0KG...",
      "prompt": "anime style, beautiful landscape",
      "strength": 0.8,
      "guidance_scale": 7.5,
      "num_inference_steps": 4
    }
    ```
    """
)
async def create_generation_task(request: ImageGenerationRequest):
    # 実装

このドキュメントをChatGPTで生成し、http://localhost:8000/docsで美しいインタラクティブなAPIドキュメントとして表示されます。

セットアップと実行方法

前提条件

必須:

  • Node.js 18.0以上
  • Python 3.10以上
  • CUDA対応GPU(推奨: RTX 3060以上、VRAMは8GB以上)

推奨:

  • Docker & Docker Compose
  • Git

フロントエンドのセットアップ

# 1. リポジトリのクローン
git clone https://github.com/YuuOhnuki/RapidGen.git
cd RapidGen

# 2. 依存関係のインストール
npm install
# または
yarn install
# または
pnpm install

# 3. 環境変数の設定
cp .env.example .env.local

.env.localの内容:

# バックエンドAPIのURL
API_URL=http://localhost:8000

# 本番環境の場合
# API_URL=https://your-api-server.com
# 4. 開発サーバーの起動
npm run dev

# 5. ブラウザでアクセス
# http://localhost:3000

バックエンドのセットアップ

# 1. リポジトリのクローン
git clone https://github.com/YuuOhnuki/rapidgen-api.git
cd rapidgen-api

# 2. Python仮想環境の作成
python -m venv venv

# Windows
venv\Scripts\activate

# macOS/Linux
source venv/bin/activate

# 3. 依存関係のインストール
pip install -r requirements.txt

# 4. 環境変数の設定
cp .env.example .env

.envの内容:

# デバイス設定(cuda または cpu)
DEVICE=cuda

# モデルID
MODEL_ID=stabilityai/stable-diffusion-xl-base-1.0

# LoRA ID
LORA_ID=latent-consistency/lcm-lora-sdxl

# デバッグモード
DEBUG=true
# 5. サーバーの起動
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

# 6. APIドキュメントにアクセス
# http://localhost:8000/docs

Dockerを使用したセットアップ(推奨)

より簡単にセットアップするには、Docker Composeを使用します。

# docker-compose.yml
version: '3.8'

services:
  frontend:
    build: ./RapidGen
    ports:
      - "3000:3000"
    environment:
      - API_URL=http://backend:8000
    depends_on:
      - backend

  backend:
    build: ./rapidgen-api
    ports:
      - "8000:8000"
    environment:
      - DEVICE=cuda
      - MODEL_ID=stabilityai/stable-diffusion-xl-base-1.0
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
# 起動
docker-compose up -d

# ログ確認
docker-compose logs -f

# 停止
docker-compose down

実際の使用例とAPI仕様

1. 画像生成フロー

ステップ1: 画像のアップロード

フロントエンドで画像をアップロードすると、Base64形式にエンコードされます。

ステップ2: 生成タスクの作成

curl -X POST "http://localhost:8000/api/v1/generate/" \
  -H "Content-Type: application/json" \
  -d '{
    "init_image": "data:image/png;base64,iVBORw0KG...",
    "prompt": "anime style, beautiful landscape, vibrant colors",
    "strength": 0.8,
    "guidance_scale": 7.5,
    "num_inference_steps": 4
  }'

レスポンス:

{
  "task_id": "550e8400-e29b-41d4-a716-446655440000"
}

ステップ3: タスクステータスの確認

curl "http://localhost:8000/api/v1/generate/tasks/550e8400-e29b-41d4-a716-446655440000"

実行中のレスポンス:

{
  "status": "IN_PROGRESS",
  "progress": 45
}

完了時のレスポンス:

{
  "status": "COMPLETED",
  "progress": 100,
  "dataUrl": "data:image/png;base64,iVBORw0KGgoAAAANS..."
}

2. Pydanticモデル定義

バックエンドのリクエスト/レスポンスは厳密に型定義されています。

# models/schemas.py
from pydantic import BaseModel, Field
from enum import Enum
from typing import Optional

class ImageGenerationRequest(BaseModel):
    """画像生成リクエスト"""
    init_image: str = Field(..., description="Base64エンコードされた元画像")
    prompt: str = Field(..., min_length=1, description="生成プロンプト")
    strength: float = Field(default=0.8, ge=0.0, le=1.0, description="変換強度")
    guidance_scale: float = Field(default=7.5, ge=1.0, le=20.0, description="ガイダンススケール")
    num_inference_steps: int = Field(default=4, ge=1, le=50, description="推論ステップ数")

class TaskStatus(str, Enum):
    """タスクの状態"""
    PENDING = "PENDING"
    IN_PROGRESS = "IN_PROGRESS"
    COMPLETED = "COMPLETED"
    FAILED = "FAILED"

class TaskCreationResponse(BaseModel):
    """タスク作成レスポンス"""
    task_id: str = Field(..., description="作成されたタスクのID")

class TaskStatusResponse(BaseModel):
    """タスクステータスレスポンス"""
    status: TaskStatus = Field(..., description="タスクの状態")
    progress: int = Field(default=0, ge=0, le=100, description="進捗率(%)")

class TaskCompletedResponse(TaskStatusResponse):
    """タスク完了レスポンス"""
    dataUrl: str = Field(..., description="生成された画像のData URL")

class ErrorResponse(BaseModel):
    """エラーレスポンス"""
    detail: str = Field(..., description="エラーの詳細")
    error_code: Optional[str] = Field(None, description="エラーコード")

スタイルプリセットの実装

RapidGenの特徴の一つが、豊富なスタイルプリセットです。

// app/editor/page.tsx(スタイルプリセット定義)
const stylePresets = [
  {
    id: 'anime',
    label: 'アニメ調',
    description: '日本のアニメ風のスタイル',
    prompt: 'anime style, cel shading, vibrant colors',
    thumbnail: '/presets/anime.jpg',
  },
  {
    id: 'ghibli',
    label: 'ジブリ風',
    description: 'スタジオジブリ作品のようなスタイル',
    prompt: 'studio ghibli style, watercolor, soft lighting, whimsical',
    thumbnail: '/presets/ghibli.jpg',
  },
  {
    id: 'monochrome',
    label: 'モノクロ',
    description: '白黒写真のようなスタイル',
    prompt: 'black and white, high contrast, film noir, dramatic lighting',
    thumbnail: '/presets/monochrome.jpg',
  },
  {
    id: '3d-render',
    label: '3Dレンダリング',
    description: 'リアルな3DCG風のスタイル',
    prompt: '3d render, octane render, hyperrealistic, unreal engine',
    thumbnail: '/presets/3d-render.jpg',
  },
  {
    id: 'oil-painting',
    label: '油絵',
    description: 'クラシックな油絵タッチ',
    prompt: 'oil painting, impressionism, thick brush strokes, textured',
    thumbnail: '/presets/oil-painting.jpg',
  },
  {
    id: 'cyberpunk',
    label: 'サイバーパンク',
    description: 'ネオン輝く未来都市',
    prompt: 'cyberpunk style, neon lights, futuristic, dystopian, blade runner',
    thumbnail: '/presets/cyberpunk.jpg',
  },
];

新しいスタイルを追加するのも簡単です。配列に要素を追加するだけで、UIに自動的に反映されます。

パフォーマンス最適化テクニック

1. 画像の最適化

アップロード前に画像サイズを最適化することで、通信量とサーバー負荷を削減します。

// lib/image-utils.ts
export async function optimizeImage(
  file: File,
  maxWidth: number = 1024,
  maxHeight: number = 1024,
  quality: number = 0.9
): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = (e) => {
      const img = new Image();
      
      img.onload = () => {
        const canvas = document.createElement('canvas');
        let width = img.width;
        let height = img.height;

        // アスペクト比を維持してリサイズ
        if (width > height) {
          if (width > maxWidth) {
            height = (height * maxWidth) / width;
            width = maxWidth;
          }
        } else {
          if (height > maxHeight) {
            width = (width * maxHeight) / height;
            height = maxHeight;
          }
        }

        canvas.width = width;
        canvas.height = height;

        const ctx = canvas.getContext('2d')!;
        ctx.drawImage(img, 0, 0, width, height);

        // JPEGで圧縮してBase64に変換
        const dataUrl = canvas.toDataURL('image/jpeg', quality);
        resolve(dataUrl);
      };

      img.onerror = () => reject(new Error('画像の読み込みに失敗しました'));
      img.src = e.target?.result as string;
    };

    reader.onerror = () => reject(new Error('ファイルの読み込みに失敗しました'));
    reader.readAsDataURL(file);
  });
}

2. バックエンドのGPUメモリ管理

Stable Diffusion XLは大量のVRAMを消費するため、適切なメモリ管理が重要です。

# services/image_generation.py
import torch
from diffusers import StableDiffusionXLImg2ImgPipeline, LCMScheduler
from config.settings import settings

class ImageGenerationService:
    def __init__(self):
        self.pipe = None
        self.device = settings.device
        self._initialize_pipeline()
    
    def _initialize_pipeline(self):
        """パイプラインの初期化"""
        try:
            # パイプラインのロード
            self.pipe = StableDiffusionXLImg2ImgPipeline.from_pretrained(
                settings.model_id,
                torch_dtype=torch.float16 if self.device == "cuda" else torch.float32,
                variant="fp16" if self.device == "cuda" else None,
            )
            
            # LCM LoRAの適用
            self.pipe.load_lora_weights(settings.lora_id)
            
            # スケジューラーの設定
            self.pipe.scheduler = LCMScheduler.from_config(
                self.pipe.scheduler.config
            )
            
            # GPUに転送
            self.pipe.to(self.device)
            
            # メモリ最適化
            if self.device == "cuda":
                # 注意機構の最適化
                self.pipe.enable_attention_slicing()
                # VAEのタイリング(大きな画像でもVRAMを節約)
                self.pipe.enable_vae_tiling()
            
            logger.info("画像生成パイプラインを初期化しました")
            
        except Exception as e:
            logger.error(f"パイプライン初期化エラー: {e}")
            raise
    
    def generate(
        self,
        init_image: Image.Image,
        prompt: str,
        strength: float = 0.8,
        guidance_scale: float = 1.0,
        num_inference_steps: int = 4,
    ) -> Image.Image:
        """画像生成を実行"""
        try:
            # 推論実行
            with torch.inference_mode():
                result = self.pipe(
                    prompt=prompt,
                    image=init_image,
                    strength=strength,
                    guidance_scale=guidance_scale,
                    num_inference_steps=num_inference_steps,
                )
            
            # メモリクリーンアップ
            if self.device == "cuda":
                torch.cuda.empty_cache()
            
            return result.images[0]
            
        except Exception as e:
            logger.error(f"画像生成エラー: {e}")
            # エラー時もメモリをクリーンアップ
            if self.device == "cuda":
                torch.cuda.empty_cache()
            raise

# シングルトンインスタンス
image_generation_service = ImageGenerationService()

メモリ最適化のポイント:

  1. enable_attention_slicing(): 注意機構を小さなチャンクに分割してVRAM使用量を削減
  2. enable_vae_tiling(): VAEデコードをタイル状に分割して処理
  3. torch.inference_mode(): 勾配計算を無効化してメモリ節約
  4. torch.cuda.empty_cache(): 使用済みメモリを解放

3. フロントエンドのレンダリング最適化

// components/editor/ResultDisplay.tsx
import { memo } from 'react';
import { Loader2, Download } from 'lucide-react';
import { Button } from '@/components/ui/button';

interface ResultDisplayProps {
  original: string | null;
  generated: string | null;
  loading: boolean;
  progress: number;
}

export const ResultDisplay = memo(function ResultDisplay({
  original,
  generated,
  loading,
  progress,
}: ResultDisplayProps) {
  const handleDownload = () => {
    if (!generated) return;

    const link = document.createElement('a');
    link.href = generated;
    link.download = `rapidgen-${Date.now()}.png`;
    link.click();
  };

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
      {/* オリジナル画像 */}
      <div className="space-y-2">
        <h3 className="text-sm font-medium text-gray-400">オリジナル</h3>
        <div className="aspect-square rounded-lg overflow-hidden bg-gray-800/50 border border-gray-700">
          {original ? (
            <img
              src={original}
              alt="Original"
              className="w-full h-full object-cover"
              loading="lazy"
            />
          ) : (
            <div className="w-full h-full flex items-center justify-center text-gray-500">
              画像をアップロードしてください
            </div>
          )}
        </div>
      </div>

      {/* 生成画像 */}
      <div className="space-y-2">
        <h3 className="text-sm font-medium text-gray-400">生成結果</h3>
        <div className="aspect-square rounded-lg overflow-hidden bg-gray-800/50 border border-gray-700">
          {loading ? (
            <div className="w-full h-full flex flex-col items-center justify-center space-y-4">
              <Loader2 className="h-12 w-12 animate-spin text-purple-500" />
              <div className="text-center">
                <p className="text-sm text-gray-400">生成中...</p>
                <p className="text-2xl font-bold text-purple-500">{progress}%</p>
              </div>
            </div>
          ) : generated ? (
            <>
              <img
                src={generated}
                alt="Generated"
                className="w-full h-full object-cover"
                loading="lazy"
              />
              <div className="p-4">
                <Button
                  onClick={handleDownload}
                  className="w-full"
                  variant="outline"
                >
                  <Download className="mr-2 h-4 w-4" />
                  ダウンロード
                </Button>
              </div>
            </>
          ) : (
            <div className="w-full h-full flex items-center justify-center text-gray-500">
              生成結果がここに表示されます
            </div>
          )}
        </div>
      </div>
    </div>
  );
});

最適化のポイント:

  1. memo: 不要な再レンダリングを防止
  2. loading="lazy": 画像の遅延読み込み
  3. 条件付きレンダリング: 必要な要素のみをレンダリング

デザインのこだわり - Glassmorphism

RapidGenは、モダンなGlassmorphismデザインを採用しています。

/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --primary: 263.4 70% 50.4%;
    --primary-foreground: 210 40% 98%;
  }
}

@layer components {
  .glass-effect {
    @apply bg-white/5 backdrop-blur-xl border border-white/10 shadow-2xl;
  }

  .glass-effect-hover {
    @apply glass-effect transition-all duration-300 hover:bg-white/10 hover:border-white/20;
  }

  .gradient-text {
    @apply bg-gradient-to-r from-purple-400 to-blue-500 bg-clip-text text-transparent;
  }

  .btn-primary {
    @apply bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white font-semibold py-3 px-6 rounded-lg shadow-lg transition-all duration-300 hover:shadow-purple-500/50;
  }
}

/* カスタムスクロールバー */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-track {
  background: rgba(0, 0, 0, 0.1);
}

::-webkit-scrollbar-thumb {
  background: rgba(139, 92, 246, 0.5);
  border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
  background: rgba(139, 92, 246, 0.7);
}

テストの実装

バックエンドのテスト

# tests/test_generation.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
import base64
from PIL import Image
from io import BytesIO

client = TestClient(app)

def create_test_image() -> str:
    """テスト用画像を生成"""
    img = Image.new('RGB', (512, 512), color='red')
    buffered = BytesIO()
    img.save(buffered, format="PNG")
    img_str = base64.b64encode(buffered.getvalue()).decode()
    return f"data:image/png;base64,{img_str}"

def test_create_generation_task():
    """画像生成タスク作成のテスト"""
    response = client.post(
        "/api/v1/generate/",
        json={
            "init_image": create_test_image(),
            "prompt": "anime style landscape",
            "strength": 0.8,
            "guidance_scale": 7.5,
            "num_inference_steps": 4,
        }
    )
    
    assert response.status_code == 201
    data = response.json()
    assert "task_id" in data
    assert isinstance(data["task_id"], str)

def test_get_task_status():
    """タスクステータス取得のテスト"""
    # まずタスクを作成
    create_response = client.post(
        "/api/v1/generate/",
        json={
            "init_image": create_test_image(),
            "prompt": "test prompt",
        }
    )
    task_id = create_response.json()["task_id"]
    
    # ステータスを取得
    status_response = client.get(f"/api/v1/generate/tasks/{task_id}")
    assert status_response.status_code == 200
    
    data = status_response.json()
    assert "status" in data
    assert data["status"] in ["PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"]

def test_invalid_prompt():
    """無効なプロンプトのテスト"""
    response = client.post(
        "/api/v1/generate/",
        json={
            "init_image": create_test_image(),
            "prompt": "",  # 空のプロンプト
        }
    )
    
    assert response.status_code == 400

def test_missing_image():
    """画像なしのリクエストのテスト"""
    response = client.post(
        "/api/v1/generate/",
        json={
            "prompt": "test prompt",
            # init_imageが欠落
        }
    )
    
    assert response.status_code == 422  # Pydantic validation error

def test_nonexistent_task():
    """存在しないタスクIDのテスト"""
    response = client.get("/api/v1/generate/tasks/nonexistent-id")
    assert response.status_code == 404

フロントエンドのテスト

// __tests__/useImageGeneration.test.ts
import { renderHook, act, waitFor } from '@testing-library/react';
import { useImageGeneration } from '@/hooks/useImageGeneration';

// モックのセットアップ
global.fetch = jest.fn();

describe('useImageGeneration', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should create task and poll status', async () => {
    // モックレスポンスの設定
    (global.fetch as jest.Mock)
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ task_id: 'test-task-id' }),
      })
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ status: 'IN_PROGRESS', progress: 50 }),
      })
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          status: 'COMPLETED',
          progress: 100,
          dataUrl: 'data:image/png;base64,test',
        }),
      });

    const { result } = renderHook(() => useImageGeneration());

    // 画像生成を開始
    await act(async () => {
      await result.current.generate({
        image: 'test-image',
        style: 'anime',
        options: {},
      });
    });

    // 完了を待機
    await waitFor(
      () => {
        expect(result.current.loading).toBe(false);
      },
      { timeout: 5000 }
    );

    expect(result.current.result).toBe('data:image/png;base64,test');
    expect(result.current.error).toBeNull();
  });

  it('should handle errors', async () => {
    (global.fetch as jest.Mock).mockRejectedValueOnce(
      new Error('Network error')
    );

    const { result } = renderHook(() => useImageGeneration());

    await act(async () => {
      await result.current.generate({
        image: 'test-image',
        style: 'anime',
        options: {},
      });
    });

    await waitFor(() => {
      expect(result.current.error).toBe('Network error');
    });

    expect(result.current.loading).toBe(false);
    expect(result.current.result).toBeNull();
  });
});

デプロイメント戦略

フロントエンドのデプロイ(Vercel)

Vercelへのデプロイは非常に簡単です。

# 1. Vercel CLIのインストール
npm install -g vercel

# 2. プロジェクトディレクトリでログイン
vercel login

# 3. デプロイ
vercel

# 4. 本番環境へのデプロイ
vercel --prod

環境変数の設定:

API_URL=https://your-backend-server.com

バックエンドのデプロイ(自宅サーバー/クラウド)

オプション1: 自宅サーバー

# 1. サーバーのセットアップ
sudo apt update
sudo apt install python3.10 python3-pip nginx

# 2. リポジトリのクローン
git clone https://github.com/YuuOhnuki/rapidgen-api.git
cd rapidgen-api

# 3. 依存関係のインストール
pip install -r requirements.txt

# 4. Systemdサービスの作成
sudo nano /etc/systemd/system/rapidgen-api.service
[Unit]
Description=RapidGen API Server
After=network.target

[Service]
User=your-user
WorkingDirectory=/path/to/rapidgen-api
Environment="PATH=/path/to/venv/bin"
ExecStart=/path/to/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
Restart=always

[Install]
WantedBy=multi-user.target
# 5. サービスの起動
sudo systemctl daemon-reload
sudo systemctl start rapidgen-api
sudo systemctl enable rapidgen-api

# 6. Nginxリバースプロキシの設定
sudo nano /etc/nginx/sites-available/rapidgen-api
server {
    listen 80;
    server_name your-domain.com;

    location / {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

オプション2: クラウド(AWS EC2)

# 1. GPU対応インスタンスの起動(g4dn.xlarge等)

# 2. CUDA Toolkitのインストール
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.0-1_all.deb
sudo dpkg -i cuda-keyring_1.0-1_all.deb
sudo apt-get update
sudo apt-get -y install cuda

# 3. アプリケーションのセットアップ(上記と同様)

# 4. セキュリティグループの設定
# ポート8000を開放

オプション3: Docker

# Dockerfile
FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04

# Pythonのインストール
RUN apt-get update && apt-get install -y python3.10 python3-pip

# 作業ディレクトリの設定
WORKDIR /app

# 依存関係のコピーとインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# アプリケーションコードのコピー
COPY . .

# ポートの公開
EXPOSE 8000

# アプリケーションの起動
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# イメージのビルド
docker build -t rapidgen-api .

# コンテナの起動
docker run --gpus all -p 8000:8000 rapidgen-api

トラブルシューティング

よくある問題と解決策

1. CUDA Out of Memory エラー

症状: torch.cuda.OutOfMemoryError

解決策:

# services/image_generation.py に追加
self.pipe.enable_attention_slicing(slice_size=1)
self.pipe.enable_vae_slicing()

# より小さい画像サイズを使用
init_image = init_image.resize((512, 512))

2. 生成が遅い

症状: 1枚の生成に1分以上かかる

解決策:

  • LCMのステップ数を減らす(推奨: 4-8ステップ)
  • guidance_scaleを低く設定(LCMでは1.0-2.0が推奨)
  • より高性能なGPUを使用

3. フロントエンドとバックエンドの接続エラー

症状: Failed to fetch エラー

解決策:

// .env.localを確認
API_URL=http://localhost:8000  // 正しいURLか確認

// CORSの設定を確認(app/main.py)
allow_origins=["http://localhost:3000", "https://your-frontend.vercel.app"]

4. モデルのダウンロードに失敗

症状: 初回起動時にHugging Faceからのダウンロードが失敗

解決策:

# Hugging Face認証トークンの設定
export HF_TOKEN=your_token_here

# または .env ファイルに追加
HF_TOKEN=your_token_here

カスタマイズガイド

新しいスタイルプリセットの追加

// app/editor/page.tsx
const stylePresets = [
  // 既存のプリセット...
  {
    id: 'watercolor',
    label: '水彩画',
    description: '水彩絵の具のようなスタイル',
    prompt: 'watercolor painting, soft colors, paper texture, artistic',
    thumbnail: '/presets/watercolor.jpg',
  },
];

生成パラメータのカスタマイズ

# config/settings.py
class Settings(BaseSettings):
    # デフォルト値の変更
    default_steps: int = 6  # ステップ数を増やして品質向上
    default_guidance: float = 1.5  # ガイダンススケール
    max_image_size: int = 1024  # 最大画像サイズ
    
    class Config:
        env_file = ".env"

UIテーマのカスタマイズ

/* app/globals.css */
:root {
  /* カラースキームの変更 */
  --primary: 220 70% 50%;  /* 青系に変更 */
  --accent: 340 80% 60%;   /* ピンク系のアクセント */
}

.glass-effect {
  /* ガラスエフェクトの調整 */
  @apply bg-white/10 backdrop-blur-2xl;
}

コミュニティへの貢献

RapidGenはOSSプロジェクトとして、コミュニティからの貢献を歓迎しています。

貢献の方法

  1. バグ報告

    • GitHubのIssuesで報告
    • 再現手順を詳細に記載
  2. 機能追加

    • Forkを作成
    • 機能ブランチで開発
    • Pull Requestを作成
  3. ドキュメント改善

    • READMEの翻訳
    • チュートリアルの追加
    • コード例の追加

貢献ガイドライン

# CONTRIBUTING.md

## Pull Requestの手順

1. リポジトリをFork
2. 機能ブランチを作成
   ```bash
   git checkout -b feature/amazing-feature
  1. 変更をコミット
    git commit -m 'Add: 素晴らしい機能を追加'
    
  2. ブランチにプッシュ
    git push origin feature/amazing-feature
    
  3. Pull Requestを作成

コーディング規約

Python (Backend)

  • Black でフォーマット
  • Flake8 でリント
  • 型ヒントを必ず記載
  • Docstringを記載

TypeScript (Frontend)

  • Prettier でフォーマット
  • ESLint でリント
  • 厳格な型定義
  • コンポーネントにコメント

今後の展望

RapidGenは、以下の機能拡張を予定しています。コミュニティの協力により、さらに進化させていきたいと考えています。

短期的な目標

  1. バッチ処理機能

    • 複数画像の一括生成
    • キュー管理の改善
  2. より多くのスタイルプリセット

    • ファインアートスタイル
    • 写真加工スタイル
    • アーティストスタイル
  3. 画像編集機能

    • クロップ、回転
    • フィルター適用
    • 明るさ・コントラスト調整

中期的な目標

  1. ControlNetの統合

    • エッジ検出による構図制御
    • ポーズ制御
  2. Inpaintingサポート

    • 部分的な画像編集
    • マスク機能
  3. ユーザー認証システム

    • アカウント管理
    • 生成履歴の保存

長期的な目標

  1. プラグインシステム

    • カスタムモデルの追加
    • サードパーティ拡張機能
  2. モバイルアプリ

    • React Native版の開発
  3. API商用化

    • クレジット制の導入
    • エンタープライズプラン

まとめ

本記事では、AIツールを最大限活用してフルスタック画像生成アプリ「RapidGen」を開発し、OSS化するまでの全プロセスを解説しました。

重要なポイント

  1. AIで開発効率が劇的に向上

    • ChatGPTでコード生成
    • Cursor AIでリファクタリング
    • バックエンド(Python/FastAPI)もAIで作成
  2. OSS化の理由

    • デプロイの課題(容量制限、GPU要件)
    • 知見の共有とコミュニティ形成
    • スターターテンプレートとしての価値
  3. 技術スタック

    • フロントエンド: Next.js 16 + React 19 + TypeScript
    • バックエンド: FastAPI + Stable Diffusion XL + LCM
    • 非同期タスク処理とGPU最適化
  4. 実装の核心

    • 詳細なコード例を提供
    • エラーハンドリングとテスト
    • パフォーマンス最適化テクニック

次のステップ

このプロジェクトを試してみたい方へ:

  1. GitHubからリポジトリをクローン

  2. セットアップガイドに従って起動

  3. 自分の環境でカスタマイズ

  4. Issues や Pull Request で貢献

フィードバックや質問がある方は、ぜひGitHubのIssuesでお知らせください。 一緒により良いプロジェクトに育てていきましょう。

RapidGenが、皆さんのフルスタック開発の学習やプロジェクトのスターターテンプレートとして役立つことを願っています。

リンク集

Discussion