docs(spec): Gemini URL 자막(유튜브 URL→한국어 SRT) 설계 추가
'Gemini 자막' 버튼으로 유튜브 URL을 Gemini에 보내 화면/음성 자막을 추출+한국어 번역, [00:00] 한국어 세그먼트로 저장 → SRT 내보내기가 곧 한국어 SRT. 제거된 OCR의 대체. 무료 티어(하루 8시간) 사용, 타임스탬프는 ~1초 근사. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7ebee1caa3
commit
aa8dc17dd8
127
docs/superpowers/specs/2026-06-25-gemini-url-subtitles-design.md
Normal file
127
docs/superpowers/specs/2026-06-25-gemini-url-subtitles-design.md
Normal file
@ -0,0 +1,127 @@
|
||||
# Gemini URL 자막 (유튜브 URL → 한국어 자막/SRT) 설계
|
||||
|
||||
- 날짜: 2026-06-25
|
||||
- 상태: 설계 확정(구현 대기)
|
||||
- 관련: 제거된 화면 자막 OCR(`revert(rework): 화면 자막 OCR 기능 제거`)의 대체
|
||||
|
||||
## 1. 목적
|
||||
|
||||
재가공(`/rework/{id}`) 화면에서 **"Gemini 자막" 버튼 한 번**으로, 저장된 유튜브 URL을 Gemini에
|
||||
넘겨 **화면/음성 자막을 추출 + 한국어로 번역**해 `[00:00] 한국어` 세그먼트로 받아온다. 결과는 기존
|
||||
스크립트 자리에 저장되어 **"SRT 내보내기"가 곧 한국어 SRT**가 된다.
|
||||
|
||||
배경: Tesseract OCR은 실제 쇼츠의 스타일 자막을 거의 못 읽어 제거됨. Gemini는 화면에 박힌
|
||||
스타일 자막과 음성을 모두 읽고 번역까지 한 번에 처리한다(무료 티어로 충분).
|
||||
|
||||
## 2. 배경 / 현재 구조
|
||||
|
||||
- 백엔드: Spring Boot 3.4 / Java 21. YouTube Data API·LibreTranslate를 h-lab이 직접 REST 호출하는 패턴.
|
||||
- 스크립트는 `ChannelVideoScript`(videoId당 1건)에 `segments_json`+`transcript`+`language`로 저장.
|
||||
스크립트 리스트·SRT 내보내기·번역(LibreTranslate)이 이 세그먼트를 공유.
|
||||
- `ChannelVideo`는 `videoId`만 저장 → URL은 `https://www.youtube.com/watch?v={videoId}`.
|
||||
- 전사 저장 공통 메서드 `ChannelService.persistScript(video, id, dto)` 존재(전사가 사용) → 재사용.
|
||||
|
||||
### Gemini 무료 티어 확인 (공식)
|
||||
- 유튜브 URL 직접 입력 지원(preview, 무료). 무료 티어 **하루 8시간 분량**, 공개 영상만.
|
||||
- Gemini 2.5+ 는 요청당 최대 10개 영상.
|
||||
- 영상을 **초당 1프레임(1 FPS)** 으로 샘플링, 시각은 **MM:SS** → 타임스탬프는 **~1초 근사**(밀리초·프레임 정밀 X).
|
||||
- 무료 티어는 **데이터가 제품 개선(학습)에 사용됨**(프라이버시 주의).
|
||||
|
||||
## 3. 사용자 흐름
|
||||
|
||||
```
|
||||
[재가공 /rework/{id}] "Gemini 자막" 클릭
|
||||
│ POST /api/v1/channel-videos/{id}/gemini-subtitles
|
||||
▼
|
||||
[Spring] videoId → youtube watch URL
|
||||
│ Gemini REST 직접 호출(GEMINI_API_KEY)
|
||||
│ parts: [ {text: 프롬프트}, {file_data:{file_uri: youtubeURL}} ]
|
||||
│ generationConfig: responseMimeType=application/json (+ responseSchema)
|
||||
│ 프롬프트: "이 영상의 화면/음성 자막을 모두 추출해 한국어로 번역.
|
||||
│ JSON 배열 [{start, end, text}] 로만 출력(start/end=초, text=한국어)."
|
||||
▼
|
||||
[Spring] candidates[0].content.parts[0].text(JSON) 파싱 → List<Segment>
|
||||
▼
|
||||
[Spring] persistScript 로 저장(language=ko, transcript=세그먼트 이어붙임, 기존 스크립트 덮어씀)
|
||||
▼
|
||||
[화면] 세그먼트 리스트에 [00:00] 한국어 표시
|
||||
▼
|
||||
"SRT 내보내기" → 한국어 SRT (세그먼트가 이미 한국어)
|
||||
```
|
||||
|
||||
## 4. 컴포넌트
|
||||
|
||||
### 4.1 `GeminiSubtitleService` (신규, `com.hlab.yanalyst.service`)
|
||||
Gemini REST 호출과 응답 파싱 담당(YouTube API를 직접 호출하듯).
|
||||
|
||||
- `List<ScriptResponseDto.Segment> fetchKoreanSubtitles(String videoId)`
|
||||
1. URL 구성: `https://www.youtube.com/watch?v={videoId}`.
|
||||
2. 요청 바디 조립(`buildRequest`): contents.parts = [프롬프트 text, file_data(file_uri=URL)],
|
||||
generationConfig.responseMimeType=`application/json`,
|
||||
responseSchema = array of object{start:number, end:number, text:string}.
|
||||
3. `POST https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey}`
|
||||
(긴 처리시간 → `geminiRestTemplate` 또는 기존 `pythonRestTemplate`처럼 read timeout 길게).
|
||||
4. 응답에서 `candidates[0].content.parts[0].text` 추출 → JSON 파싱 → 세그먼트 리스트.
|
||||
- 순수 메서드(프로세스/HTTP 없음, **단위테스트 대상**):
|
||||
- `buildRequest(model, videoId, prompt)` → 요청 Map(구조 검증용)
|
||||
- `extractSegments(ObjectMapper, String responseJson)` → `List<Segment>`
|
||||
(candidates→parts→text 안의 JSON 배열 파싱, 코드펜스/잡텍스트 방어적으로 제거 후 파싱)
|
||||
- 설정 주입: `@Value gemini.api-key`, `gemini.model`.
|
||||
|
||||
### 4.2 `ChannelService` 또는 `CurationService` 연결
|
||||
- `CurationService.geminiSubtitles(Long videoId)`:
|
||||
- `find(videoId)` → `geminiSubtitleService.fetchKoreanSubtitles(videoId)` 로 세그먼트.
|
||||
- 빈 결과면 `IllegalArgumentException("Gemini가 자막을 찾지 못했습니다(영상에 자막/음성이 없거나 비공개).")`.
|
||||
- `ScriptResponseDto` 구성(language="ko", segments=결과, transcript=세그먼트 텍스트 이어붙임)
|
||||
→ `ChannelService.persistKoreanScript(...)`(또는 기존 저장 경로 재사용)로 저장.
|
||||
- 응답 Map: `{hasScript, language:"ko", transcript, segments}`(전사 응답과 동일 형식 → 프론트 재사용).
|
||||
- 저장은 전사와 동일하게 **기존 스크립트 덮어씀**(videoId당 1건 유지).
|
||||
- 참고: 세그먼트 텍스트 이어붙이기 헬퍼가 필요하면 작은 순수 메서드로 둔다.
|
||||
|
||||
### 4.3 컨트롤러
|
||||
- `POST /{id}/gemini-subtitles` → `curationService.geminiSubtitles(id)`.
|
||||
- Swagger: 유튜브 URL을 Gemini로 보내 화면/음성 자막을 한국어로 추출·저장. 응답 {hasScript, language, transcript, segments}.
|
||||
|
||||
### 4.4 `rework.html`
|
||||
- 툴바에 **"Gemini 자막"** 버튼(아이콘 `sparkles`/`wand`).
|
||||
- 클릭 → 상태표시("Gemini 분석 중… 영상 길이에 따라 시간 걸림") → `POST /{id}/gemini-subtitles`.
|
||||
성공 시 기존 전사 성공 핸들러처럼 `renderSegments(s.segments)` + transcript 채움.
|
||||
- 결과 세그먼트가 한국어 → 기존 **SRT 내보내기/세그먼트 클릭 seek/재작성 복사** 그대로 동작.
|
||||
|
||||
## 5. 설정 (`application.yml`)
|
||||
|
||||
```yaml
|
||||
gemini:
|
||||
api-key: ${GEMINI_API_KEY:} # AI Studio API 키(무료 티어)
|
||||
model: ${GEMINI_MODEL:gemini-2.5-flash} # 유튜브 URL 지원, 무료 티어
|
||||
```
|
||||
|
||||
`geminiRestTemplate` 빈(긴 read timeout, 예: 5분) 추가 — 또는 기존 `pythonRestTemplate` 재사용.
|
||||
|
||||
## 6. 에러 처리
|
||||
|
||||
- API 키 없음/401 → 명확한 메시지("GEMINI_API_KEY 미설정/유효하지 않음").
|
||||
- 비공개·자막없음·빈 결과 → 400 안내 메시지.
|
||||
- 무료 티어 한도 초과(429) → 메시지에 한도 안내.
|
||||
- JSON 파싱 실패(모델이 잡텍스트 반환) → 코드펜스 제거 후 재시도, 그래도 실패면 원문 일부를 담은 예외.
|
||||
- 시크릿(키)은 로그/응답에 노출하지 않음.
|
||||
|
||||
## 7. 출력/저장 정책
|
||||
|
||||
- Gemini 결과는 **한국어 세그먼트**로 스크립트에 저장(language=ko) → **"SRT 내보내기" = 한국어 SRT**.
|
||||
- 기존 음성전사/URL자막/LibreTranslate(번역→재작성·한국어SRT) 경로는 **그대로 공존**(원본 기반 흐름).
|
||||
- Gemini 경로는 "URL→한국어 자막 직통"의 별도 선택지. 마지막 실행 소스가 스크립트에 반영(덮어씀).
|
||||
|
||||
## 8. 테스트 (`src/test`)
|
||||
|
||||
순수 로직 위주(HTTP 없음):
|
||||
- `buildRequest`: parts에 프롬프트+file_data(youtube URL) 포함, responseMimeType=application/json.
|
||||
- `extractSegments`: 정상 응답(candidates→parts→text의 JSON 배열) 파싱, 코드펜스(```json) 감싼 경우 제거 후 파싱, 빈/깨진 응답 → 빈 리스트 또는 예외.
|
||||
- 실제 Gemini 호출은 라이브 검증(짧은 공개 쇼츠로 세그먼트/한국어/타임코드 확인).
|
||||
|
||||
## 9. 범위 제외 (YAGNI)
|
||||
|
||||
- 원본+한국어 동시 저장(이번엔 한국어만)
|
||||
- 비동기 작업큐/진행률
|
||||
- 무료 티어 사용량 모니터링·자동 백오프
|
||||
- 타임스탬프 정밀 보정(Gemini ~1초 근사 그대로 수용)
|
||||
Loading…
Reference in New Issue
Block a user