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>
169 lines
8.1 KiB
Markdown
169 lines
8.1 KiB
Markdown
# 자막 타임라인 스튜디오 — 설계 (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`로 교체 가능하게 설계.
|