Commit Graph

33 Commits

Author SHA1 Message Date
e5205d9e9c feat: 내 채널 분석 페이지 분리 — OWN 역할 + 소재별 성과 랭킹
내 채널을 등록하면 그 영상이 수집함·칸반에 소재로 섞여 들어갔다. 이미 발행한
영상은 소재가 아니라 성과이므로 아예 분리한다.

- ChannelRole.OWN 추가. OWN 채널 영상은 source=OWN 으로 저장되고, 수집함·칸반·
  발굴 쿼리는 CHANNEL·SEARCH 화이트리스트라 자동으로 격리된다(쿼리 수정 불필요).
  /channels 목록에도 안 뜨고 피드에도 안 섞인다.
- /my-channel 신설: 성과 요약(구독·총조회·영상평균·최근30일), 성장 추이 차트,
  내 영상 목록.
- 핵심은 소재별 성과 랭킹(TopicPerformance). 해시태그별 평균 조회수를 채널 평균
  대비 배수로 보여준다. 실제 데이터로 유퀴즈 2.43x, 워크맨 0.30x 처럼 갈려서
  피드에서 무엇을 자를지 판단하는 근거가 된다. 1편짜리 우연은 2편 미만 컷으로 제외.
- 자동 수집·스냅샷 대상에 OWN 포함(findTrackedChannels). 해시태그 역분석의
  원천도 MY → OWN 으로 바꿨다.

테스트 8건 추가(TopicPerformance 6, ChannelRole 2) — 총 84건 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 18:28:31 +09:00
32540d1ceb fix: 내 채널이 없을 때 후보 자동 발굴이 조용히 0건으로 끝나던 문제
내 채널을 등록하면 그 영상이 수집함·칸반에 소재로 섞여 들어가서 등록을
취소했다. 그러면 해시태그 역분석의 원천이 사라지는데, 기존 코드는 키워드
0개로 검색 루프를 그냥 빠져나와 "발굴했지만 아무것도 못 찾음"처럼 보였다.

- 역분석할 해시태그가 없으면 이유를 명시한 오류를 던진다.
- 피드 화면의 '후보 자동 발굴'은 먼저 감지된 키워드를 보여주고 확인을 받는다.
  키워드가 없으면 프로그램 이름을 직접 입력받아 검색한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 09:41:59 +09:00
2574174d22 fix: 시드 자동 발굴이 엉뚱한 채널을 잡던 문제 — 채널검색→영상검색 집계, 이중 인코딩 제거
실제 내 채널(clipOut-log) 데이터로 돌려보니 구독자 1~2명짜리 채널만
잡혀서 원인을 둘 찾았다.

1) type=channel 검색은 채널 '이름'을 매칭한다. 프로그램 클립은 "유퀴즈"라는
   이름의 채널이 아니라 tvN D ENT·뜬뜬 같은 제작사 채널에 올라오므로
   이름으로는 절대 찾을 수 없다. type=video 로 25건을 뽑아 채널별로 집계하고
   3건 이상 등장한 채널만 채택하도록 바꿨다.

2) UriComponentsBuilder.encode().toUriString() 을 RestTemplate 에 String 으로
   넘기면 URI 템플릿으로 보고 한 번 더 인코딩한다. 한글 키워드가
   %25EC.. 로 깨져 검색어가 무의미해졌다. URI 오버로드로 교체.
   ChannelService.saveChannelFromUrl 도 한글 @핸들에서 같은 문제가 생기므로
   함께 고쳤다. (YoutubeSearchService 등 기존 코드는 이미 올바르게 쓰고 있었다)

수정 후 8개 키워드 전부 정확한 공식채널을 찾는다:
유퀴즈→tvN D ENT/디글 클래식, 워크맨→워크맨-Workman, 살롱드립→TEO 테오,
짠한형 신동엽·미미미누·핫이슈지·입만열면→각 공식채널.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 09:04:16 +09:00
592ed12c2b feat: 소재 발굴 피드 — 소스 롱폼/경쟁 쇼츠 최신순 2탭
쇼츠 클립 채널의 소재를 최신순으로 발굴하는 /feed 화면과 수집 파이프라인.

