こんにちは😊
株式会社プロドウガの@YushiYamamotoです!
JapanLifeStart.comの開発・運営を担当しながら、React.js・Next.js専門のフリーランスエンジニアとしても活動しています❗️
今回は、Next.jsとFastAPIを使って、ブラウザからAI音楽生成ができるフルスタックWebアプリを構築する方法を完全解説します。バックエンドには2026年2月にリリースされたオープンソースの音楽生成AI「ACE-Step 1.5」を採用し、TypeScriptとPythonの型安全性を活かした実装を紹介します。
ローカル開発環境の構築から、Dockerコンテナ化、そして本番環境(Vercel + GCP Cloud Run)へのデプロイ戦略まで、実務レベルの知見を詰め込みました。
🎯 この記事で構築するもの
- フロントエンド: Next.js 14 (App Router) + TypeScript + Tailwind CSS
- バックエンド: FastAPI + ACE-Step 1.5 + Uvicorn
- データストア: PostgreSQL(生成履歴管理)
- インフラ: Docker Compose(ローカル)/ Vercel + Cloud Run(本番)
アーキテクチャ概要
🛠️ 技術スタックの選定理由
| レイヤー | 技術 | 選定理由 |
|---|---|---|
| フロントエンド | Next.js 14 | SSR/SSG対応、App Routerの柔軟性、Vercelデプロイ容易性 |
| バックエンド | FastAPI | Pythonエコシステムとの親和性、Pydanticによる型安全性、高速なASGI |
| AIモデル | ACE-Step 1.5 | MIT License(商用可)、ローカル実行可能、低レイテンシー |
| データベース | PostgreSQL | JSONB対応(メタデータ保存)、ACID特性、拡張性 |
| コンテナ | Docker | 環境の再現性、GPUサポート(NVIDIA Container Toolkit) |
🏗️ プロジェクト構成
ai-music-generator/
├── docker-compose.yml # ローカル開発環境
├── frontend/ # Next.jsアプリケーション
│ ├── app/
│ ├── components/
│ ├── lib/api.ts # APIクライアント
│ └── types/index.ts # TypeScript型定義
└── backend/ # FastAPIアプリケーション
├── app/
│ ├── main.py # アプリケーションエントリ
│ ├── routers/
│ │ └── music.py # 音楽生成API
│ ├── models/
│ │ └── database.py # SQLAlchemyモデル
│ └── services/
│ └── ace_step.py # ACE-Stepラッパー
├── Dockerfile
└── requirements.txt
🔧 バックエンド実装(FastAPI + ACE-Step)
1. 依存関係の定義
# backend/requirements.txt
fastapi==0.115.0
uvicorn[standard]==0.32.0
sqlalchemy==2.0.36
psycopg2-binary==2.9.9
pydantic==2.9.0
pydantic-settings==2.6.0
python-multipart==0.0.17
ace-step>=1.5.0
torch>=2.1.0
numpy>=1.24.0
2. 設定管理(Pydantic Settings)
backend/app/core/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
# アプリケーション設定
APP_NAME: str = "ACE-Step Music API"
DEBUG: bool = False
API_V1_STR: str = "/api/v1"
# データベース
DATABASE_URL: str = "postgresql://user:password@localhost:5432/musicdb"
# ACE-Step設定
ACESTEP_DIT_MODEL: str = "acestep-v15-turbo"
ACESTEP_LM_MODEL: str = "acestep-5Hz-lm-1.7B"
ACESTEP_DEVICE: str = "cuda" # または "cpu"
# ファイルストレージ
STORAGE_PATH: str = "./storage"
MAX_FILE_SIZE: int = 50 * 1024 * 1024 # 50MB
# CORS
CORS_ORIGINS: list[str] = ["http://localhost:3000", "https://your-domain.vercel.app"]
class Config:
env_file = ".env"
case_sensitive = True
@lru_cache()
def get_settings() -> Settings:
return Settings()
settings = get_settings()
3. データベースモデル(SQLAlchemy)
backend/app/models/database.py
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, JSON, Float, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
Base = declarative_base()
engine = create_engine(settings.DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class GenerationRecord(Base):
__tablename__ = "generation_records"
id = Column(Integer, primary_key=True, index=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 生成パラメータ
prompt = Column(String(1000), nullable=False)
lyrics = Column(String(5000))
duration = Column(Integer, default=30)
bpm = Column(Integer)
style_tags = Column(JSON) # ["pop", "electronic", "upbeat"]
# 結果
status = Column(String(50), default="pending") # pending, completed, failed
file_path = Column(String(500))
file_size = Column(Integer)
duration_actual = Column(Float)
# メタデータ
model_version = Column(String(50), default="acestep-v15-turbo")
generation_time_ms = Column(Integer)
error_message = Column(String(1000))
# ユーザー管理(簡易版)
session_id = Column(String(100), index=True)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def init_db():
Base.metadata.create_all(bind=engine)
4. ACE-Stepサービスラッパー
backend/app/services/ace_step.py
import os
import time
import torch
from typing import Optional, BinaryIO
from pathlib import Path
from acestep import ACEStepPipeline
from app.core.config import settings
from app.models.database import GenerationRecord
class ACEStepService:
_instance: Optional["ACEStepService"] = None
_pipe: Optional[ACEStepPipeline] = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if self._pipe is not None:
return
print("🔄 Initializing ACE-Step pipeline...")
start_time = time.time()
self._pipe = ACEStepPipeline.from_pretrained(
"ACE-Step/Ace-Step1.5",
dit_model=settings.ACESTEP_DIT_MODEL,
lm_model=settings.ACESTEP_LM_MODEL,
torch_dtype=torch.float16 if settings.ACESTEP_DEVICE == "cuda" else torch.float32,
device_map="auto"
)
init_time = time.time() - start_time
print(f"✅ Pipeline initialized in {init_time:.2f}s")
@property
def pipe(self) -> ACEStepPipeline:
if self._pipe is None:
raise RuntimeError("Pipeline not initialized")
return self._pipe
async def generate(
self,
prompt: str,
lyrics: Optional[str] = None,
duration: int = 30,
bpm: Optional[int] = None,
output_path: str = "./output.wav"
) -> dict:
"""
音楽を生成してファイルに保存
"""
start_time = time.time()
try:
# 推論実行
result = self.pipe(
prompt=prompt,
lyrics=lyrics,
duration=duration,
bpm=bpm
)
# 保存
result.save(output_path)
generation_time = int((time.time() - start_time) * 1000)
file_size = os.path.getsize(output_path)
return {
"success": True,
"file_path": output_path,
"file_size": file_size,
"generation_time_ms": generation_time
}
except Exception as e:
return {
"success": False,
"error": str(e),
"generation_time_ms": int((time.time() - start_time) * 1000)
}
# シングルトンインスタンス
ace_step_service = ACEStepService()
5. APIルーター(エンドポイント定義)
backend/app/routers/music.py
import uuid
import os
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import Optional, List
from app.models.database import get_db, GenerationRecord
from app.services.ace_step import ace_step_service
from app.core.config import settings
router = APIRouter(prefix="/music", tags=["music"])
class GenerationRequest(BaseModel):
prompt: str = Field(..., min_length=10, max_length=1000, description="音楽の説明(スタイル、楽器、雰囲気)")
lyrics: Optional[str] = Field(None, max_length=5000, description="歌詞と構造タグ")
duration: int = Field(default=30, ge=5, le=600, description="生成時間(秒)")
bpm: Optional[int] = Field(None, ge=30, le=300, description="テンポ")
style_tags: Optional[List[str]] = Field(default=[], description="スタイルタグ")
session_id: Optional[str] = None
class GenerationResponse(BaseModel):
id: int
status: str
prompt: str
duration: int
created_at: str
class Config:
from_attributes = True
@router.post("/generate", response_model=GenerationResponse)
async def create_generation(
request: GenerationRequest,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db)
):
"""
音楽生成リクエストを受け付け、非同期で生成処理を実行
"""
# セッションID生成
session_id = request.session_id or str(uuid.uuid4())
# データベースにレコード作成
db_record = GenerationRecord(
prompt=request.prompt,
lyrics=request.lyrics,
duration=request.duration,
bpm=request.bpm,
style_tags=request.style_tags,
session_id=session_id,
status="pending"
)
db.add(db_record)
db.commit()
db.refresh(db_record)
# バックグラウンドタスクで生成実行
background_tasks.add_task(
process_generation,
db_record.id,
request,
db
)
return GenerationResponse(
id=db_record.id,
status="pending",
prompt=request.prompt,
duration=request.duration,
created_at=db_record.created_at.isoformat()
)
async def process_generation(
record_id: int,
request: GenerationRequest,
db: Session
):
"""
実際の生成処理を実行
"""
db_record = db.query(GenerationRecord).filter(GenerationRecord.id == record_id).first()
if not db_record:
return
# 出力ディレクトリ作成
output_dir = Path(settings.STORAGE_PATH) / "generations"
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / f"{record_id}_{uuid.uuid4().hex}.wav"
try:
# ACE-Stepで生成
result = await ace_step_service.generate(
prompt=request.prompt,
lyrics=request.lyrics,
duration=request.duration,
bpm=request.bpm,
output_path=str(output_path)
)
if result["success"]:
db_record.status = "completed"
db_record.file_path = str(output_path)
db_record.file_size = result["file_size"]
db_record.generation_time_ms = result["generation_time_ms"]
else:
db_record.status = "failed"
db_record.error_message = result.get("error", "Unknown error")
except Exception as e:
db_record.status = "failed"
db_record.error_message = str(e)
db.commit()
@router.get("/status/{generation_id}")
async def get_generation_status(
generation_id: int,
db: Session = Depends(get_db)
):
"""
生成ジョブのステータスを取得
"""
record = db.query(GenerationRecord).filter(GenerationRecord.id == generation_id).first()
if not record:
raise HTTPException(status_code=404, detail="Generation not found")
return {
"id": record.id,
"status": record.status,
"prompt": record.prompt,
"duration": record.duration,
"generation_time_ms": record.generation_time_ms,
"created_at": record.created_at.isoformat(),
"download_url": f"/api/v1/music/download/{record.id}" if record.status == "completed" else None
}
@router.get("/download/{generation_id}")
async def download_music(
generation_id: int,
db: Session = Depends(get_db)
):
"""
生成された音楽ファイルをダウンロード
"""
record = db.query(GenerationRecord).filter(GenerationRecord.id == generation_id).first()
if not record or record.status != "completed":
raise HTTPException(status_code=404, detail="File not found")
if not os.path.exists(record.file_path):
raise HTTPException(status_code=404, detail="File not found on disk")
return FileResponse(
record.file_path,
media_type="audio/wav",
filename=f"generated_{generation_id}.wav"
)
@```markdown
router.get("/history/{session_id}")
async def get_generation_history(
session_id: str,
limit: int = 10,
db: Session = Depends(get_db)
):
"""
セッションIDに紐づく生成履歴を取得
"""
records = db.query(GenerationRecord)\
.filter(GenerationRecord.session_id == session_id)\
.order_by(GenerationRecord.created_at.desc())\
.limit(limit)\
.all()
return [
{
"id": r.id,
"status": r.status,
"prompt": r.prompt,
"duration": r.duration,
"created_at": r.created_at.isoformat()
}
for r in records
]
6. アプリケーションエントリポイント
# backend/app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
from app.models.database import init_db
from app.routers import music
app = FastAPI(
title=settings.APP_NAME,
openapi_url=f"{settings.API_V1_STR}/openapi.json"
)
# CORS設定
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# データベース初期化
@app.on_event("startup")
async def startup_event():
init_db()
# ルーター登録
app.include_router(music.router, prefix=settings.API_V1_STR)
@app.get("/health")
async def health_check():
return {"status": "healthy", "service": "ace-step-api"}
7. Dockerfile(GPU対応)
backend/Dockerfile
# NVIDIA PyTorchベースイメージ
FROM nvidia/cuda:12.1-runtime-ubuntu22.04
# Python環境
ENV PYTHONUNBUFFERED=1
ENV DEBIAN_FRONTEND=noninteractive
# システム依存関係
RUN apt-get update && apt-get install -y \
python3.11 \
python3-pip \
python3.11-venv \
libsndfile1 \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*
# 作業ディレクトリ
WORKDIR /app
# 依存関係のインストール
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
# モデルダウンロード(ビルド時に実行)
RUN python3 -c "from acestep import ACEStepPipeline; ACEStepPipeline.from_pretrained('ACE-Step/Ace-Step1.5', dit_model='acestep-v15-turbo', lm_model='acestep-5Hz-lm-1.7B')"
# アプリケーションコード
COPY app/ ./app/
# ストレージディレクトリ
RUN mkdir -p /app/storage/generations
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
🎨 フロントエンド実装(Next.js 14)
1. 型定義(TypeScript)
frontend/types/index.ts
// 生成リクエスト
export interface GenerationRequest {
prompt: string;
lyrics?: string;
duration: number;
bpm?: number;
style_tags?: string[];
session_id?: string;
}
// 生成レスポンス
export interface GenerationResponse {
id: number;
status: 'pending' | 'completed' | 'failed';
prompt: string;
duration: number;
created_at: string;
}
// ステータスレスポンス
export interface GenerationStatus {
id: number;
status: string;
prompt: string;
duration: number;
generation_time_ms?: number;
created_at: string;
download_url?: string;
}
// 履歴アイテム
export interface GenerationHistory {
id: number;
status: string;
prompt: string;
duration: number;
created_at: string;
}
2. APIクライアント
frontend/lib/api.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1';
class MusicAPI {
async generateMusic(data: GenerationRequest): Promise<GenerationResponse> {
const response = await fetch(`${API_BASE_URL}/music/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async getStatus(generationId: number): Promise<GenerationStatus> {
const response = await fetch(`${API_BASE_URL}/music/status/${generationId}`);
if (!response.ok) throw new Error('Failed to fetch status');
return response.json();
}
async getHistory(sessionId: string): Promise<GenerationHistory[]> {
const response = await fetch(`${API_BASE_URL}/music/history/${sessionId}`);
if (!response.ok) throw new Error('Failed to fetch history');
return response.json();
}
getDownloadUrl(generationId: number): string {
return `${API_BASE_URL}/music/download/${generationId}`;
}
}
export const musicAPI = new MusicAPI();
3. メインコンポーネント
frontend/app/page.tsx
'use client';
import { useState, useEffect, useCallback } from 'react';
import { MusicGenerator } from '@/components/MusicGenerator';
import { GenerationHistory } from '@/components/GenerationHistory';
import { AudioPlayer } from '@/components/AudioPlayer';
import { musicAPI } from '@/lib/api';
import { GenerationResponse, GenerationStatus } from '@/types';
export default function Home() {
const [sessionId, setSessionId] = useState<string>('');
const [currentGeneration, setCurrentGeneration] = useState<GenerationResponse | null>(null);
const [generationStatus, setGenerationStatus] = useState<GenerationStatus | null>(null);
const [audioUrl, setAudioUrl] = useState<string | null>(null);
const [isPolling, setIsPolling] = useState(false);
// セッションIDの初期化
useEffect(() => {
const stored = localStorage.getItem('music_session_id');
if (stored) {
setSessionId(stored);
} else {
const newSession = crypto.randomUUID();
localStorage.setItem('music_session_id', newSession);
setSessionId(newSession);
}
}, []);
// ポーリング処理
const pollStatus = useCallback(async (generationId: number) => {
setIsPolling(true);
const checkStatus = async () => {
try {
const status = await musicAPI.getStatus(generationId);
setGenerationStatus(status);
if (status.status === 'completed') {
setAudioUrl(musicAPI.getDownloadUrl(generationId));
setIsPolling(false);
return;
} else if (status.status === 'failed') {
setIsPolling(false);
return;
}
// 2秒後に再確認
setTimeout(checkStatus, 2000);
} catch (error) {
console.error('Polling error:', error);
setIsPolling(false);
}
};
checkStatus();
}, []);
const handleGenerate = async (data: {
prompt: string;
lyrics?: string;
duration: number;
bpm?: number;
}) => {
try {
setAudioUrl(null);
setGenerationStatus(null);
const response = await musicAPI.generateMusic({
...data,
session_id: sessionId,
style_tags: [],
});
setCurrentGeneration(response);
pollStatus(response.id);
} catch (error) {
console.error('Generation error:', error);
alert('生成リクエストに失敗しました');
}
};
return (
<main className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-black text-white">
<div className="container mx-auto px-4 py-8 max-w-6xl">
<header className="text-center mb-12">
<h1 className="text-5xl font-bold mb-4 bg-gradient-to-r from-pink-500 to-cyan-500 bg-clip-text text-transparent">
🎵 AI Music Generator
</h1>
<p className="text-gray-300 text-lg">
ACE-Step 1.5 powered - Generate music in seconds
</p>
</header>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* 生成フォーム */}
<div className="space-y-6">
<MusicGenerator
onGenerate={handleGenerate}
isLoading={isPolling}
/>
{/* ステータス表示 */}
{isPolling && generationStatus && (
<div className="bg-white/10 backdrop-blur-lg rounded-xl p-6 border border-white/20">
<div className="flex items-center space-x-3">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-cyan-400"></div>
<span>生成中... {generationStatus.status}</span>
</div>
{generationStatus.generation_time_ms && (
<p className="text-sm text-gray-400 mt-2">
生成時間: {(generationStatus.generation_time_ms / 1000).toFixed(1)}秒
</p>
)}
</div>
)}
{/* オーディオプレーヤー */}
{audioUrl && (
<AudioPlayer
url={audioUrl}
onDownload={() => window.open(audioUrl, '_blank')}
/>
)}
</div>
{/* 履歴 */}
<div>
<GenerationHistory
sessionId={sessionId}
currentGenerationId={currentGeneration?.id}
/>
</div>
</div>
</div>
</main>
);
}
4. 音楽生成フォームコンポーネント
frontend/components/MusicGenerator.tsx
'use client';
import { useState } from 'react';
interface Props {
onGenerate: (data: {
prompt: string;
lyrics?: string;
duration: number;
bpm?: number;
}) => void;
isLoading: boolean;
}
const STYLE_PRESETS = [
{ label: 'J-Pop', value: 'upbeat J-pop with electronic elements, female vocal' },
{ label: 'Lo-Fi', value: 'lo-fi hip hop, chill beats, relaxing, study music' },
{ label: 'Rock', value: 'energetic rock, electric guitar, powerful drums' },
{ label: 'Ambient', value: 'ambient, atmospheric, ethereal, meditation' },
];
const DURATION_OPTIONS = [15, 30, 60, 120, 180, 300];
export function MusicGenerator({ onGenerate, isLoading }: Props) {
const [prompt, setPrompt] = useState('');
const [lyrics, setLyrics] = useState('');
const [duration, setDuration] = useState(30);
const [bpm, setBpm] = useState<number | undefined>(undefined);
const [showAdvanced, setShowAdvanced] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!prompt.trim() || isLoading) return;
onGenerate({
prompt,
lyrics: lyrics.trim() || undefined,
duration,
bpm,
});
};
const applyPreset = (preset: string) => {
setPrompt(preset);
};
return (
<form onSubmit={handleSubmit} className="bg-white/10 backdrop-blur-lg rounded-xl p-6 border border-white/20">
<h2 className="text-2xl font-semibold mb-6 flex items-center">
<span className="mr-2">🎹</span> Create Music
</h2>
{/* プリセットボタン */}
<div className="mb-4">
<label className="text-sm text-gray-400 mb-2 block">Quick Presets</label>
<div className="flex flex-wrap gap-2">
{STYLE_PRESETS.map((preset) => (
<button
key={preset.label}
type="button"
onClick={() => applyPreset(preset.value)}
className="px-3 py-1 text-sm bg-white/20 hover:bg-white/30 rounded-full transition"
>
{preset.label}
</button>
))}
</div>
</div>
{/* プロンプト入力 */}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">
Style Description *
</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="e.g., upbeat J-pop with electronic elements, female vocal, energetic"
className="w-full px-4 py-3 bg-black/30 border border-white/20 rounded-lg focus:outline-none focus:border-cyan-400 transition resize-none"
rows={3}
required
/>
</div>
{/* 歌詞入力(オプション) */}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">
Lyrics (Optional)
<span className="text-gray-500 text-xs ml-2">- Supports [Verse], [Chorus] tags</span>
</label>
<textarea
value={lyrics}
onChange={(e) => setLyrics(e.target.value)}
placeholder="[Verse] Your lyrics here... [Chorus] Sing along!"
className="w-full px-4 py-3 bg-black/30 border border-white/20 rounded-lg focus:outline-none focus:border-cyan-400 transition resize-none font-mono text-sm"
rows={5}
/>
</div>
{/* 高度な設定 */}
<div className="mb-6">
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
className="text-sm text-cyan-400 hover:underline"
>
{showAdvanced ? 'Hide Advanced' : 'Show Advanced'}
</button>
{showAdvanced && (
<div className="mt-4 grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">Duration (seconds)</label>
<select
value={duration}
onChange={(e) => setDuration(Number(e.target.value))}
className="w-full px-3 py-2 bg-black/30 border border-white/20 rounded-lg"
>
{DURATION_OPTIONS.map((d) => (
<option key={d} value={d}>{d}s</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">BPM (optional)</label>
<input
type="number"
value={bpm || ''}
onChange={(e) => setBpm(e.target.value ? Number(e.target.value) : undefined)}
placeholder="120"
min={30}
max={300}
className="w-full px-3 py-2 bg-black/30 border border-white/20 rounded-lg"
/>
</div>
</div>
)}
</div>
{/* 生成ボタン */}
<button
type="submit"
disabled={isLoading || !prompt.trim()}
className="w-full py-4 bg-gradient-to-r from-pink-500 to-cyan-500 rounded-lg font-semibold text-lg hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition flex items-center justify-center"
>
{isLoading ? (
<>
<span className="animate-spin mr-2">⏳</span>
Generating...
</>
) : (
<>
<span className="mr-2">🎵</span>
Generate Music
</>
)}
</button>
</form>
);
}
🐳 Docker Compose設定
# docker-compose.yml
version: '3.8'
services:
# バックエンド(GPU対応)
backend:
build:
context: ./backend
dockerfile: Dockerfile
runtime: nvidia # NVIDIA Container Toolkit必要
environment:
- NVIDIA_VISIBLE_DEVICES=all
- CUDA_VISIBLE_DEVICES=0
- DATABASE_URL=postgresql://postgres:password@db:5432/musicdb
- STORAGE_PATH=/app/storage
volumes:
- ./storage:/app/storage
- acestep_cache:/root/.cache/ace_step
ports:
- "8000:8000"
depends_on:
- db
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
# データベース
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_DB=musicdb
volumes:
- postgres_data```yaml
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
# フロントエンド(開発用)
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
environment:
- NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
ports:
- "3000:3000"
volumes:
- ./frontend:/app
- /app/node_modules
command: npm run dev
depends_on:
- backend
volumes:
postgres_data:
acestep_cache:
NVIDIA Container Toolkitのインストールが必要です: DockerでGPUを使用するには、事前にNVIDIA Container Toolkitをインストールしておく必要があります。
# Ubuntuの場合
sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker
🚀 本番デプロイ戦略
アーキテクチャ構成(本番環境)
1. バックエンド:Cloud Run(GPU)へのデプロイ
Cloud Run GPUの制約: 執筆時点(2026年2月)で、Cloud RunのGPU対応は限定されたリージョンで利用可能です。L4 GPU(NVIDIA)が推奨されています。
cloudrun.yaml(Cloud Run設定)
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: ace-step-api
annotations:
run.googleapis.com/ingress: all
run.googleapis.com/execution-environment: gen2
spec:
template:
metadata:
annotations:
run.googleapis.com/cpu-throttling: "false"
run.googleapis.com/gpu: "nvidia-l4"
run.googleapis.com/max-scale: "5"
run.googleapis.com/min-scale: "1"
spec:
containerConcurrency: 1
containers:
- image: gcr.io/PROJECT_ID/ace-step-api:latest
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-url
key: latest
- name: ACESTEP_DEVICE
value: "cuda"
- name: STORAGE_PATH
value: "/tmp/storage"
resources:
limits:
cpu: "8"
memory: "32Gi"
nvidia.com/gpu: "1"
requests:
cpu: "4"
memory: "16Gi"
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
CI/CDパイプライン(GitHub Actions):
# .github/workflows/deploy-backend.yml
name: Deploy Backend to Cloud Run
on:
push:
branches: [main]
paths: ['backend/**']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Configure Docker
run: gcloud auth configure-docker
- name: Build and Push
run: |
docker build -t gcr.io/${{ secrets.GCP_PROJECT_ID }}/ace-step-api:${{ github.sha }} ./backend
docker push gcr.io/${{ secrets.GCP_PROJECT_ID }}/ace-step-api:${{ github.sha }}
- name: Deploy to Cloud Run
run: |
gcloud run services replace cloudrun.yaml \
--region=asia-northeast1 \
--image=gcr.io/${{ secrets.GCP_PROJECT_ID }}/ace-step-api:${{ github.sha }}
2. フロントエンド:Vercelへのデプロイ
# Vercel CLIでのデプロイ
cd frontend
vercel --prod
# 環境変数の設定
vercel env add NEXT_PUBLIC_API_URL
# 値: https://ace-step-api-xxx.run.app/api/v1
Vercel設定(vercel.json):
{
"buildCommand": "next build",
"outputDirectory": ".next",
"framework": "nextjs",
"regions": ["hnd1"],
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Access-Control-Allow-Origin",
"value": "*"
}
]
}
]
}
⚡ パフォーマンス最適化
1. モデルロードの最適化
ACE-Stepの初期ロードは時間がかかるため、以下の戦略を推奨します:
| 戦略 | 効果 | 実装 |
|---|---|---|
| Warmupリクエスト | コールドスタートを防ぐ | Cloud Runのmin-scale=1設定 |
| モデルキャッシュ | 2回目以降の高速化 |
/root/.cacheを永続ボリューム化 |
| Lazy Loading | 必要時のみロード | 初回リクエスト時に初期化 |
2. レスポンス時間の改善
非同期処理とキューイング(Celery + Redis)
# backend/app/celery_app.py
from celery import Celery
celery_app = Celery(
"ace_step",
broker="redis://redis:6379/0",
backend="redis://redis:6379/0"
)
@celery_app.task(bind=True)
def generate_music_task(self, prompt: str, lyrics: str, duration: int):
"""Celeryタスクとして非同期実行"""
self.update_state(state="PROGRESS", meta={"progress": 10})
# 生成処理
result = ace_step_service.generate(...)
self.update_state(state="SUCCESS", meta={"file_path": result["file_path"]})
return result
3. ストリーミングレスポンス
大きな音声ファイルを効率的に配信するため、ストリーミング対応:
from fastapi.responses import StreamingResponse
@app.get("/music/stream/{generation_id}")
async def stream_music(generation_id: int):
record = get_record(generation_id)
def iterfile():
with open(record.file_path, "rb") as f:
while chunk := f.read(8192):
yield chunk
return StreamingResponse(
iterfile(),
media_type="audio/wav",
headers={"Content-Disposition": f"attachment; filename=generated_{generation_id}.wav"}
)
🔍 トラブルシューティング
よくある問題と解決策
| 症状 | 原因 | 解決策 |
|---|---|---|
| CUDA Out of Memory | GPUメモリ不足 |
torch_dtype=torch.float16を使用、batch_sizeを1に制限 |
| コールドスタート遅延 | モデル初期化時間 |
min-scale=1で常時起動、または warmup エンドポイントを設置 |
| CORSエラー | ドメイン不一致 |
CORS_ORIGINSに正しいフロントエンドURLを設定 |
| 音質が不安定 | ランダム性(seed) | 固定seedを使用、または複数バッチ生成して選択 |
ログ確認コマンド
# Cloud Runログ確認
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=ace-step-api" --limit=50
# ローカルDockerログ
docker-compose logs -f backend
# パフォーマンスプロファイリング
nvidia-smi dmon -s mu # GPU使用状況の監視
📊 コスト試算(月間)
| 項目 | スペック | 月間コスト(概算) |
|---|---|---|
| Cloud Run GPU | L4 GPU × 常時1インスタンス | ¥150,000〜 |
| Cloud SQL | db-f1-micro(開発用) | ¥5,000〜 |
| Cloud Storage | 1TB(音声ファイル) | ¥2,500〜 |
| Vercel Pro | チームプラン | ¥2,000〜 |
| 合計 | ¥160,000〜 |
コスト削減のポイント: 開発環境ではGPUインスタンスをスケールダウン(0に)し、デモ時のみ起動する運用がおすすめです。Cloud Runは秒単位の課金なので、アイドル時のコストが発生しません。
📝 まとめ
本記事では、Next.js + FastAPI + ACE-Step 1.5を使った、フルスタックのAI音楽生成Webアプリケーションの構築方法を解説しました。
主要なポイント:
- ✅ 型安全: TypeScript + Pydanticでエンドツーエンドの型安全性を確保
- ✅ スケーラブル: Cloud Run + Cloud SQLでオートスケーリング対応
- ✅ リアルタイム: ポーリングによる非同期生成と進捗表示
- ✅ 商用可能: MIT LicenseのACE-Stepを使用し、所有権を保持
このアーキテクチャをベースに、カスタムLoRAモデルの統合やリアルタイムコラボレーション機能など、さらなる拡張も可能です。
ソースコード: GitHub - ace-step-nextjs-demo(サンプル実装)
最後に:業務委託のご相談を承ります
私は業務委託エンジニアとしてWEB制作やシステム開発を請け負っています。最新技術を活用したレスポンシブなWebサイト制作、インタラクティブなアプリケーション開発、API連携など幅広いご要望に対応可能です。
「課題解決に向けた即戦力が欲しい」「高品質なWeb制作を依頼したい」という方は、お気軽にご相談ください。一緒にビジネスの成長を目指しましょう!
👦 在留外国人のためのメディア(JapanLifeStart.com)
👉 ポートフォリオ
────────────────────────────
🚀 エンジニアの方へ:10万円もらえる特別キャンペーン
現在、フリーランスエージェントの 「クラウドワークステック」 が、紹介経由での登録&稼働で「10万円(税込)」がもらえる 異例のキャンペーンを実施中です。
「自分の市場価値(単価)を知りたい」「週3日からの副業・リモート案件を探している」という方は、ぜひこの機会に登録してみてください。
※キャンペーンは予告なく終了する場合があります。