재가공 화면 '원본 다운로드' 버튼으로 yt-dlp 로컬 실행 → 서버 캐시 → 기존 Python 전사/렌더 파이프라인 재사용. 전사·렌더 양쪽에서 수동 업로드 제거. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.3 KiB
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)ChannelVideo조회,videoId형식 검증: 정규식^[A-Za-z0-9_-]{11}$. 불일치 시IllegalArgumentException.- 출력 경로
{download.dir}/{channelVideoId}.mp4계산(폴더 없으면 생성). buildCommand(videoId, outPath)로 만든 인자 리스트를 ProcessBuilder에 전달(shell 미사용).- 프로세스 실행,
timeout-seconds초과 시 강제종료 + 예외. 비정상 종료 시 stderr 일부를 메시지에 담아 예외. - 결과 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=trueset. 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(
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(설정값 고정)