- 소재 원본 탭: 웹예능 공식채널의 신규 롱폼(durationSec > 65)만 수집.
  아직 아무도 안 자른 구간을 선점하는 용도라 공식계정 쇼츠는 제외한다.
- 경쟁 쇼츠 탭: 예능짤 채널의 신규 쇼츠. 제목·썸네일 벤치마킹용이라
  재가공 대신 원본 열기 액션을 준다.
- Channel.role(MY/SOURCE/RIVAL) 컬럼 하나로 역할을 구분하고, 기존
  uploads 플레이리스트 동기화를 그대로 재사용한다. 채널당 2 units라
  search.list(100 units) 대비 쿼터가 거의 들지 않는다.
- 3시간 주기 수집(FeedCollectionService). 소재 선점은 업로드 직후가
  승부라 기존 일 1회 채널 수집으로는 늦다.
- 수집함/발굴/떡상 후보 쿼리는 source 미지정 시 CHANNEL·SEARCH만 보도록
  좁혀, 피드 영상이 기존 화면을 덮지 않게 격리했다.
- 시드는 자동 후보 → 수동 승인. 내 채널 해시태그를 역분석해(HashtagExtractor)
  공식채널 후보를 추천 목록에 쌓고, 승인 시 SOURCE/RIVAL로 등록한다.
- 연속 3회 수집 실패한 시드는 자동 스킵해 쿼터 낭비를 막는다.
- role이 null인 기존 채널은 부팅 시 MY로 1회 백필(ddl-auto:update 특성).

UX: 기존 Editorial 디자인 시스템 유지. 골든타임(24h)·공식클립/풀에피·
떡상중·작업함 배지, 프로그램/길이/기간 필터, URL 상태 보존, 스켈레톤·
빈 상태·에러 복구 액션, 탭 키보드 이동, 이모지 대신 Lucide 아이콘.

테스트: HashtagExtractor·FeedBadges·ChannelRole 순수 로직 16건 추가(총 76건 통과).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:42:26 +09:00
aecbdb4753 feat: 발굴 포맷 옵션(롱폼/쇼츠) 추가 — 롱폼 기본
발굴 대상이 SHORTS 하드코딩이었으나, 롱폼 콘텐츠 대응을 위해 포맷을 선택 가능하게 함.

- ChannelDiscoveryService: runDiscovery(String format) 오버로드. format(SHORTS|LONG_FORM),
  null/미인식이면 설정 기본값(hlab.discovery.format, 기본 LONG_FORM) 사용. 검색 조건의
  하드코딩 SHORTS를 fmt로 교체(키워드 없는 검색·키워드 검색 양쪽).
- application.yml: hlab.discovery.format 추가(기본 LONG_FORM, DISCOVERY_FORMAT env로 오버라이드).
- RecommendedChannelController /run: format 쿼리파라미터 수용.
- recommend.html: '지금 발굴' 옆 롱폼/쇼츠 선택 드롭다운(롱폼 기본), 소제목 KR·롱폼 반영.

동일 파일에 있던 진행 중 기능(추천채널 대표영상 '영상 재가공' 버튼 + /{id}/rework 엔드포인트)도 함께 포함.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:32:28 +09:00
ab49e30a76 chore: 발굴 지역 인라인 기본값도 KR로 통일
application.yml(KR)이 이미 우선 적용되나, @Value 인라인 fallback이
KR,JP,US로 남아 혼동을 줘 KR로 맞춤.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:26:13 +09:00
9dc0f0248a feat: 발굴 예능 특화(키워드 시드) + 텔레그램 아침 추천 알림
예능 전환 2단계 — 소재 발굴을 예능 쪽으로 강화하고, 매일 아침 추천을 자동으로 받는다.

