# 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 cachedFile(Long id)` / `boolean isCached(Long id)`: 캐시 존재·반환(렌더 재사용용). - `List 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(설정값 고정)