docs: add subtitle timeline studio design spec

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>
This commit is contained in:
hehihoho3@gmail.com 2026-06-12 15:34:53 +09:00
parent aaf8901f13
commit fa7cec7f14

View File

@ -0,0 +1,168 @@
# 자막 타임라인 스튜디오 — 설계 (Subtitle Timeline Studio)
작성일: 2026-06-12
대상: 재가공 에디터(`/rework/{id}`)의 스크립트 추출 정교화
## 1. 목표 (사용자 의도)
재가공 에디터에서 영상의 텍스트를 **타임스탬프 동기**로 추출하고, CapCut으로 가져가 편집할 수 있게 한다. 구체적으로:
1. 영상에 있는 텍스트를 (후처리·수정 없이) **그대로**, 영상과 **싱크가 맞게** 추출
2. CapCut 화면처럼 자막을 보여주기
3. 파일로 내보내(SRT) CapCut에 import해서 수정 가능
4. 전체 배속(예: 1.2x) 적용 시 자막 타임스탬프도 같이 배속 보정
5. 무음 구간 제거 시 자막 타임스탬프도 같이 보정
## 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` — ffmpeg `silencedetect`로 무음 구간 반환.
- **Java (이 저장소, Spring Boot)**: 에디터 UI + 오케스트레이션.
- 파일 업로드 수신 → Python `/transcribe` 호출 → 세그먼트를 DB 저장.
- SRT 생성·다운로드(순수 계산, 배속 파라미터 반영).
- **프론트(`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일 때):
```dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
```
모델 캐시 영속화 — `python/docker-compose.yml` 서비스에 볼륨 추가(재빌드 시 모델 재다운로드 방지):
```yaml
environment:
- HF_HOME=/models
volumes:
- ./models:/models
```
`python/app/main.py` 에 추가(기존 `/transcript`는 유지):
```python
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` (multipart `file`) → 세그먼트 응답.
- `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`(ffmpeg `silencedetect`)로 무음 구간 목록 확보.
- 무음 구간 제거 후 남는 구간을 이어붙이는 타임라인 재매핑 → 세그먼트 타임스탬프 보정 → 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`로 교체 가능하게 설계.