발굴 예능 특화
- ChannelDiscoveryService: 지역 인기 Shorts 발굴에 더해, 설정된 예능 키워드
  (hlab.discovery.keywords, 기본 "예능,방송,하이라이트")로 지역×키워드 Shorts를
  조회수순 검색해 예능 클립 소재 채널을 우선 발굴.
- 매 검색마다 쿼터 가드, 소진 시 전체 발굴 중단(라벨 break). 키워드 비면 기존 동작 유지.

텔레그램 아침 추천
- TelegramNotifier 신규(global/notify): bot-token/chat-id 미설정 시 no-op,
  발송 예외는 삼켜 호출측을 깨지 않음. @Qualifier("restTemplate") 주입.
- 매일 04:30 발굴 직후 NEW 추천채널 상위 N개를 한국어 HTML 다이제스트로 발송.
- 활성화: env TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID 설정.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:06:41 +09:00
d0faada002 perf: SQL 로깅 상시화 해제 + channel_videos 인덱스 추가
느려짐의 유력 원인 두 가지를 정리한다.
1) p6spy가 모든 SQL을 파라미터값 포함해 매번 로깅하던 것을 기본 OFF로.
   디버깅 시 P6SPY_LOGGING=true 로 켠다.
2) channel_videos에 인덱스가 하나도 없어 데이터가 늘면 풀스캔이었다.
   리포지토리의 실제 필터/정렬 컬럼에 인덱스 추가(ddl-auto가 자동 생성):
   video_id, channel_id, interest_status, source, category_id,
   published_at, views_per_sub_ratio

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:13:55 +09:00
84155a69f8 feat(channels): 국가별 탭(KR/JP/US/기타) + Channel.country 수집·백필
Channel 에 country 추가(YouTube snippet.country 저장), 추천 채널 region 으로 기존 채널
백필(POST /api/channels/backfill-country), /channels 에 국가 탭+카운트 필터.
선택 영상 보기 버튼 한글화.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:24:40 +09:00
fd18a8981a feat(discover): 제외 목록 보기 + 복원 기능
- 목록 API status 파라미터(NEW|EXCLUDED|REGISTERED), POST /{id}/restore(→NEW)
- recommend.html: 헤더 '제외 목록' 토글로 EXCLUDED 채널 보기, 카드에 '복원' 버튼
  (제외 보기에선 등록/제외 대신 복원+채널이동만), 지역 탭은 그대로 적용

검증: 제외→EXCLUDED 목록 노출→복원→NEW 복귀 end-to-end 확인.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:48:34 +09:00
08d50230c4 feat(discover): 추천 채널 지역(KR/JP/US) 탭 + 발견 지역 태깅
- region 을 채널 국가가 아닌 '검색 발견 지역(regionCode)'으로 태깅(UNKNOWN 해소)
- upsert: 같은 배율이면 갱신(지역 등 메타 최신화)
- recommend.html: 전체/KR/JP/US 탭으로 필터, 등록/제외 시 목록 갱신

검증: 재발굴 후 KR14·JP7·US1 로 분포, 탭 필터 동작.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:34:43 +09:00
e9b192eabb feat(discover): 추천 채널 전용 페이지 /recommend + 사이드바 + 발굴 조회수순 보강
- recommend.html(카드: 썸네일·구독자·배율·대표영상, 등록/제외) + WebController 라우트 + 사이드바 메뉴
- 발굴 검색 order=viewCount 강제(키워드 없을 때 최신순→조회수순) — 떡상 채널이 잡히게
  (YoutubeSearchCondition.order 추가, YoutubeSearchService 반영)

검증: /run → KR,JP,US 26개 떡상 채널 발굴(구독 76~7천, 배율 1500~9만x).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:49:42 +09:00
35944a8fd6 feat(discover): 발굴 일별 스케줄 연결 + 추천 채널 REST(목록/등록/제외/수동)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:44:27 +09:00
02fa5be25b feat(discover): ChannelDiscoveryService(지역 Shorts 검색→떡상 채널 upsert)+설정
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:43:24 +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
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
7ebee1caa3 revert(rework): 화면 자막 OCR 기능 제거
Tesseract 기반 OCR이 실제 쇼츠의 스타일 자막을 거의 못 읽어 실효성이 없고,
URL→Gemini 방향으로 전환하기로 해 h-lab의 OCR 연동을 제거한다.

