File-upload + local faster-whisper synced transcription, segment editor, SRT export, with speed/silence-removal phases. N150 CPU, small int8 model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.1 KiB
8.1 KiB
자막 타임라인 스튜디오 — 설계 (Subtitle Timeline Studio)
작성일: 2026-06-12
대상: 재가공 에디터(/rework/{id})의 스크립트 추출 정교화
1. 목표 (사용자 의도)
재가공 에디터에서 영상의 텍스트를 타임스탬프 동기로 추출하고, CapCut으로 가져가 편집할 수 있게 한다. 구체적으로:
- 영상에 있는 텍스트를 (후처리·수정 없이) 그대로, 영상과 싱크가 맞게 추출
- CapCut 화면처럼 자막을 보여주기
- 파일로 내보내(SRT) CapCut에 import해서 수정 가능
- 전체 배속(예: 1.2x) 적용 시 자막 타임스탬프도 같이 배속 보정
- 무음 구간 제거 시 자막 타임스탬프도 같이 보정
2. 핵심 제약과 결정
- mp4 파일 자체에는 텍스트가 없다 → 텍스트 확보는 (a) YouTube 자막 읽기(URL 필요, 자막 있을 때만) 또는 (b) 음성인식(Whisper) 중 하나. 사용자는 파일 업로드 기반을 원함 → (b) Whisper 채택.
- 비용 0 요구 → API가 아닌 로컬
faster-whisper사용. - 서버 사양: Intel N150(4코어), RAM 15GB, GPU 없음, Docker 구동. Java(
springboot/)와 Python(python/)이 동일 우분투 호스트에 공존. - 타겟이 Shorts(≤60초) → N150 CPU +
small(int8) 모델로 1개당 약 20~60초 전사. 인터랙티브하게 수용 가능. - 무음 제거(Phase 3)는 실제 오디오가 필요 → 업로드한 파일을 서버에서 ffmpeg로 처리.
3. 역할 분담
연산은 우분투(Python)에 집중, Java는 오케스트레이션·저장·내보내기.
- h-python (
python/app/main.py, Docker): 무거운 작업.- 신설
POST /transcribe— 업로드된 영상 파일 →faster-whisper→ 세그먼트 반환. - 기존
POST /transcript(URL 자막, 평문) 은 그대로 유지. - (Phase 3)
POST /silence— ffmpegsilencedetect로 무음 구간 반환.
- 신설
- Java (이 저장소, Spring Boot): 에디터 UI + 오케스트레이션.
- 파일 업로드 수신 → Python
/transcribe호출 → 세그먼트를 DB 저장. - SRT 생성·다운로드(순수 계산, 배속 파라미터 반영).
- 파일 업로드 수신 → Python
- 프론트(
rework.html): 업로드 영상 로컬 재생 + 세그먼트 리스트(재생 동기) + SRT 내보내기.
4. 데이터 모델
세그먼트 단위 = {start: 초, end: 초, text: 문자열}.
ChannelVideoScript에 컬럼 추가:segments_json TEXT— 세그먼트 배열 JSON 직렬화. (ddl-auto:update가 자동 생성)- 기존
transcript(평문, 세그먼트 텍스트를 공백 조인) 유지 → 기존 평문 UI 폴백. - 기존
language유지.
ScriptResponseDto에List<Segment> segments추가 (Segment{double start; double end; String text;}).
5. Phase 1 — 동기 추출 + 세그먼트 에디터 + SRT 내보내기 (코어)
5.1 h-python 변경 (사용자가 적용)
python/app/requirements.txt 에 추가:
faster-whisper
python/Dockerfile — 오디오 디코드/Phase 3 대비 ffmpeg 설치(베이스가 slim일 때):
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
모델 캐시 영속화 — python/docker-compose.yml 서비스에 볼륨 추가(재빌드 시 모델 재다운로드 방지):
environment:
- HF_HOME=/models
volumes:
- ./models:/models
python/app/main.py 에 추가(기존 /transcript는 유지):
import os, tempfile
from fastapi import UploadFile, File, Form
from faster_whisper import WhisperModel
# 모델 1회 로드(지연 싱글턴). N150 CPU → small/int8.
_model = None
def get_model():
global _model
if _model is None:
_model = WhisperModel(
os.getenv("WHISPER_MODEL", "small"),
device="cpu",
compute_type="int8",
)
return _model
@app.post("/transcribe")
async def transcribe(file: UploadFile = File(...),
language: str | None = Form(None)):
# 업로드 파일을 임시 저장
suffix = os.path.splitext(file.filename or "")[1] or ".mp4"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
tmp.write(await file.read())
path = tmp.name
try:
segments, info = get_model().transcribe(
path,
language=language, # None=자동 감지
vad_filter=True, # 무음 기반 구간화로 타임스탬프 품질↑
beam_size=5,
)
seg_list, texts = [], []
for s in segments: # 제너레이터 → 순회 시 실제 연산
seg_list.append({
"start": round(s.start, 3),
"end": round(s.end, 3),
"text": s.text.strip(),
})
texts.append(s.text.strip())
return {
"language": info.language,
"duration": round(info.duration, 3),
"segments": seg_list,
"transcript": " ".join(texts),
}
finally:
os.remove(path)
동시 요청은 N150에서 CPU 경합이 크므로 1인 사용 전제(직렬 처리). 필요 시 향후 락/큐 추가.
5.2 Java 변경 (이 저장소)
ScriptResponseDto:segments필드 추가 +Segment중첩 DTO.ChannelVideoScript:segmentsJson컬럼 추가(+getter/setter).ChannelService:- 신설
transcribeFromFile(Long channelVideoId, MultipartFile file)→ Python/transcribe멀티파트 호출 → 세그먼트 JSON 저장 +transcript평문 저장 +hasScript=true. 재추출 시 기존 1건 유지(현행 정책 동일). - SRT 생성 유틸
buildSrt(List<Segment>, double speed)(순수 함수).
- 신설
- 컨트롤러(
ChannelVideoCurationController영역):POST /api/v1/channel-videos/{id}/transcribe(multipartfile) → 세그먼트 응답.GET /api/v1/channel-videos/{id}/script.srt?speed=1.0→text/plain다운로드.- 기존
/{id}/script응답에segments포함.
5.3 프론트 변경 (rework.html)
- "원본 스크립트" 카드에:
- 영상 파일 업로드 입력. 선택 시
URL.createObjectURL로 좌측<video>재생(YouTube iframe 대체/병행) + 서버/transcribe호출. - 응답 세그먼트를 리스트로 렌더: 각 행
[mm:ss] 텍스트. 클릭 시video.currentTime = start.timeupdate로 현재 세그먼트 하이라이트. - SRT 내보내기 버튼(+ 배속 입력, 기본 1.0) →
script.srt?speed=다운로드. - 기존 평문 textarea/복사 기능은 폴백으로 유지.
- 영상 파일 업로드 입력. 선택 시
- 업로드 파일은 Phase 1에서는 클라이언트(object URL) 재생 + 서버 전사용으로만 사용하고 서버 영속화는 하지 않음(Phase 3에서 영속화).
5.4 Phase 1 완료 기준
- 영상 파일 업로드 → 세그먼트 추출 →
[mm:ss] 텍스트리스트 표시 → 재생 동기 하이라이트 → SRT 다운로드(CapCut import 가능)까지 동작.
6. Phase 2 — 배속
- SRT 내보내기에
speed적용: 모든start/end를/ speed. (예: 1.2x → 시간 0.833배) - 에디터에 배속 미리보기(선택). 저장 데이터는 원본 타임스탬프 유지, 내보내기 시점에만 변환.
7. Phase 3 — 무음 제거
- 업로드 영상을 서버에 영속화(또는 재업로드) → Python
/silence(ffmpegsilencedetect)로 무음 구간 목록 확보. - 무음 구간 제거 후 남는 구간을 이어붙이는 타임라인 재매핑 → 세그먼트 타임스탬프 보정 → SRT 반영.
- 배속과 조합 가능(무음 제거 후 배속).
8. 범위 밖(YAGNI)
- 가로 파형 타임라인 + 클립 드래그 같은 본격 CapCut형 UI(무거움). Phase 1은 "재생 동기 세그먼트 리스트"로 시작.
- CapCut 네이티브 draft(.json) 직접 생성. 표준 SRT import로 충분.
- 실제 영상 렌더링(배속/컷 적용된 mp4 출력)은 CapCut이 담당.
- 플랫폼 업로드 API 연동.
9. 열린 항목
- Python
Dockerfile베이스 이미지 확인(slim이면 ffmpeg apt 설치 필요, full이면 불필요). small한국어 품질이 부족하면large-v3-turbo로 상향(속도 trade-off) — 환경변수WHISPER_MODEL로 교체 가능하게 설계.