박재오 7결정 + Obsidian 3개 문서(7결정 통합/API 부하/역할 분담)를 실행 가능한 형태로 정리. 12개 SP 분할 (Track A Quick Win 2건 + Track B Infrastructure 10건), 의존성 그래프, 시간대 조건부 우선순위(평일 비휴장일만 트레이딩 HIGH), Windows Render Worker 통합 패턴 (인스타·음악·영상 셋이 같은 구조), Redis 큐 컨벤션, SMB direct write + NAS internal webhook, X-WebAI-Key / X-Internal-Key 분리, 3-layer 차단(IP 화이트리스트 + Tailscale + 헤더), Suno+영상 API 키 Windows 이전 명세. 첫 plan 대상: Track A (SP-A1 web-ai 캐시 TTL + SP-A2 NAS stock TTLCache, ~40분 작업, V2 재시작 시 NAS 인바운드 70% 감소). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
585 lines
22 KiB
Markdown
585 lines
22 KiB
Markdown
# NAS ↔ Windows 분산 아키텍처 — Design Spec
|
||
|
||
**Date:** 2026-05-18
|
||
**Author:** CEO (with Claude)
|
||
**Scope:** `web-backend` + `web-ai` + 신규 `web-ai-services` (Windows WSL2 컨테이너 모음)
|
||
|
||
---
|
||
|
||
## 1. 배경 & 목적
|
||
|
||
NAS Synology J4025 (Celeron 2C/2.0GHz, 18GB)에서 11개 docker 컨테이너가 CPU를 과점유. 진단 결과 가장 큰 원인은 **외부 인바운드 API 호출 빈도** (web-ai signal_v1/v2가 분당 12회 NAS stock 호출) + **insta-lab Playwright Chromium의 동시 launch 비용**이었다.
|
||
|
||
박재오 통찰: *"NAS = 24/7 표출·게이트웨이 / Windows = 트레이딩 메인 + 트리거 기반 컴퓨팅"*. 박재오가 이미 7건의 의사결정을 마쳤고 1주 셋업 가이드도 정리되어 있다 (`Obsidian Vault/raw/2026-05-18-Windows-NAS-아키텍처-7결정-통합.md`).
|
||
|
||
본 spec은 그 위에 **실행 단위 분할(SP) + 의존성 그래프 + 통합 패턴 + 데이터 플로우**를 정리해서 실제 구현 plan으로 진입 가능한 형태로 만든다.
|
||
|
||
### 박재오 7결정 (수용된 결정 사항)
|
||
|
||
1. **d+b 조합** — Windows 작업 감지 큐 정지 + 트레이딩 우선순위 High
|
||
2. **insta-lab Playwright 1순위** 이전 (NAS → Windows)
|
||
3. **트리거 B(비동기) + C(예약)** — 즉시 응답 X, task_id 발급 + 폴링
|
||
4. **외부 영상 생성 도구** (Runway·Sora·Veo·Pika·Kling·Luma)
|
||
5. **Redis NAS 컨테이너** — 24/7 안정 큐
|
||
6. **옵션 4 하이브리드** — 트레이딩 Native Python / 신규 WSL2 Docker Engine
|
||
7. **NSSM** — Windows Service 도구 (자동 시작·우선순위)
|
||
|
||
---
|
||
|
||
## 2. 전체 아키텍처
|
||
|
||
```
|
||
[사용자 브라우저]
|
||
↓ HTTPS
|
||
[NAS Synology J4025] ─── 24/7 안정 · 표출 · 게이트웨이 · 상태(state)
|
||
├─ frontend (nginx :8080) React SPA
|
||
├─ redis (:6379) ⭐ NEW 24/7 큐 + 캐시
|
||
├─ stock (:18500) +TTLCache 메타 + KIS data + WebAI gateway
|
||
├─ insta-lab (:18700) 분할 후 카피 생성 + DB + Redis push
|
||
├─ music-lab (:18600) 분할 후 메타 + Redis push (Suno/MusicGen 미실행)
|
||
├─ video-lab (:18XXX) ⭐ NEW 영상 게이트웨이 + Redis push
|
||
├─ agent-office (:18900) 텔레그램 라우팅 + scheduler
|
||
├─ lotto / realestate-lab / personal / packs-lab / travel-proxy
|
||
└─ deployer (:19010)
|
||
↓ Redis BLPOP / 직접 HTTP webhook
|
||
[Windows AI Server 192.168.45.59] ─── 트레이딩 최우선 · 트리거 컴퓨팅
|
||
├─ 🔵 Native Python (NSSM HIGH priority)
|
||
│ ├─ signal_v2 (:8001) ⭐ 트레이딩 절대 우선
|
||
│ ├─ Ollama qwen3:14b (:11435)
|
||
│ └─ MusicGen (:8765)
|
||
└─ 🟢 WSL2 + Docker Engine (NORMAL priority)
|
||
├─ insta-render (:18710) ⭐ NEW Playwright Chromium pool
|
||
├─ music-render (:18711) ⭐ NEW Suno API + MusicGen orchestration
|
||
├─ video-render (:18712) ⭐ NEW 외부 영상 API gateway (6 provider)
|
||
└─ task-watcher 박재오 작업 감지 + 시간대 분기
|
||
```
|
||
|
||
### 핵심 원칙
|
||
|
||
1. **NAS = state(DB) + view(nginx 미디어 서빙)**, **Windows = stateless compute**
|
||
2. **트레이딩 절대 우선** — 시간대 조건부 (아래 §3 참조)
|
||
3. **무거운 작업 시간대 분리** — 데드존 23:30–04:30 + 주말·휴장일 = 골든타임
|
||
|
||
---
|
||
|
||
## 3. 시간대별 우선순위 모드
|
||
|
||
| 모드 | 조건 | signal_v2 | task-watcher 정책 |
|
||
|------|------|-----------|------------------|
|
||
| 🔴 트레이딩 | 평일 비휴장일 07:00–16:30 | NSSM HIGH, polling 활성 | 박재오 활동 감지 시 `queue:paused` SET |
|
||
| 🟡 일반 | 평일 16:30–23:30 (NXT) | NSSM HIGH 유지 (5분 폴링 가벼움) | 박재오 활동 감지 시 SET |
|
||
| 🟢 자유 | 주말·휴장일 + 평일 23:30–04:30 | 자동 idle (휴장일 polling 미실행) | `queue:paused` DEL 유지 — 큐 항상 활성 |
|
||
|
||
### 구현 위치
|
||
- **signal_v2의 휴장일 인식**: `web-ai` CHECK_POINT #7 `holidays.json` 자동 동기화 항목. 휴장일·주말에 polling 자체 미실행.
|
||
- **휴장일 단일 소스**: `web-backend/stock/app/holidays.json` 정본. NAS stock이 `GET /api/stock/holidays`로 노출. signal_v2 + task-watcher가 매일 00:00 갱신.
|
||
- **task-watcher 시간대 분기**: `current_mode()` 함수가 30초 폴링마다 모드 판정 → `queue:paused` 토글.
|
||
|
||
---
|
||
|
||
## 4. Sub-project 카탈로그 (12개)
|
||
|
||
| SP | 명칭 | 트랙 | 위치 | 소요 |
|
||
|----|------|------|------|------|
|
||
| **SP-A1** | web-ai 캐시 TTL 증가 | A | `web-ai/signal_v2/stock_client.py` | 10분 |
|
||
| **SP-A2** | NAS stock TTLCache | A | `web-backend/stock/app/*` | 30분 |
|
||
| **SP-1** | NAS Redis 컨테이너 | B (Base) | `web-backend/docker-compose.yml` | 30분 |
|
||
| **SP-2** | Windows WSL2 + Docker Engine | B (Base) | (Windows AI) | 2h |
|
||
| **SP-3** | insta-render Windows 서비스 | B | `web-ai-services/insta-render/` (신규) | 4h |
|
||
| **SP-4** | NAS insta-lab 분할 | B | `web-backend/insta-lab` | 2h |
|
||
| **SP-5** | music-render Windows 서비스 | B | `web-ai-services/music-render/` (신규) | 3h |
|
||
| **SP-6** | NAS music-lab 분할 | B | `web-backend/music-lab` | 2h |
|
||
| **SP-7** | video-render Windows 서비스 | B | `web-ai-services/video-render/` (신규) | 3h |
|
||
| **SP-8** | NAS video-lab 신설 | B | `web-backend/video-lab/` (신규 컨테이너) | 2h |
|
||
| **SP-9** | NSSM 자동 시작 + 우선순위 | B | (Windows) | 1h |
|
||
| **SP-10** | task-watcher (시간대 + 활동 감지) | B | `web-ai-services/task-watcher/` (신규) | 2h |
|
||
|
||
**총 작업시간**: ~22.5h (1주 일정에 부합)
|
||
|
||
### 의존성 그래프
|
||
|
||
```
|
||
A 트랙 (병행, ~40분)
|
||
SP-A1 ─╮
|
||
├── V2 재시작 시 효과
|
||
SP-A2 ─╯
|
||
|
||
B 트랙 Base (Day 1~2)
|
||
SP-1 (Redis) ─┐
|
||
├── 인스타·음악·영상 3 트랙 모두 의존
|
||
SP-2 (WSL2) ──┘
|
||
|
||
인스타 트랙 (Day 3~4)
|
||
SP-3 (insta-render) ──→ SP-4 (NAS insta-lab 분할)
|
||
|
||
음악 트랙 (Day 4~5)
|
||
SP-5 (music-render) ──→ SP-6 (NAS music-lab 분할)
|
||
|
||
영상 트랙 (Day 5~6)
|
||
SP-7 (video-render) ──→ SP-8 (NAS video-lab 신설)
|
||
|
||
인프라 마무리 (Day 6~7)
|
||
SP-9 (NSSM) ──→ SP-10 (task-watcher)
|
||
```
|
||
|
||
### Critical Path
|
||
`SP-1 ∥ SP-2` → `SP-3` → `SP-4` → `SP-9` → `SP-10` (최단 약 11.5h)
|
||
|
||
병렬화: SP-1(NAS)·SP-2(Windows)는 다른 머신이라 동시 진행. 인스타·음악·영상 트랙은 패턴이 같아 한 번 정착 후 빠르게 복제.
|
||
|
||
---
|
||
|
||
## 5. 통합 패턴 — "Windows Render Worker"
|
||
|
||
인스타·음악·영상 3 트랙이 **완전히 같은 패턴**. 한 번만 정의하고 3번 재사용한다.
|
||
|
||
### 시퀀스
|
||
|
||
```
|
||
사용자 ─POST /api/{kind}/generate ...──→ NAS {kind}-lab
|
||
│
|
||
├─ DB.create_task() → task_id
|
||
├─ Redis RPUSH queue:{kind}-render {task_id, params, ...}
|
||
└─ 200 {task_id} ─→ 사용자
|
||
|
||
[Windows {kind}-render]
|
||
│ (queue:paused 체크 후 BLPOP queue:{kind}-render)
|
||
│
|
||
├─ POST /api/internal/{kind}/update
|
||
│ {status: "processing", progress: 30} ─→ NAS DB update
|
||
│
|
||
├─ 무거운 작업 (Playwright / Suno / Runway 등)
|
||
│ 결과 파일 → /mnt/nas/data/{kind}/{id}/{file} (SMB direct write)
|
||
│
|
||
└─ POST /api/internal/{kind}/update
|
||
{status: "succeeded", progress: 100,
|
||
result_path: "/media/{kind}/{id}/{file}"} ─→ NAS DB update
|
||
|
||
사용자 ─GET /api/{kind}/tasks/{task_id}──→ NAS {kind}-lab
|
||
└─ DB.get_task() → {status, progress, result_path}
|
||
─→ 사용자 (폴링)
|
||
```
|
||
|
||
### 4가지 미세 개선 (반영됨)
|
||
|
||
1. **결과물 저장**: SMB direct write (`/mnt/nas/data/`) — 별도 HTTP upload 단계 제거
|
||
2. **NAS 알림**: Windows → NAS internal webhook (`POST /api/internal/{kind}/update`) — NAS polling 부담 0
|
||
3. **사용자 응답**: 폴링 유지 (YAGNI, 미래 SSE 검토)
|
||
4. **인증 키 분리**: `X-WebAI-Key`(read, web-ai→NAS) vs `X-Internal-Key`(write, Windows→NAS)
|
||
|
||
---
|
||
|
||
## 6. Redis 키 컨벤션
|
||
|
||
| 키 | 종류 | TTL | 용도 |
|
||
|----|------|-----|------|
|
||
| `queue:insta-render` | list | (없음) | 인스타 카드 렌더 작업 큐 |
|
||
| `queue:music-render` | list | (없음) | 음악 생성 작업 큐 |
|
||
| `queue:video-render` | list | (없음) | 영상 생성 작업 큐 |
|
||
| `queue:paused` | string `"1"` | 600s | task-watcher가 set/del. worker가 BLPOP 전 확인 |
|
||
| (옵션) `cache:stock:*` | string (json) | 120~600s | NAS stock Redis 캐시 (SP-A2와 별개 옵션) |
|
||
|
||
### 큐 payload 표준 (JSON)
|
||
|
||
```json
|
||
{
|
||
"task_id": "uuid-...",
|
||
"kind": "insta|music|video",
|
||
"params": { ... },
|
||
"submitted_at": "2026-05-18T08:30:00+09:00"
|
||
}
|
||
```
|
||
|
||
Worker는 `BLPOP queue:{kind}-render` (1초 timeout) → `queue:paused` 체크 → 처리.
|
||
|
||
---
|
||
|
||
## 7. NAS 볼륨 레이아웃 + nginx 서빙
|
||
|
||
### 실 파일 시스템
|
||
```
|
||
/volume1/docker/webpage/data/
|
||
├── insta/{slate_id}/01.png ~ 10.png
|
||
├── music/{track_id}/{file}.mp3
|
||
└── video/{video_id}/{file}.mp4
|
||
```
|
||
|
||
### WSL2 마운트
|
||
```bash
|
||
# WSL2 /etc/fstab
|
||
//gahusb.synology.me/docker/webpage/data /mnt/nas cifs username=...,vers=3.0,uid=1000,_netdev 0 0
|
||
```
|
||
|
||
### nginx 서빙
|
||
```
|
||
https://gahusb.synology.me/media/insta/{id}/01.png
|
||
/music/{id}/...
|
||
/video/{id}/...
|
||
```
|
||
|
||
→ nginx `location /media/` 블록은 `/volume1/docker/webpage/data/`를 alias로 서빙 (기존 패턴).
|
||
|
||
---
|
||
|
||
## 8. NAS internal webhook 명세
|
||
|
||
### Endpoint
|
||
`POST /api/internal/{kind}/update` (kind ∈ `insta`|`music`|`video`)
|
||
|
||
### 인증 — 3-layer 차단
|
||
1. **nginx IP 화이트리스트** (Layer 1·2):
|
||
```nginx
|
||
location /api/internal/ {
|
||
allow 192.168.45.0/24; # LAN 화이트리스트
|
||
allow 100.64.0.0/10; # Tailscale CGNAT 대역
|
||
deny all;
|
||
...
|
||
}
|
||
```
|
||
2. **`X-Internal-Key` 헤더 검증** (Layer 3): `verify_internal_key` dependency
|
||
|
||
### Payload
|
||
```json
|
||
{
|
||
"task_id": "uuid-...",
|
||
"status": "processing|succeeded|failed",
|
||
"progress": 0-100,
|
||
"result_path": "/media/insta/123/01.png", // succeeded일 때만, nginx 경로
|
||
"error": "exception message" // failed일 때만
|
||
}
|
||
```
|
||
|
||
### NAS 측 처리
|
||
1. `tasks` 테이블 row update (status, progress, result_path, error)
|
||
2. (옵션) Redis PUBLISH `task:{id}` — 미래 SSE 통합 시 활용
|
||
3. 200 응답 (또는 401 if invalid key)
|
||
|
||
### 인증 키 정책
|
||
|
||
| 키 | 방향 | 권한 | 위치 |
|
||
|----|------|------|------|
|
||
| `X-WebAI-Key` | web-ai → NAS | read-only (`GET /api/webai/*`) | NAS `.env` + web-ai `.env` |
|
||
| `X-Internal-Key` | Windows worker → NAS | write-only (`POST /api/internal/*`) | NAS `.env` + Windows `.env` |
|
||
|
||
분리 사유: Principle of Least Privilege, 독립 로테이션, 감사 로그 명확성.
|
||
|
||
### 인증 helper (NAS 공통 모듈, `web-backend/_shared/auth.py` 또는 각 컨테이너 복제)
|
||
|
||
```python
|
||
from fastapi import Header, HTTPException
|
||
import os
|
||
|
||
async def verify_internal_key(x_internal_key: str = Header(...)):
|
||
expected = os.getenv("INTERNAL_API_KEY")
|
||
if not expected or x_internal_key != expected:
|
||
raise HTTPException(401, "Invalid X-Internal-Key")
|
||
|
||
# 라우터 사용
|
||
@app.post("/api/internal/insta/update", dependencies=[Depends(verify_internal_key)])
|
||
async def insta_update(payload: InternalUpdate): ...
|
||
```
|
||
|
||
기존 `verify_webai_key` 패턴(메모리 `reference_webai_auth_pattern.md`)을 복제.
|
||
|
||
---
|
||
|
||
## 9. Suno + 외부 영상 API 키 이전
|
||
|
||
NAS `.env`에서 다음 키들을 **제거** → Windows `.env`로 이전:
|
||
|
||
| 키 | NAS 이전 | Windows 이후 |
|
||
|-----|---------|-------------|
|
||
| `SUNO_API_KEY` | music-lab | music-render |
|
||
| `RUNWAY_API_KEY` | (없음) | video-render |
|
||
| `OPENAI_API_KEY` (Sora) | (있을 수도) | video-render |
|
||
| `GEMINI_API_KEY` (Veo) | (없음) | video-render |
|
||
| `PIKA_API_KEY` / `KLING_API_KEY` / `LUMA_API_KEY` | (없음) | video-render |
|
||
|
||
→ NAS music-lab + video-lab은 외부 API 호출 코드를 가지지 않음. Redis push만.
|
||
|
||
---
|
||
|
||
## 10. SP 상세 명세
|
||
|
||
### SP-A1 — web-ai 캐시 TTL 증가 (10분)
|
||
|
||
**파일**: `web-ai/signal_v2/stock_client.py`
|
||
|
||
변경:
|
||
```python
|
||
# 변경 전
|
||
PORTFOLIO_TTL = 60
|
||
NEWS_TTL = 300
|
||
SCREENER_TTL = 60
|
||
|
||
# 변경 후
|
||
PORTFOLIO_TTL = 180 # 3분
|
||
NEWS_TTL = 600 # 10분
|
||
SCREENER_TTL = 300 # 5분
|
||
```
|
||
|
||
**효과**: 분당 12 → 3~4 호출 (~70% 감소), 캐시 hit ratio 0~50% → 66~80%
|
||
|
||
### SP-A2 — NAS stock TTLCache (30분)
|
||
|
||
**파일**: `web-backend/stock/app/*` (webai endpoint 위치 확인 후)
|
||
|
||
```python
|
||
from cachetools import TTLCache
|
||
|
||
_PORTFOLIO_CACHE = TTLCache(maxsize=1, ttl=120)
|
||
_NEWS_CACHE = TTLCache(maxsize=10, ttl=600)
|
||
_SCREENER_CACHE = TTLCache(maxsize=10, ttl=180)
|
||
|
||
@app.get("/api/webai/portfolio", dependencies=[Depends(verify_webai_key)])
|
||
async def portfolio():
|
||
if "result" in _PORTFOLIO_CACHE:
|
||
return _PORTFOLIO_CACHE["result"]
|
||
result = await compute_portfolio()
|
||
_PORTFOLIO_CACHE["result"] = result
|
||
return result
|
||
```
|
||
|
||
3 endpoint 적용: `/api/webai/portfolio` · `/api/webai/news-sentiment` · `/api/stock/screener/run`. `cachetools` 의존성 requirements.txt 확인.
|
||
|
||
**효과**: V1+V2 동시 호출도 NAS에서 1회 계산. KIS·LLM 재호출 방지.
|
||
|
||
### SP-1 — NAS Redis 컨테이너 (30분)
|
||
|
||
**파일**: `web-backend/docker-compose.yml`에 추가
|
||
|
||
```yaml
|
||
redis:
|
||
image: redis:7-alpine
|
||
container_name: redis
|
||
restart: unless-stopped
|
||
ports:
|
||
- "6379:6379"
|
||
volumes:
|
||
- ${RUNTIME_PATH}/redis-data:/data
|
||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||
healthcheck:
|
||
test: ["CMD", "redis-cli", "ping"]
|
||
interval: 60s
|
||
timeout: 5s
|
||
retries: 3
|
||
```
|
||
|
||
검증: `docker exec redis redis-cli PING` → `PONG`
|
||
|
||
### SP-2 — Windows WSL2 + Docker Engine + Tailscale (2h)
|
||
|
||
박재오 Windows AI Server에서 (관리자 PowerShell):
|
||
|
||
```powershell
|
||
wsl --install -d Ubuntu-22.04
|
||
# 재부팅 후
|
||
wsl -d Ubuntu-22.04
|
||
```
|
||
|
||
WSL2 안:
|
||
```bash
|
||
# Docker Engine
|
||
sudo apt update && sudo apt install -y ca-certificates curl gnupg
|
||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
|
||
sudo tee /etc/apt/sources.list.d/docker.list
|
||
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||
sudo usermod -aG docker $USER
|
||
|
||
# Tailscale
|
||
curl -fsSL https://tailscale.com/install.sh | sh
|
||
sudo tailscale up
|
||
|
||
# NAS SMB mount
|
||
sudo mkdir -p /mnt/nas
|
||
echo "//gahusb.synology.me/docker/webpage/data /mnt/nas cifs username=...,vers=3.0,uid=1000,_netdev 0 0" | sudo tee -a /etc/fstab
|
||
sudo mount -a
|
||
```
|
||
|
||
검증: `docker ps`, `tailscale status`, `ls /mnt/nas`
|
||
|
||
### SP-3 — insta-render Windows 서비스 (4h)
|
||
|
||
**디렉토리**: `C:\Users\jaeoh\Desktop\workspace\web-ai-services\insta-render\`
|
||
|
||
```
|
||
insta-render/
|
||
├── Dockerfile
|
||
├── docker-compose.yml
|
||
├── requirements.txt
|
||
├── .env
|
||
├── main.py
|
||
├── worker.py
|
||
└── card_renderer.py # 기존 NAS insta-lab/app/card_renderer.py 이식
|
||
```
|
||
|
||
핵심 로직:
|
||
- `worker.py`: Redis BLPOP `queue:insta-render` (paused 체크)
|
||
- `card_renderer.py`: Browser pool (`init_browser`/`shutdown_browser`) + `render_slate`
|
||
- `main.py`: 시작 시 browser init + worker async task spawn
|
||
- 완료 시 `/mnt/nas/data/insta/{slate_id}/` 저장 + NAS webhook `POST /api/internal/insta/update`
|
||
|
||
### SP-4 — NAS insta-lab 분할 (2h)
|
||
|
||
**파일**: `web-backend/insta-lab/app/main.py` + `app/card_renderer.py`
|
||
|
||
변경:
|
||
```python
|
||
# 변경 전 — NAS에서 직접 렌더
|
||
async def _bg_render(task_id: str, slate_id: int):
|
||
async with RENDER_SEMAPHORE:
|
||
await card_renderer.render_slate(slate_id, ...)
|
||
|
||
# 변경 후 — Redis 큐에 push만
|
||
import redis.asyncio as aioredis
|
||
redis_client = aioredis.from_url(os.getenv("REDIS_URL", "redis://redis:6379"))
|
||
|
||
async def _bg_render(task_id: str, slate_id: int):
|
||
payload = {"task_id": task_id, "kind": "insta",
|
||
"params": {"slate_id": slate_id, "theme": "hedgy75"},
|
||
"submitted_at": datetime.now(KST).isoformat()}
|
||
await redis_client.rpush("queue:insta-render", json.dumps(payload))
|
||
```
|
||
|
||
추가: `POST /api/internal/insta/update` endpoint (Windows webhook 수신).
|
||
삭제: `card_renderer.py` Playwright 코드 (Browser pool, Semaphore 등), `requirements.txt`에서 `playwright` 제거, Dockerfile에서 Chromium install 제거.
|
||
|
||
### SP-5 — music-render Windows 서비스 (3h)
|
||
|
||
**디렉토리**: `web-ai-services/music-render/`
|
||
|
||
- Suno API client (외부 SaaS, polling 1~5분)
|
||
- MusicGen local call (Windows localhost:8765)
|
||
- Redis BLPOP `queue:music-render`
|
||
- 결과 mp3 → `/mnt/nas/data/music/{track_id}/{file}.mp3`
|
||
- NAS webhook `POST /api/internal/music/update`
|
||
|
||
`SUNO_API_KEY` Windows `.env`에 단독 보관.
|
||
|
||
### SP-6 — NAS music-lab 분할 (2h)
|
||
|
||
Suno 호출 코드 + MusicGen 호출 코드 삭제. `_bg_generate` 함수를 Redis push로 변경. `POST /api/internal/music/update` endpoint 추가.
|
||
|
||
### SP-7 — video-render Windows 서비스 (3h)
|
||
|
||
**디렉토리**: `web-ai-services/video-render/`
|
||
|
||
6 provider gateway (Runway·Sora·Veo·Pika·Kling·Luma) — provider 선택은 payload에서. 각 외부 API 호출 + 결과 mp4 다운로드 → `/mnt/nas/data/video/{id}/`. NAS webhook.
|
||
|
||
### SP-8 — NAS video-lab 신설 (2h)
|
||
|
||
새 docker 컨테이너. `web-backend/video-lab/`:
|
||
- `app/main.py`: 2 endpoint
|
||
- `POST /api/video/generate` → Redis push `queue:video-render` + task_id 반환
|
||
- `GET /api/video/tasks/{id}` → DB 조회
|
||
- `app/db.py`: video_tasks 테이블 (sqlite)
|
||
- `POST /api/internal/video/update` (Windows webhook)
|
||
- Dockerfile, requirements, docker-compose.yml entry
|
||
|
||
매우 가벼움 (NAS CPU 부담 미미).
|
||
|
||
### SP-9 — NSSM 자동 시작 + 우선순위 (1h)
|
||
|
||
Windows AI에서 NSSM 다운로드 후:
|
||
|
||
```powershell
|
||
# 트레이딩 (Native, HIGH)
|
||
nssm install signal_v2 "C:\Python312\python.exe" "-m uvicorn main:app --host 0.0.0.0 --port 8001"
|
||
nssm set signal_v2 AppDirectory "C:\Users\jaeoh\Desktop\workspace\web-ai\signal_v2"
|
||
nssm set signal_v2 Priority HIGH_PRIORITY_CLASS
|
||
nssm set signal_v2 AppStartup AUTO
|
||
|
||
# WSL2 Docker (NORMAL)
|
||
nssm install wsl_docker "wsl" "-d Ubuntu-22.04 -- sudo service docker start && cd /workspace/web-ai-services && docker compose up -d"
|
||
nssm set wsl_docker Priority NORMAL_PRIORITY_CLASS
|
||
nssm set wsl_docker AppStartup AUTO
|
||
|
||
nssm start signal_v2
|
||
nssm start wsl_docker
|
||
```
|
||
|
||
### SP-10 — task-watcher (2h)
|
||
|
||
**디렉토리**: `web-ai-services/task-watcher/`
|
||
|
||
WSL2 Docker 컨테이너. 30초마다:
|
||
1. `current_mode()` 판정 (시간대 + holidays.json 체크 + KST 시각)
|
||
2. `is_user_active()` 판정 (마우스/키보드 idle < 5분 또는 게임 process 감지)
|
||
3. 모드 + 활동 → `queue:paused` 토글
|
||
- `mode == "free"` → `DEL queue:paused`
|
||
- `mode != "free" and active` → `SET queue:paused 1 EX 600`
|
||
- `mode != "free" and idle` → `DEL queue:paused`
|
||
|
||
---
|
||
|
||
## 11. 데이터 플로우 검증 — 인스타 사례 end-to-end
|
||
|
||
```
|
||
1. 사용자 클릭 "카드 생성"
|
||
POST /api/insta/slates/123/render
|
||
↓ NAS insta-lab
|
||
2. NAS insta-lab
|
||
- db.create_task("slate_render", {slate_id: 123}) → task_id="t-abc"
|
||
- redis.rpush("queue:insta-render", {task_id: "t-abc", kind: "insta", params: {slate_id: 123, theme: "hedgy75"}})
|
||
- 응답 {task_id: "t-abc"}
|
||
↓ 즉시 사용자
|
||
3. Windows insta-render worker
|
||
- redis.blpop("queue:insta-render", 1)
|
||
- paused 체크 → 통과
|
||
- webhook(processing, 10%) → NAS DB update
|
||
- Playwright 카드 10장 렌더 → /mnt/nas/data/insta/123/01.png..10.png
|
||
- webhook(processing, 90%) 진행률 보고
|
||
- webhook(succeeded, 100, result_path="/media/insta/123/01.png") → NAS DB update
|
||
4. 사용자 폴링
|
||
GET /api/insta/tasks/t-abc → {status: "succeeded", result_path: "/media/insta/123/01.png"}
|
||
브라우저에서 <img src="/media/insta/123/01.png" /> 렌더
|
||
```
|
||
|
||
---
|
||
|
||
## 12. Out of Scope
|
||
|
||
- V1/V2 재시작 결정 (사용자 보류, 두 process 정지 유지)
|
||
- NAS 하드웨어 업그레이드 (#12 보류)
|
||
- 컨테이너 리소스 제한 cpus 0.5 (#11 박재오 진행 금지)
|
||
- SSE/WS push 모델 (YAGNI, 폴링 유지)
|
||
- Grafana 모니터링 (NAS 자산 활용 옵션, 향후)
|
||
|
||
## 13. 위험 요소
|
||
|
||
| 위험 | 완화 |
|
||
|------|------|
|
||
| Windows 재부팅 시 worker 중단 | NSSM AppStartup AUTO + WSL2 자동 시작 (SP-9) |
|
||
| Windows ↔ NAS 네트워크 단절 | task가 큐에 남음, NAS 측 timeout 처리 (예: 30분 timeout → failed) |
|
||
| 박재오 게임·작업 중 worker 충돌 | task-watcher queue:paused (SP-10) + NORMAL priority |
|
||
| Suno API rate limit | music-render 내부에서 retry + 큐 직렬 처리 |
|
||
| SMB 마운트 실패 | WSL2 부팅 시 `mount -a`, 실패 시 alarm (로그) |
|
||
| Redis 다운 | docker restart unless-stopped + healthcheck. 다운 시 모든 worker idle (NAS는 응답 계속) |
|
||
| 키 노출 | 3-layer 차단 (IP 화이트리스트 + nginx + X-Internal-Key) |
|
||
|
||
## 14. 첫 plan 작성 대상
|
||
|
||
**옵션 A — Track A만 (사용자 선택 확정)**:
|
||
- SP-A1: web-ai 캐시 TTL 증가 (10분)
|
||
- SP-A2: NAS stock TTLCache (30분)
|
||
|
||
이 plan은 즉시 NAS CPU 70% 감소 효과 (V2 재시작 시). Track B는 별도 spec/plan으로 차후 진행.
|
||
|
||
차후 plan 작성 순서 권장:
|
||
1. **Plan-A (이번)** — SP-A1 + SP-A2
|
||
2. **Plan-B-Base** — SP-1 + SP-2
|
||
3. **Plan-B-Insta** — SP-3 + SP-4 (1순위 패턴 정착)
|
||
4. **Plan-B-Music** — SP-5 + SP-6
|
||
5. **Plan-B-Video** — SP-7 + SP-8
|
||
6. **Plan-B-Infra** — SP-9 + SP-10
|
||
|
||
## 15. 참고
|
||
|
||
- 박재오 7결정 통합: `Obsidian Vault/raw/2026-05-18-Windows-NAS-아키텍처-7결정-통합.md`
|
||
- API 부하 해결: `Obsidian Vault/raw/2026-05-18-NAS-Window-AI-API-부하-해결방안.md`
|
||
- 역할 분담 최적화: `Obsidian Vault/raw/2026-05-18-NAS-Windows-역할-분담-최적화.md`
|
||
- web-backend CHECK_POINT.md (즉시·중기·장기 + 7결정 매핑)
|
||
- web-ai CHECK_POINT.md (Phase 진행도)
|
||
- 기존 인증 패턴: 메모리 `reference_webai_auth_pattern.md`
|