- 컨트롤러 POST /{id}/ocr 제거
- CurationService.ocrScreenSubtitles 제거
- ChannelService.ocrFromCached/joinSegmentText 제거(persistScript는 전사가 계속 사용)
- rework.html '화면 자막 OCR' 버튼·crop/fps 입력·ocrScreen() 제거
- docs/python-service/ocr_video_endpoint.py(videocr 예시) 삭제

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:19:32 +09:00
bba81760a2 fix(rework): 원본 다운로드와 전사 분리 — 전사 지연/실패에 안 갇히게
기존엔 '원본 다운로드'가 다운로드+Whisper전사를 한 동기호출로 처리해, 전사 서버
(h-python)가 느리거나 타임아웃나면 화면이 '다운로드 중'에서 안 풀렸다.
다운로드(빠름)와 전사(분리)를 나눠, 다운로드는 즉시 끝나고 영상이 바로 뜨며
전사가 실패해도 다운로드 결과는 유지(화면자막 OCR/재시도 가능).

- POST /{id}/download: 다운로드만(캐시), 응답 {downloaded, sizeBytes}
- POST /{id}/transcribe-cached: 받은 원본 전사(분리)
- downloadOriginal(): 1)다운로드 즉시완료·영상표시 2)전사 별도(실패해도 다운로드 유지)

검증: /download 2.7초 완료. 전사는 분리 단계로 진행/실패 격리.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:19:18 +09:00
6a0a7294bb feat(rework): 화면 자막 OCR 에 자막영역(crop) 전달 추가
OCR 정확도·속도 향상을 위해 '자막영역 하단 N%' 입력을 추가, 플레이어 영상의 실제
해상도로 crop 픽셀(crop_x/y/width/height)을 환산해 /ocr_video 로 전달한다.
100%면 use_fullframe=true, 해상도 못 읽으면 생략(서버 기본 하단30%).

- ocrFromCached(file, formParams Map)로 일반화 — 서버 필드명 그대로 전달
- CurationService/Controller에 useFullframe·cropX/Y/Width/Height 파라미터 추가
- rework.html: 자막영역 % 입력 + ocrScreen()이 영상 해상도로 crop 계산

검증: sample_fps=3 + 하단30% crop 으로 2m28s 완료(프록시 600s 적용 후 504 해소),
crop 전달 정상 동작 확인.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:53:42 +09:00
f7ac96f1dd feat(rework): 화면 자막 OCR(영상 박힌 자막) 연동
받은 원본 영상을 Python /ocr_video(ffmpeg+Tesseract)로 보내 화면에 박힌 자막을
시간 싱크 세그먼트로 추출·저장한다. 음성 전사와 같은 자리(ChannelVideoScript)에
저장되어 스크립트 리스트·SRT·번역·한국어 SRT에 그대로 흐른다.

- ChannelService.ocrFromCached + persistScript 공통 추출(전사/OCR 공유), joinSegmentText
- CurationService.ocrScreenSubtitles(받은 원본 캐시 사용), POST /{id}/ocr
- rework.html '화면 자막 OCR' 버튼 + ocrScreen()
- sampleFps/confThreshold null이면 미전송 → 서버 기본값 사용

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:31:56 +09:00
e9ad800134 feat(rework): 한국어 SRT 내보내기 추가
세그먼트를 한국어로 일괄번역해 기존 타임라인(타임코드·배속·말없는구간 제거)에
그대로 끼워 SRT로 내보낸다. 원본 SRT와 별개로 '한국어 SRT' 버튼 추가.

- translateScript format=segments: 세그먼트와 1:1 정렬된 번역 배열 반환
- rework.html exportKoSrt(): texts 배열을 workingSegments에 매핑해 buildSrt로 생성
  (텍스트만 한국어로 교체, 타임코드/배속은 기존 로직 재사용)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:45:20 +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
