diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e8f65a2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +__pycache__ +.pytest_cache +*.pyc +*.pyo +*.pyd +.env +.venv diff --git a/DOC/Chapter01.md b/DOC/Chapter01.md new file mode 100644 index 0000000..f12159a --- /dev/null +++ b/DOC/Chapter01.md @@ -0,0 +1,22 @@ +# Chapter 01. 저장소 개요와 목표 + +이 저장소는 **Python 기반 생성형 이미지/영상 실험**을 중심으로 시작되었으며, 현재는 운영/테스트를 쉽게 하기 위해 FastAPI 백엔드 + 바닐라 JavaScript 프런트엔드 구조를 함께 갖추도록 확장되었습니다. + +## 핵심 목표 + +1. 기존 실험 스크립트의 기술 스택을 문서화 +2. 웹에서 확인 가능한 API/대시보드 제공 +3. Docker 기반의 재현 가능한 테스트 환경 제공 + +## 기존 구성 요약 + +- 단일 Python 스크립트 중심 실험 (`cuda*.py`, `cpu.py`, `mp4make.py`) +- Hugging Face `diffusers` + `torch` 기반 이미지 생성 +- `opencv`, `Pillow`, `ffmpeg` 기반 영상 후처리 + +## 확장 구성 요약 + +- `backend/app/main.py`: FastAPI 엔트리포인트 +- `frontend/`: 정적 웹 UI (Vanilla JS) +- `tests/`: API 테스트 +- `Dockerfile`, `docker-compose.yml`: 컨테이너 실행/테스트 환경 diff --git a/DOC/Chapter02.md b/DOC/Chapter02.md new file mode 100644 index 0000000..3e5ce66 --- /dev/null +++ b/DOC/Chapter02.md @@ -0,0 +1,20 @@ +# Chapter 02. Python 런타임과 핵심 라이브러리 + +## Python + +- Python 3.11을 기준으로 컨테이너 이미지를 구성했습니다. +- FastAPI/테스트 생태계와 호환성이 좋고, 현대적인 타입 힌트 및 성능 이점을 활용할 수 있습니다. + +## 핵심 라이브러리 역할 + +- `fastapi`: REST API 및 정적 파일 제공 +- `uvicorn`: ASGI 서버 실행 +- `pydantic`: 응답 모델 검증 +- `pytest`: 자동 테스트 +- `httpx`: 테스트/클라이언트 요청 유틸 + +## 기존 스크립트 생태계 라이브러리 + +- `torch`, `diffusers`: 생성형 모델 추론 +- `opencv-python`, `Pillow`, `numpy`: 영상/이미지 처리 +- `controlnet_aux`, `transformers`: 조건부 생성 파이프라인 보조 diff --git a/DOC/Chapter03.md b/DOC/Chapter03.md new file mode 100644 index 0000000..cb78bef --- /dev/null +++ b/DOC/Chapter03.md @@ -0,0 +1,22 @@ +# Chapter 03. 생성형 AI 파이프라인 스택 + +기존 저장소의 본질은 **생성형 모델 추론 파이프라인 실험**입니다. + +## 주요 구성 요소 + +- Stable Diffusion 계열 텍스트→이미지 생성 +- Img2Img 변형 생성 +- ControlNet(OpenPose) 기반 조건부 생성 +- 프레임 시퀀스 생성 후 동영상 합성 + +## 실행 특성 + +- GPU 환경(CUDA)에서 `float16` 사용 시 속도/메모리 효율 향상 +- CPU fallback 경로 제공 +- 프롬프트 품질, 시드, 스텝 수에 따라 결과가 크게 달라짐 + +## 운영 시 고려 사항 + +- VRAM/디스크 사용량 큼 +- 모델 다운로드/캐시 관리 필요 +- 장시간 작업(프레임 생성)에서 실패 복구 전략 필요 diff --git a/DOC/Chapter04.md b/DOC/Chapter04.md new file mode 100644 index 0000000..685879d --- /dev/null +++ b/DOC/Chapter04.md @@ -0,0 +1,21 @@ +# Chapter 04. 미디어 처리 및 FFmpeg 연동 + +생성 스크립트는 이미지 결과를 영상으로 결합하는 파이프라인을 포함합니다. + +## 처리 단계 + +1. 프레임 이미지 생성 +2. FPS/해상도 정규화 +3. ffmpeg로 영상 인코딩 +4. 필요 시 오디오 병합 + +## 장점 + +- 프레임 단위 제어가 쉬움 +- 후처리 유연성 높음 + +## 주의점 + +- 인코딩 옵션에 따라 속도/품질 차이 큼 +- ffmpeg 바이너리 설치가 필수 +- 파일 경로/네이밍 규칙이 중요 diff --git a/DOC/Chapter05.md b/DOC/Chapter05.md new file mode 100644 index 0000000..1119cea --- /dev/null +++ b/DOC/Chapter05.md @@ -0,0 +1,15 @@ +# Chapter 05. FastAPI 백엔드 아키텍처 + +새 백엔드는 기존 스크립트를 직접 대체하기보다, **스택 정보를 API로 노출하고 UI를 제공하는 관문 레이어**로 설계했습니다. + +## 엔드포인트 + +- `GET /api/health`: 상태 확인 +- `GET /api/stack`: 저장소 기술 스택 상세 정보 +- `GET /api/files`: 루트 Python 스크립트 목록 + +## 설계 포인트 + +- 타입 안정성을 위한 Pydantic 모델 사용 +- CORS 허용(로컬 개발 편의) +- 정적 파일(`frontend/`)을 루트(`/`)에 서빙 diff --git a/DOC/Chapter06.md b/DOC/Chapter06.md new file mode 100644 index 0000000..3402545 --- /dev/null +++ b/DOC/Chapter06.md @@ -0,0 +1,21 @@ +# Chapter 06. 바닐라 JavaScript 프런트엔드 + +프런트엔드는 별도 프레임워크 없이, 브라우저 기본 API만 사용합니다. + +## UI 구성 + +- 상태 패널(Health) +- 기술 스택 카드 목록 +- Python 파일 목록 + +## 기술 선택 이유 + +- 의존성 최소화 +- 빌드 단계 없이 즉시 실행 +- Docker 데모/검증에 적합 + +## 확장 아이디어 + +- 필터/검색 UI 추가 +- API 호출 에러 리트라이 +- 생성 작업 큐 상태 시각화 diff --git a/DOC/Chapter07.md b/DOC/Chapter07.md new file mode 100644 index 0000000..f3a4236 --- /dev/null +++ b/DOC/Chapter07.md @@ -0,0 +1,18 @@ +# Chapter 07. Docker 기반 실행 전략 + +## 기본 구성 + +- 단일 `Dockerfile`에서 앱 이미지 빌드 +- `docker-compose.yml`로 포트/재시작 정책 관리 + +## 실행 흐름 + +1. 이미지 빌드 +2. 컨테이너 실행 (`8000` 포트 노출) +3. 브라우저에서 UI 접근 +4. API 엔드포인트로 기능 검증 + +## 장점 + +- 로컬 환경 차이를 줄여 재현성 확보 +- 온보딩 비용 절감 diff --git a/DOC/Chapter08.md b/DOC/Chapter08.md new file mode 100644 index 0000000..826a0ec --- /dev/null +++ b/DOC/Chapter08.md @@ -0,0 +1,16 @@ +# Chapter 08. 테스트 전략과 품질 기준 + +## 자동 테스트 + +- `pytest` + FastAPI `TestClient`로 핵심 API 검증 + +## 테스트 항목 + +- `/api/health` 응답 코드/필드 +- `/api/stack` 데이터 개수/구조 +- `/api/files`에 기존 `.py` 스크립트가 노출되는지 확인 + +## 품질 기준 + +- 실패 시 원인 파악 가능한 메시지 +- 스키마 변경 시 테스트 동반 업데이트 diff --git a/DOC/Chapter09.md b/DOC/Chapter09.md new file mode 100644 index 0000000..ac72f91 --- /dev/null +++ b/DOC/Chapter09.md @@ -0,0 +1,18 @@ +# Chapter 09. 운영/보안/설정 관리 + +## 환경 변수 + +- HF 토큰 등 민감정보는 환경 변수로 주입 +- 코드 하드코딩 지양 + +## 운영 포인트 + +- 로그 레벨 분리(dev/prod) +- 리버스 프록시(Nginx) 연계 가능 +- 헬스체크 기반 배포 자동화 용이 + +## 보안 권장 사항 + +- CORS 허용 도메인 최소화 +- API 입력 검증 강화 +- 의존성 취약점 주기적 점검 diff --git a/DOC/Chapter10.md b/DOC/Chapter10.md new file mode 100644 index 0000000..96381c6 --- /dev/null +++ b/DOC/Chapter10.md @@ -0,0 +1,17 @@ +# Chapter 10. 로드맵 + +## 단기 + +- 기존 생성 스크립트 실행을 API 잡(Job)으로 래핑 +- 실행 상태/결과 파일 다운로드 API 제공 + +## 중기 + +- 작업 큐(Celery/RQ) 도입 +- WebSocket 기반 실시간 진행률 표시 + +## 장기 + +- 멀티 모델 라우팅 +- 사용자별 프로젝트/이력 관리 +- 클라우드 GPU 오토스케일링 연계 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..50aa674 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY backend/requirements.txt /app/backend/requirements.txt +RUN pip install --no-cache-dir -r /app/backend/requirements.txt + +COPY . /app + +EXPOSE 8000 + +CMD ["uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md index a22ef6d..ef92a40 100644 --- a/README.md +++ b/README.md @@ -3,53 +3,40 @@ 간단한 Python 기반 이미지/영상 생성 실험 저장소입니다. `diffusers`와 `torch`를 이용해 텍스트→이미지, 이미지→이미지, 포즈 기반 영상 프레임 생성, ffmpeg 후처리(영상 합성/오디오 병합)를 수행합니다. -## 기술 스택 - -- **언어**: Python 3 -- **AI/생성 모델**: - - `diffusers` (Stable Diffusion, Img2Img, ControlNet Pipeline) - - `torch` (CUDA/CPU 실행) - - Hugging Face 모델 허브 모델 사용 -- **컴퓨터 비전/이미지 처리**: - - `opencv-python` (`cv2`) - 프레임 기반 영상 생성 - - `Pillow` (`PIL`) - 이미지 입출력/리사이즈 - - `numpy` -- **포즈/조건부 생성**: - - `controlnet_aux` (`OpenposeDetector`) - - ControlNet (`lllyasviel/sd-controlnet-openpose`) -- **미디어 파이프라인**: - - `ffmpeg` (프레임↔영상 변환, FPS 변환, 오디오 병합) - - `subprocess` 기반 외부 명령 호출 -- **기타**: - - `transformers` (`CLIPImageProcessor` 일부 스크립트에서 사용) - -## 파일 구성 - -- `1.py`: 텍스트 프롬프트 기반 이미지 프레임 생성 후 mp4 저장 -- `3.py`: 챔피언 프롬프트 기반 이미지 생성 -- `cpu.py`, `cuda.py`, `cuda2.py`, `cuda3.py`, `cuda22.py`: 다양한 생성 파이프라인 실험 스크립트 -- `cuda2_rife.py`, `cuda2_rife_v2.py`: 프레임 생성 후 보간/합성(고FPS/오디오 결합) -- `mp4make.py`: 프레임 기반 영상 생성 및 합성 유틸 성격 스크립트 -- `imgwork.py`: 다수 프롬프트 배치 생성 스크립트 - -## 실행 환경 메모 - -1. CUDA GPU가 있으면 `torch.float16` + `cuda` 경로를 사용하고, 없으면 CPU 경로(`float32`)를 사용합니다. -2. 일부 스크립트는 대량 프레임을 생성하므로 VRAM/디스크 여유 공간이 필요합니다. -3. ffmpeg가 설치되어 있어야 영상 합성 스크립트가 정상 동작합니다. - -## 보안/콘텐츠 정리 사항 - -- 저장소 내 하드코딩된 Hugging Face 토큰은 마스킹 처리되었고, 아래 형태로 환경변수 사용을 권장합니다. - - `HF_TOKEN = os.getenv("HF_TOKEN", "hf_***MASKED***")` - -## 빠른 시작 예시 +## 기술 문서 + +상세 기술 스택 문서는 `DOC/Chapter01.md` ~ `DOC/Chapter10.md`를 참고하세요. + +## 웹 대시보드 (FastAPI + Vanilla JS) + +- 백엔드: FastAPI +- 프런트엔드: Vanilla JS (정적 파일) +- 주요 API + - `GET /api/health` + - `GET /api/stack` + - `GET /api/files` + +## 로컬 실행 ```bash python3 -m venv .venv source .venv/bin/activate -export HF_TOKEN="hf_your_real_token" -python 3.py +pip install -r backend/requirements.txt +uvicorn backend.app.main:app --reload ``` -필요 시 각 스크립트 내 `model_id`, `prompt`, `fps`, `width/height`, `num_frames`를 목적에 맞게 조정하세요. +브라우저에서 `http://localhost:8000` 접속. + +## Docker 실행 + +```bash +docker compose up --build +``` + +브라우저에서 `http://localhost:8000` 접속. + +## 테스트 + +```bash +pytest -q +``` diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..51e856c --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,69 @@ +from pathlib import Path + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +BASE_DIR = Path(__file__).resolve().parents[2] +FRONTEND_DIR = BASE_DIR / "frontend" + + +class HealthResponse(BaseModel): + status: str + + +class StackItem(BaseModel): + name: str + category: str + description: str + + +class FilesResponse(BaseModel): + python_files: list[str] + + +app = FastAPI(title="python-generate-image dashboard", version="1.0.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/api/health", response_model=HealthResponse) +def health() -> HealthResponse: + return HealthResponse(status="ok") + + +@app.get("/api/stack", response_model=list[StackItem]) +def stack() -> list[StackItem]: + return [ + StackItem(name="Python 3", category="Language", description="Core runtime for scripts and API service."), + StackItem(name="FastAPI", category="Backend", description="REST API and static file serving."), + StackItem(name="Vanilla JavaScript", category="Frontend", description="Lightweight browser UI without framework."), + StackItem(name="PyTorch", category="AI", description="Tensor runtime for CPU/GPU inference."), + StackItem(name="Diffusers", category="AI", description="Stable Diffusion and related image generation pipelines."), + StackItem(name="OpenCV/Pillow", category="Media", description="Frame and image processing utilities."), + StackItem(name="FFmpeg", category="Media", description="Video encoding and audio merge pipeline."), + StackItem(name="Docker", category="DevOps", description="Container-based local execution and testing."), + ] + + +@app.get("/api/files", response_model=FilesResponse) +def files() -> FilesResponse: + python_files = sorted(path.name for path in BASE_DIR.glob("*.py")) + return FilesResponse(python_files=python_files) + + +if FRONTEND_DIR.exists(): + app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static") + + +@app.get("/") +def index() -> FileResponse: + return FileResponse(FRONTEND_DIR / "index.html") diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..4b9e36a --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.111.1 +uvicorn[standard]==0.30.6 +pydantic==2.8.2 +pytest==8.3.2 +httpx==0.27.2 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1e68f17 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3.9" + +services: + web: + build: . + container_name: python-generate-image-web + ports: + - "8000:8000" + restart: unless-stopped diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..5ece5c3 --- /dev/null +++ b/frontend/app.js @@ -0,0 +1,53 @@ +async function fetchJson(url) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`요청 실패: ${url}`); + } + return response.json(); +} + +function renderList(elementId, items, formatter) { + const el = document.getElementById(elementId); + el.innerHTML = ""; + + if (!items.length) { + const li = document.createElement("li"); + li.className = "muted"; + li.textContent = "데이터가 없습니다."; + el.appendChild(li); + return; + } + + items.forEach((item) => { + const li = document.createElement("li"); + li.textContent = formatter(item); + el.appendChild(li); + }); +} + +async function loadDashboard() { + const healthEl = document.getElementById("health"); + + try { + const [health, stack, files] = await Promise.all([ + fetchJson("/api/health"), + fetchJson("/api/stack"), + fetchJson("/api/files"), + ]); + + healthEl.textContent = `상태: ${health.status}`; + + renderList( + "stack-list", + stack, + (item) => `[${item.category}] ${item.name} - ${item.description}`, + ); + + renderList("files-list", files.python_files, (name) => name); + } catch (error) { + healthEl.textContent = "상태 확인 실패"; + healthEl.classList.add("muted"); + } +} + +loadDashboard(); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0b61b5d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,30 @@ + + + + + + python-generate-image Stack Dashboard + + + +
+

