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

7.3 KiB

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.downloadChannelService.transcribeFromCached.)
  • 기존 POST /{id}/renderfile선택값으로 변경:
    • 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, 모두 기본값 有)

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(IllegalArgumentExceptionGlobalExceptionHandler).
  • 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(설정값 고정)