Commit Graph

13 Commits

Author SHA1 Message Date
6136eb83e9 fix(comment-cards): 일시적 YouTube 403/5xx 자동 재시도
commentThreads가 order=relevance에서 간헐적으로 403 forbidden을 반환하는 문제 대응.
generic 403/rateLimit/5xx는 백오프 후 최대 2회 재시도, commentsDisabled/quotaExceeded는
재시도 없이 즉시 안내. 재시도 성공/비재시도 단위 테스트 추가.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 18:13:19 +09:00
6f59554b6d fix(comment-cards): YouTube API 오류를 reason별로 구분 처리
403을 무조건 '댓글 비활성화'로 표시하던 문제 수정. error.errors[0].reason을 파싱해
commentsDisabled/quotaExceeded/기타(keyInvalid 등)를 각각 안내하고, 비-403 오류가
opaque 500으로 새던 것도 의미있는 메시지로 노출. 단위 테스트 추가.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:54:50 +09:00
39e2b42995 feat(comment-cards): 유튜브 videoId 파서 추가
watch/youtu.be/shorts/embed/순수ID 추출, 단위 테스트 포함

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:04:15 +09:00
507ae7c760 feat(discover): RecommendedChannel 엔티티+Repo, DiscoveryRanker 랭킹로직(TDD)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:41:52 +09:00
d16f23a4c0 feat(rework): AI 자막 패널에 원문+번역 비교 표시
화면(Gemini)·음성 패널에서 원문과 한국어 번역을 함께 보여줘 비교 가능하게 함.
- ScriptResponseDto.Segment: original 필드 추가(NON_NULL)
- Gemini 프롬프트/스키마: original(화면 원문) + text(한국어 번역) 둘 다 요청,
  text 는 반드시 한국어(영어 방지) 명시
- rework.html: 각 줄에 한국어(위)+원문(아래 회색) 표시, 음성 패널은 Whisper 원문 사용

검증: 470 화면자막 → 원문 일본어 + 한국어 번역 + 진행형 타임스탬프 정상.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:12:31 +09:00
447645c19a feat(rework): AI 자막 2종(음성 Whisper / 화면 Gemini)→한국어, 좌우 분할 + SRT
재가공 화면에 'AI 자막(음성/화면)' 카드 추가. '생성' 한 번에:
- 왼쪽(음성): 저장된 Whisper 세그먼트(정밀 타임스탬프) → LibreTranslate 한국어
- 오른쪽(화면): 유튜브 URL → Gemini가 화면 박힌 자막 추출+한국어 번역
각 패널을 [00:00] 한국어 리스트로 표시, 각각 SRT 다운로드(audio_ko/screen_ko).

- GeminiSubtitleService 신규: youtube URL을 generativelanguage API에 전송,
  responseMimeType=json 구조화 응답 파싱. buildRequest/extractSegments/sanitizeApiKey 순수+단위테스트
- sanitizeApiKey: 잘못 붙은 선행 '='·공백 정리(env var 오타 방어)
- geminiRestTemplate 빈(5분), POST /{id}/gemini-subtitles, CurationService.geminiScreenSubtitles
- 설정 gemini.api-key/model(gemini-2.5-flash), rework.html 분할 카드+JS
- 음성 한국어는 기존 translate(format=segments) 재사용

검증: 470 화면자막 → 한국어 27세그먼트 30초 추출(스타일 일본어 자막도 정확).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:04:24 +09:00
198334c6f8 feat(rework): 타임라인 [00:00] 형식 번역 옵션 추가
'[00:00] 번역' 버튼 추가 — 세그먼트별로 번역해 '[mm:ss] 한국어' 줄로 재작성 칸에 내린다.
LibreTranslate의 q 배열 일괄번역(1회 호출, 입력순서 1:1 정렬)을 사용.
기존 '번역→재작성'(평문 흐름)은 그대로 유지(format=plain|timeline).

- TranslateService.translateBatch + buildBatchRequestBody/extractTranslatedTexts(+단위테스트)
- translateScript(id,target,format): timeline 분기, 세그먼트 시각 [mm:ss] 머리표시
- 컨트롤러 format 파라미터, rework.html '[00:00] 번역' 버튼

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:10:05 +09:00
77e77894c9 feat(rework): 원본 스크립트 번역 → 재작성(내 버전) 버튼 추가
자가호스팅 LibreTranslate(h-etc2.tolag.shop) 연동. 재가공 화면 '번역→재작성'
버튼으로 원본 전사 스크립트를 한국어로 번역해 재작성 칸 초안으로 채운다.
소스 언어는 전사 언어로 자동(없으면 auto), 타깃 기본 ko.

- TranslateService 신규: POST /translate {q,source,target} → translatedText
  (요청 조립/응답 파싱 순수 메서드 + 단위테스트)