python-generate-image 기술 스택 대시보드

+
+

서비스 상태

+

확인 중...

+
+ +
+

기술 스택

+ +
+ +
+

루트 Python 스크립트

+ +
+
+ + + + diff --git a/frontend/styles.css b/frontend/styles.css new file mode 100644 index 0000000..d7e6a12 --- /dev/null +++ b/frontend/styles.css @@ -0,0 +1,54 @@ +:root { + --bg: #0f172a; + --panel: #111827; + --text: #e5e7eb; + --muted: #9ca3af; + --accent: #22d3ee; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: Arial, sans-serif; + background: linear-gradient(160deg, #0b1020, var(--bg)); + color: var(--text); +} + +.container { + max-width: 900px; + margin: 0 auto; + padding: 32px 16px 48px; +} + +h1 { + margin-bottom: 24px; +} + +.panel { + background: var(--panel); + border: 1px solid #1f2937; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; +} + +h2 { + margin-top: 0; + color: var(--accent); +} + +ul { + margin: 0; + padding-left: 20px; +} + +li { + margin: 8px 0; +} + +.muted { + color: var(--muted); +} diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..9ca22b7 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,28 @@ +from fastapi.testclient import TestClient + +from backend.app.main import app + + +client = TestClient(app) + + +def test_health() -> None: + response = client.get("/api/health") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + +def test_stack() -> None: + response = client.get("/api/stack") + assert response.status_code == 200 + payload = response.json() + assert len(payload) >= 5 + assert all("name" in item and "category" in item for item in payload) + + +def test_files() -> None: + response = client.get("/api/files") + assert response.status_code == 200 + payload = response.json() + assert "python_files" in payload + assert "cpu.py" in payload["python_files"]