# Qwen-Image-Layered로 포스터 자동 레이어 분해 (2/10): 기술 스택 선정과 아키텍처 설계 ## 기술 스택 선정의 원칙 AI 서비스를 구축할 때 가장 먼저 직면하는 질문: "어떤 언어와 프레임워크를 쓸 것인가?" 이번 프로젝트의 선택 기준: 1. **AI 모델 통합 용이성** - Qwen-Image-Layered는 Python 기반 2. **개발 속도** - 빠른 프로토타이핑과 검증 3. **운영 안정성** - 프로덕션 배포 시 신뢰성 4. **확장성** - 향후 기능 추가 가능성 ## 백엔드: Python FastAPI vs Node.js ### 최종 선택: Python + FastAPI **선택 이유**: 1. **AI 모델 네이티브 지원** ```python # Qwen-Image-Layered 로딩이 자연스러움 from transformers import Qwen2VLForConditionalGeneration model = Qwen2VLForConditionalGeneration.from_pretrained( "Qwen/Qwen-Image-Layered" ) ``` Node.js에서는 Python 브릿지(child_process)가 필요하여 복잡도 증가. 2. **이미지 처리 라이브러리 풍부** - PIL/Pillow: 이미지 조작 - OpenCV: 고급 후처리 - numpy: 행렬 연산 - scikit-image: Alpha matting 3. **FastAPI의 현대적 기능** ```python from fastapi import FastAPI, UploadFile, File from fastapi.responses import StreamingResponse app = FastAPI() @app.post("/api/decompose") async def decompose( image: UploadFile = File(...), num_layers: int = 5 ): # 타입 힌팅, 자동 검증, OpenAPI 문서 생성 ... ``` - 자동 API 문서 (Swagger UI) - 비동기 처리 (async/await) - WebSocket 지원 - Pydantic 데이터 검증 ### 거부한 대안: Node.js + Express **장점**: - JavaScript 풀스택 가능 - npm 생태계 방대 - 비동기 I/O 우수 **단점**: - Python AI 모델 통합 복잡 - 이미지 처리 라이브러리 빈약 - GPU 연산 지원 제한적 **결론**: AI 중심 프로젝트에서는 Python이 압도적으로 유리. ## 프론트엔드: Vanilla JS vs React ### 최종 선택: Vanilla HTML/CSS/JavaScript **선택 이유**: 1. **단순성** - 빌드 도구 불필요 - 의존성 최소화 - 정적 파일 서빙만으로 배포 2. **certificate-automation UI 재사용** 20251219-make-certificate-automation 프로젝트의 검증된 인터페이스: ```html

포스터 이미지를 드래그하거나 클릭하여 업로드