- POST /{id}/translate 엔드포인트, CurationService.translateScript
- rework.html: '번역→재작성' 버튼 + translateToEditor()
- 설정 translate.base-url(기본값 有)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:41:23 +09:00
986dcffd55 feat(rework): 받은 원본 삭제 + 영상 삭제 시 캐시 자동정리
1) 재가공 화면에 '받은 원본 삭제' 버튼 추가(캐시 보유 시에만 노출).
   GET/DELETE /{id}/download 로 캐시 상태 조회·삭제(자막/세그먼트는 유지).
   진입 시 캐시 상태를 조회해 삭제버튼 노출 + 렌더 캐시 사용 여부를 갱신.
2) 수집함 영상 삭제 시 downloads/{id}.mp4 도 함께 삭제(고아 파일 방지).

- VideoDownloadService.deleteCache(id) + 단위테스트(@TempDir)
- CurationService: downloadStatus/deleteDownloadCache, delete() 캐시 정리

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:10:02 +09:00
26b8be1ff1 feat(rework): yt-dlp 원본 다운로드 → 전사·렌더 파이프라인 자동연결
재가공 화면 '원본 다운로드' 버튼으로 저장된 videoId를 yt-dlp(로컬 ProcessBuilder)로
받아 서버에 캐시하고, 그 파일을 기존 Python /transcribe·/render 에 그대로 투입한다.
수동 다운로드+업로드 단계를 전사·렌더 양쪽에서 제거.

- VideoDownloadService 신규: 인자 리스트 ProcessBuilder(셸 미사용), videoId 검증, 캐시 조회
- ChannelService: 전사·렌더를 Resource 기반 공통 메서드로 추출(업로드/캐시 공유)
- 컨트롤러: POST /{id}/download(다운로드+전사), /render 의 file 을 선택값으로(없으면 캐시)
- rework.html: '원본 다운로드' 버튼 + downloadOriginal(), 렌더가 서버 캐시 재사용
- 설정 ytdlp.*/download.dir(기본값 有), downloads/ gitignore
- buildCommand/videoId 검증 단위테스트(src/test 신규)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:58:50 +09:00
8178d45209 feat(rework): speech-gap trimming + render, language override
Phase 3: remove "no-talk" gaps (Whisper-segment based, not audio silencedetect
which finds nothing under background music) and render a trimmed (+speed) video
via ffmpeg, with subtitles remapped to match.

- KeepIntervalPlanner + TimelineRemapper (pure, unit-tested): keep/remove plan
  from segments (pad/minGap) and timestamp remap f(t)=t-removedBefore(t)
- GET /{id}/trim-plan (preview: keep/remove/remapped segments/kept duration)
- POST /{id}/render (multipart: file,pad,minGap,speed) -> proxy Python /render
  (ffmpeg trim/atrim+concat+atempo) -> mp4 download; ffmpeg graph validated locally
- rework.html: export panel (speed + speech-gap trim preview + SRT/video export),
  client-side SRT from working segments, language selector (auto/ko/en/zh/ja)
- transcribeFromFile forwards optional language (Whisper auto-detect misfired -> zh)

Spec updated with the audio-silence -> speech-gap design correction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 16:53:32 +09:00
b9aa04d4a3 feat(rework): synced subtitle extraction via file upload + Whisper, SRT export
Phase 1 of subtitle timeline studio. Upload a video in the rework editor →
proxy to Python /transcribe (faster-whisper) → store segments → render a
playback-synced segment list and export CapCut-importable SRT.

- ScriptSegment + SrtFormatter (pure, unit-tested) with speed rescaling
- ChannelVideoScript.segmentsJson column (ddl-auto adds it)
- ChannelService.transcribeFromFile + getSegments/parseSegments
- POST /{id}/transcribe (multipart), GET /{id}/script.srt?speed=, /script now returns segments
- rework.html: upload button, local <video>, segment list, SRT export + speed
- multipart 200MB limit; python.base-url config (PYTHON_BASE_URL)
- repo: findFirstByVideoIdOrderByIdDesc/findAllByVideoId (dedup-safe lookups)

Spec: docs/superpowers/specs/2026-06-12-subtitle-timeline-studio-design.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 16:23:55 +09:00
2cdae03998 test: add unit test infrastructure and core-logic tests
First tests in the project (src/test was absent). Add testRuntimeOnly
junit-platform-launcher (required by Gradle 9 to load the JUnit Platform),
then cover:
- VideoMetrics: duration parse, isShorts boundary, viewsPerHour clamp/
  division, viewsPerSubRatio rounding & zero-guards (9 cases, pure)
- DashboardService.summary(): composes each domain service under the
  right key (Mockito)
- PublishService.dashboardSummary(): status counts + recent capped at 5

12 tests, all green. No Spring context / DB needed — fast.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 13:37:52 +09:00