h-lab/docs/superpowers/specs/2026-06-24-ytdlp-download-integration-design.md
hehihoho3@gmail.com a177b0c987 docs(spec): yt-dlp 다운로드 → 재가공 파이프라인 자동연결 설계 추가
재가공 화면 '원본 다운로드' 버튼으로 yt-dlp 로컬 실행 → 서버 캐시 →
기존 Python 전사/렌더 파이프라인 재사용. 전사·렌더 양쪽에서 수동 업로드 제거.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:29:05 +09:00

134 lines
7.3 KiB
Markdown

# yt-dlp 다운로드 → 재가공 파이프라인 자동연결 설계
- 날짜: 2026-06-24
- 상태: 설계 확정(구현 대기)
- 관련: `2026-06-12-subtitle-timeline-studio-design.md`(자막 타임라인 스튜디오)
## 1. 목적
재가공(`/rework/{id}`) 화면에서 **"원본 다운로드" 버튼 한 번**으로
`yt-dlp`가 저장된 `videoId`로 영상을 받아 서버에 캐시하고, 그 파일을 그대로
기존 Python 파이프라인(`/transcribe`, `/render`)에 흘려보낸다. 결과적으로
지금까지 필요했던 **수동 다운로드 + 수동 업로드** 단계를 전사·렌더 양쪽에서 모두 제거한다.
## 2. 배경 / 현재 구조
- 백엔드: Spring Boot 3.4 / Java 21, Thymeleaf SSR. 로컬 실행(`bootRun`, 포트 8088, Dockerfile 없음).
- 영상 처리는 원격 Python 마이크로서비스(`PYTHON_BASE_URL`, 기본 `http://h-python.tolag.shop`)의
`/transcribe`(faster-whisper), `/render`(ffmpeg)를 `pythonRestTemplate`로 호출.
- 현재 흐름: 사용자가 **유튜브에서 수동 다운로드 → 브라우저에서 파일 업로드**.
업로드한 File을 JS변수 `UPLOADED_FILE`에 담아 전사·렌더가 클라이언트에서 재사용.
렌더 버튼은 `UPLOADED_FILE`이 없으면 "먼저 업로드하세요"로 막힘.
- 로컬 설치 확인: `yt-dlp 2026.03.17`(`C:\...\Python313\Scripts\yt-dlp`), `ffmpeg 8.0.1`(`D:\utils\...`).
- `ChannelVideo`: 전체 URL 없이 `videoId`만 저장(+`is_shorts` 플래그). URL은 `https://www.youtube.com/watch?v={videoId}`로 구성.
### 실행 위치 결정
yt-dlp는 **Spring이 로컬에서 직접 실행(ProcessBuilder)**, 받은 mp4를 기존처럼 원격 Python으로 전송한다.
- 근거: yt-dlp가 로컬 설치됨, h-lab도 로컬 실행, 유튜브는 데이터센터 IP를 자주 차단하므로 가정용 IP에서 받는 게 안정적.
## 3. 사용자 흐름
```
[재가공 /rework/{id}] "원본 다운로드" 클릭
│ POST /api/curation/videos/{id}/download
[Spring] videoId 검증 → yt-dlp(ProcessBuilder) → {download.dir}/{id}.mp4 캐시 저장
[Spring] 캐시 파일 → 원격 Python /transcribe → 세그먼트 저장 + hasScript=true
[화면] 세그먼트 표시 + 서버파일 보유 플래그 set
"말없는구간 제거·렌더" → 같은 캐시 파일 재사용(업로드 불필요)
```
## 4. 컴포넌트
### 4.1 `VideoDownloadService` (신규, `domain/channel`)
서버사이드 yt-dlp 실행과 다운로드 캐시를 담당.
- `File download(Long channelVideoId)`
1. `ChannelVideo` 조회, `videoId` 형식 검증: 정규식 `^[A-Za-z0-9_-]{11}$`. 불일치 시 `IllegalArgumentException`.
2. 출력 경로 `{download.dir}/{channelVideoId}.mp4` 계산(폴더 없으면 생성).
3. `buildCommand(videoId, outPath)`로 만든 인자 리스트를 ProcessBuilder에 전달(**shell 미사용**).
4. 프로세스 실행, `timeout-seconds` 초과 시 강제종료 + 예외. 비정상 종료 시 stderr 일부를 메시지에 담아 예외.
5. 결과 File 반환.
- `Optional<File> cachedFile(Long id)` / `boolean isCached(Long id)`: 캐시 존재·반환(렌더 재사용용).
- `List<String> buildCommand(String videoId, Path outPath)`**순수 메서드(프로세스 실행 없음, 단위테스트 대상)**:
```
{ytdlp.bin}
--no-playlist
--force-overwrites
-f bv*[height<={max-height}]+ba/b[height<={max-height}]/b
--merge-output-format mp4
[--ffmpeg-location {ffmpeg-location}] # 값이 있을 때만
-o {outPath}
https://www.youtube.com/watch?v={videoId}
```
### 4.2 `ChannelService` 리팩터 — Resource 기반 공통화
전사·렌더 핵심 로직을 `Resource` 입력으로 추출해 MultipartFile(기존)과 File(캐시)이 공유.
- 내부 공통: `doTranscribe(ChannelVideo, Resource, language)`, `doRender(ChannelVideo, Resource, pad, minGap, speed)`.
- 기존 `transcribeFromFile(id, MultipartFile, lang)` / `renderTrimmed(id, MultipartFile, ...)`
Resource 어댑터를 거쳐 공통 메서드 호출하도록 유지(외부 동작 불변).
- 신규: `transcribeFromCached(id, lang)`, `renderTrimmedFromCached(id, pad, minGap, speed)`
`VideoDownloadService.cachedFile(id)``FileSystemResource` 생성 후 공통 메서드 호출.
캐시 없으면 `IllegalArgumentException("원본 영상이 없습니다. 먼저 '원본 다운로드'를 실행하세요.")`.
- `toFileResource(File)` 헬퍼 추가(파일명 보존). 기존 `toFileResource(MultipartFile)` 유지.
### 4.3 `ChannelVideoCurationController` 엔드포인트
- `POST /{id}/download` — yt-dlp 다운로드 + 캐시 전사를 한 호출로 수행.
응답: `{ downloaded:true, sizeBytes, ...전사결과(hasScript, language, duration, transcript, segments) }`.
(서비스 위임: `VideoDownloadService.download``ChannelService.transcribeFromCached`.)
- 기존 `POST /{id}/render``file`을 **선택값**으로 변경:
- `file` 있으면 기존대로(업로드 렌더).
- `file` 없으면 `renderTrimmedFromCached` 사용(캐시 없으면 400, 메시지 노출).
- `CurationService`는 컨트롤러↔ChannelService 사이 기존 위임 패턴을 따라 대응 메서드 추가.
### 4.4 `rework.html` UI
- "영상 업로드·전사" 버튼 옆에 **"원본 다운로드"** 버튼 추가(아이콘 `download`).
- 클릭 → 스피너/상태표시 → `POST /{id}/download`.
성공 시 기존 전사 성공 핸들러 재사용(세그먼트 렌더), 전역 플래그 `HAS_SERVER_FILE=true` set.
- `renderVideo()` 수정: `UPLOADED_FILE` 없어도 `HAS_SERVER_FILE`이면 진행.
업로드 파일 있으면 multipart로 전송, 없으면 `file` 없이 `/render` 호출(서버 캐시 사용).
## 5. 설정 (`application.yml`, 모두 기본값 有)
```yaml
ytdlp:
bin: ${YTDLP_BIN:yt-dlp}
ffmpeg-location: ${FFMPEG_LOCATION:} # 비우면 PATH 사용
max-height: ${YTDLP_MAX_HEIGHT:1080}
timeout-seconds: ${YTDLP_TIMEOUT_SECONDS:600}
download:
dir: ${DOWNLOAD_DIR:downloads} # 캐시 폴더(상대경로 = 작업디렉토리 기준)
```
`downloads/``.gitignore`에 추가(영상 캐시 커밋 방지).
## 6. 에러 처리
- videoId 형식 오류 → 400(`IllegalArgumentException` → `GlobalExceptionHandler`).
- yt-dlp 실행 실패/타임아웃 → 500, 로그에 stderr 요약. 화면에 "다운로드 실패" 상태 표시.
- 렌더 시 캐시·세그먼트 없음 → 400, 사용자에게 다음 행동 안내 메시지.
- 외부로 시크릿 노출 없음(여기선 API 키 불필요). 명령은 인자 리스트 전달로 셸 인젝션 차단.
## 7. 동기/비동기
MVP는 기존 전사·렌더와 동일하게 **동기 호출 + 프론트 스피너**. `timeout-seconds` 가드만 둔다.
비동기 작업큐/진행률 %는 범위 외(추후).
## 8. 테스트 (`src/test` 신규 생성)
프로세스 실행 없이 검증 가능한 단위 위주:
- `buildCommand`: 인자 순서/포맷 문자열/ffmpeg-location 유무 분기/URL 구성.
- videoId 검증: 정상 11자 통과, 비정상(길이/문자) 거부.
- 렌더 폴백 분기: `file` 없음 + 캐시 없음 → 예외, 캐시 있음 → 캐시 경로 사용(서비스 레벨, Python 호출은 모킹).
## 9. 범위에서 제외 (YAGNI)
- 비동기 작업큐 / 진행률 표시
- 임의 URL 붙여넣기(이번엔 저장된 videoId 버튼만)
- 캐시 자동정리·만료 정책
- 다운로드 화질/포맷 사용자 선택 UI(설정값 고정)