``` 3. **빠른 프로토타이핑** React 같은 프레임워크는 다음이 필요: - npm install - webpack/vite 설정 - JSX 컴파일 - 상태 관리 라이브러리 단순 파일 업로드 UI에는 과도한 복잡도. ### 거부한 대안: React **장점**: - 컴포넌트 재사용성 - 상태 관리 체계적 - 대규모 UI 확장 용이 **단점**: - 초기 설정 시간 - 번들 크기 증가 - 서버 사이드 렌더링 고려 필요 **결론**: MVP에서는 Vanilla JS로 시작. 향후 복잡도 증가 시 React 마이그레이션 고려. ## AI 인프라: 로컬 GPU vs Vertex AI ### 하이브리드 전략 채택 **Phase 1 (개발): 로컬 GPU** - RTX 3090 (24GB VRAM) 사용 - 빠른 실험 및 디버깅 - 비용 제로 **Phase 2 (프로덕션): Vertex AI Prediction** - 사용자 요청 폭증 시 클라우드로 오프로드 - 자동 스케일링 - GPU 유휴 시간 제거 ### 로컬 GPU 설정 ```python import torch # GPU 사용 가능 여부 확인 if torch.cuda.is_available(): device = "cuda" print(f"GPU: {torch.cuda.get_device_name(0)}") else: device = "cpu" print("Warning: CPU 모드 (느림)") # 모델 로딩 model = Qwen2VLForConditionalGeneration.from_pretrained( "Qwen/Qwen-Image-Layered", torch_dtype=torch.float16, # GPU 메모리 절약 device_map="auto" ) ``` ### Vertex AI 백업 전략 ```python from google.cloud import aiplatform class HybridDecomposer: def __init__(self): self.local_model = QwenLocalModel() self.vertex_endpoint = aiplatform.Endpoint( endpoint_name="qwen-layered-endpoint" ) async def decompose(self, image): if self.local_model.is_available(): # 로컬 GPU 우선 사용 return await self.local_model.process(image) else: # GPU 사용 중이면 Vertex AI로 폴백 return await self.vertex_endpoint.predict(image) ``` ## 전체 시스템 아키텍처 ``` ┌─────────────────────────────────────────────────────┐ │ Web Browser │ │ ┌──────────────────────────────────────────────┐ │ │ │ Vanilla HTML/CSS/JavaScript │ │ │ │ - Drag & Drop Upload │ │ │ │ - Progress Bar (WebSocket) │ │ │ │ - Layer Preview │ │ │ └──────────────┬───────────────────────────────┘ │ └─────────────────┼───────────────────────────────────┘ │ HTTP/WebSocket ┌─────────────────▼───────────────────────────────────┐ │ FastAPI Backend (Python 3.11) │ │ ┌──────────────────────────────────────────────┐ │ │ │ API Routes │ │ │ │ - POST /api/upload │ │ │ │ - POST /api/decompose │ │ │ │ - GET /api/status/:job_id (WebSocket) │ │ │ │ - GET /api/download/:job_id │ │ │ └──────────────┬───────────────────────────────┘ │ │ ┌──────────────▼───────────────────────────────┐ │ │ │ Business Logic Layer │ │ │ │ - ImageProcessor │ │ │ │ - JobQueue (Redis) │ │ │ │ - StorageManager │ │ │ └──────────────┬───────────────────────────────┘ │ └─────────────────┼───────────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────────┐ │ AI Processing Layer │ │ ┌────────────────────┐ ┌──────────────────┐ │ │ │ QwenDecomposer │ │ GeminiAnalyzer │ │ │ │ (Local GPU) │ │ (Vertex AI) │ │ │ │ - Model Loading │ │ - Layer Desc │ │ │ │ - Inference │ │ - Recommend │ │ │ │ - RGBA Export │ │ - Quality Check │ │ │ └────────────────────┘ └──────────────────┘ │ └─────────────────┬───────────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────────┐ │ Storage Layer │ │ /storage/ │ │ /uploads/ - 원본 이미지 임시 저장 │ │ /results/ - 처리 완료 레이어 │ │ /{job_id}/ │ │ - layer_0.png │ │ - layer_1.png │ │ - metadata.json │ │ - preview.jpg (썸네일) │ └─────────────────────────────────────────────────────┘ ``` ## 데이터 흐름 ### 사용자 → 레이어 다운로드 전체 흐름 ``` 1. 사용자: 이미지 업로드 (poster.jpg, 5 레이어) ↓ 2. FastAPI: /api/decompose 엔드포인트 - 파일 검증 (크기, 형식) - Job ID 생성 (UUID) - Redis 큐에 추가 ↓ 3. Background Worker: Job 처리 시작 - 이미지 로드 - Qwen-Image-Layered 추론 - 진행 상황 → WebSocket 전송 (10%, 30%, 50%...) ↓ 4. AI Processing: - 레이어 생성 (RGBA PNG) - 각 레이어 저장 ↓ 5. Gemini Vision API (선택): - 각 레이어 분석 - 설명 생성 ("배경", "텍스트", "메인 이미지") ↓ 6. 완료: - metadata.json 생성 - WebSocket: 100% 완료 알림 - 프론트엔드: 다운로드 버튼 활성화 ↓ 7. 사용자: ZIP 다운로드 또는 개별 레이어 다운로드 ``` ## 비동기 작업 큐: Redis 왜 작업 큐가 필요한가? 이미지 분해는 30초 ~ 10분 소요. HTTP 요청은 기본 타임아웃 30초. → 백그라운드 작업 필수. ### Redis 기반 작업 큐 ```python # job_queue.py import redis import json import uuid class JobQueue: def __init__(self): self.redis = redis.Redis( host='localhost', port=6379, db=0 ) def enqueue(self, image_path, num_layers): """작업 큐에 추가""" job_id = str(uuid.uuid4()) job_data = { "job_id": job_id, "image_path": image_path, "num_layers": num_layers, "status": "queued", "progress": 0, "created_at": datetime.now().isoformat() } # Redis에 저장 self.redis.set(f"job:{job_id}", json.dumps(job_data)) self.redis.lpush("job_queue", job_id) return job_id def update_progress(self, job_id, progress, message): """진행 상황 업데이트""" job_data = json.loads(self.redis.get(f"job:{job_id}")) job_data["progress"] = progress job_data["message"] = message self.redis.set(f"job:{job_id}", json.dumps(job_data)) # WebSocket으로 브로드캐스트 self.broadcast_update(job_id, job_data) ``` ### Worker 프로세스 ```python # worker.py async def process_jobs(): """백그라운드 워커""" queue = JobQueue() decomposer = QwenDecomposer() while True: # 큐에서 작업 가져오기 job_id = queue.dequeue() if job_id: job_data = queue.get_job(job_id) try: # 상태 업데이트 queue.update_progress(job_id, 10, "모델 로딩 중...") # AI 처리 layers = await decomposer.decompose( image_path=job_data["image_path"], num_layers=job_data["num_layers"], progress_callback=lambda p: queue.update_progress( job_id, p, f"레이어 {p//20} 생성 중..." ) ) # 완료 queue.update_progress(job_id, 100, "완료!") queue.mark_completed(job_id, layers) except Exception as e: queue.mark_failed(job_id, str(e)) await asyncio.sleep(0.1) ``` ## 환경 변수 관리 `.env` 파일: ```bash # App 설정 APP_ENV=development HOST=0.0.0.0 PORT=8000 DEBUG=true # Qwen-Image-Layered MODEL_NAME=Qwen/Qwen-Image-Layered MODEL_CACHE_DIR=./models DEFAULT_RESOLUTION=1024 DEFAULT_NUM_LAYERS=5 INFERENCE_STEPS=50 # GPU 설정 CUDA_VISIBLE_DEVICES=0 PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512 # Google Cloud Vertex AI GOOGLE_CLOUD_PROJECT=planitai-project GOOGLE_APPLICATION_CREDENTIALS=./service-account.json VERTEX_AI_LOCATION=us-central1 # Redis REDIS_HOST=localhost REDIS_PORT=6379 REDIS_DB=0 # Storage UPLOAD_DIR=./storage/uploads RESULTS_DIR=./storage/results MAX_FILE_SIZE=10485760 # 10MB CLEANUP_AFTER_HOURS=24 ``` ## 프로젝트 파일 구조 ``` 20251221-qwen-image-layered/ ├── backend/ │ ├── app/ │ │ ├── __init__.py │ │ ├── main.py # FastAPI 앱 진입점 │ │ ├── config.py # 환경 변수 로딩 │ │ │ │ │ ├── api/ # API 라우트 │ │ │ ├── __init__.py │ │ │ ├── upload.py │ │ │ ├── decompose.py │ │ │ └── status.py │ │ │ │ │ ├── models/ # AI 모델 │ │ │ ├── __init__.py │ │ │ ├── qwen_decomposer.py │ │ │ └── gemini_analyzer.py │ │ │ │ │ ├── services/ # 비즈니스 로직 │ │ │ ├── __init__.py │ │ │ ├── processor.py │ │ │ ├── storage.py │ │ │ └── job_queue.py │ │ │ │ │ └── utils/ │ │ ├── logger.py │ │ └── validators.py │ │ │ ├── worker.py # 백그라운드 워커 │ ├── requirements.txt │ ├── Dockerfile │ └── .env │ ├── frontend/ │ ├── index.html │ ├── style.css │ └── app.js │ ├── storage/ │ ├── uploads/ │ └── results/ │ └── docs/ └── architecture.md ``` ## 다음 단계 v3에서는 **Qwen-Image-Layered 모델**을 깊이 분석한다: - 논문 리뷰: 어떻게 레이어 분해를 하는가? - Qwen2.5-VL 백본 이해 - Diffusion 기반 레이어 생성 원리 - 입력/출력 형식 상세 기술 스택이 결정되었으니, 이제 핵심 AI 모델이 실제로 어떻게 작동하는지 이해할 차례다. --- **이전 글**: [프로젝트 시작과 목표 (1/10)](./qwen-image-layered-v1.md) **다음 글**: [Qwen-Image-Layered 모델 깊이 이해 (3/10)](./qwen-image-layered-v3.md)