ff9609c9d1 feat(rework): 받은 원본을 왼쪽 플레이어에 싣고 타임라인 클릭으로 seek
원본 다운로드/캐시 보유 시 왼쪽 플레이어를 YouTube 임베드 대신 받은 원본 mp4로
전환한다. 세그먼트(타임라인) 클릭 시 기존 seekTo/하이라이트가 그대로 동작해
해당 지점으로 이동·재생된다. (업로드본이 있으면 업로드본 우선)

- GET /{id}/download/file: 받은 원본 스트리밍(Resource 반환 → HTTP Range 206 자동 지원)
- CurationService.cachedDownloadFile(id)
- rework.html: showServerVideoInPlayer/showYoutubeInPlayer, updateCacheUI가 플레이어 전환,
  다운로드 직후/진입 시 캐시 있으면 자동 적용, 삭제 시 YouTube로 복귀

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:58: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
fa6342f97e fix(rework): long-op timeout, dedup query, clearer error, logging
Review follow-ups (safe fixes):
- pythonRestTemplate bean (10min read timeout) for /transcribe and /render so
  ffmpeg encoding / Whisper don't hit the shared 120s timeout; resolved by bean
  name so existing restTemplate injections are unaffected
- getScriptData: fetch the script row once instead of 3 separate queries
- renderTrimmed "no segments" now IllegalArgumentException (400 + visible message)
  instead of IllegalStateException swallowed as generic 500
- ProductionService: System.out.println -> log.info (3x)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:06:00 +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
152dc0bae4 fix(discover): cast nullable params so Postgres can infer type
The discover @Query used the `(:param is null or col >= :param)` idiom.
For the timestamp (publishedAfter) and numeric (minRatio) params, Postgres
threw "could not determine data type of parameter $1" because the bare
`$1 is null` placeholder is untyped — returning HTTP 500 and breaking the
whole /discover page. (The search() query survives because its nullable
params are only bigint/varchar, which Postgres can null-type.) Wrap the
two problematic params in cast(... as timestamp/big_decimal) in the
is-null check so the type is explicit. ORDER BY ... nulls last / fetch
first were never the problem.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:56:28 +09:00
2ec3915789 feat(dashboard): add full-set pipeline summary with single endpoint
Add GET /api/dashboard/summary aggregating pipeline status, category
distribution, publish summary, and outperformers in one call. Rewrite
dashboard.html with 5 KPI cards, pipeline funnel, publish status, and
category/source-format breakdowns (CSS bars, no chart lib).

Backend: ChannelVideoRepository counts (shorts/uncategorized),
PublishPackageRepository.countByStatus, pipelineStats shorts/longForm,
CategoryService.distribution, PublishService.dashboardSummary, new
DashboardService + DashboardApiController.

Fix: PublishService.list(null) hit UnsupportedOperationException because
findAll(Sort) uses Criteria, which rejects nullsLast precedence. Route the
no-status path through a @Query method so Sort is appended as HQL ORDER BY
(supports NULLS LAST). Also fixes the latent bug in /api/v1/publish all-list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 22:31:21 +09:00
hehih
f232d20f46 feat(discover): add discovery page for finding rework candidates
New /discover page surfaces collected ChannelVideos for picking rework
targets, filtered by period/min-ratio/source/shorts/unprocessed and
sorted by ratio/velocity/views/recency (EXCLUDED always hidden, null
ratios sorted last). Reuses existing curation endpoints for row actions
(status, bookmark).

- ChannelVideoRepository.discover() JPQL query
- ChannelVideoCurationService.discover() (defaults, limit cap, nullsLast)
- GET /api/v1/channel-videos/discover endpoint
- /discover page route + discover.html + sidebar link
Verified by clean compileJava.
2026-05-30 21:51:28 +09:00
hehih
da04dbe15c Baseline before video model consolidation 2026-05-30 18:56:21 +09:00