From fa7cec7f14bf70b0ba683618007f5ab47c19ca68 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Fri, 12 Jun 2026 15:34:53 +0900 Subject: [PATCH] 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) --- ...6-06-12-subtitle-timeline-studio-design.md | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-12-subtitle-timeline-studio-design.md diff --git a/docs/superpowers/specs/2026-06-12-subtitle-timeline-studio-design.md b/docs/superpowers/specs/2026-06-12-subtitle-timeline-studio-design.md new file mode 100644 index 0000000..96bed64 --- /dev/null +++ b/docs/superpowers/specs/2026-06-12-subtitle-timeline-studio-design.md @@ -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 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, 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`로 좌측 `