Channel 에 country 추가(YouTube snippet.country 저장), 추천 채널 region 으로 기존 채널
백필(POST /api/channels/backfill-country), /channels 에 국가 탭+카운트 필터.
선택 영상 보기 버튼 한글화.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
querySelector('i')가 lucide 치환 후 null→TypeError로 시각 토글이 첫 카드에서 멈춰
전체 선택이 안 되는 것처럼 보였음. querySelector('svg, i') + null 가드로 수정.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 목록 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>
- region 을 채널 국가가 아닌 '검색 발견 지역(regionCode)'으로 태깅(UNKNOWN 해소)
- upsert: 같은 배율이면 갱신(지역 등 메타 최신화)
- recommend.html: 전체/KR/JP/US 탭으로 필터, 등록/제외 시 목록 갱신
검증: 재발굴 후 KR14·JP7·US1 로 분포, 탭 필터 동작.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
URL자막 버튼 제거에 따라 안내문/placeholder의 'URL자막' 언급 삭제,
미사용 함수 extractScript/translateToEditor 제거(copyToEditor는 유지).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AI 자막(음성/화면) 기능이 번역을 대체해 중복되는 버튼 3개 삭제.
원본 다운로드·자막 추출(음성)·받은 원본 삭제·영상 업로드·에디터로 복사는 유지.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
화면(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>
재생 중 활성 세그먼트로 이동할 때 scrollIntoView가 페이지를 스크롤해 sticky 영상이
위아래로 튀던 현상 수정. 세그먼트 박스(#segmentList) 내부에서만 스크롤하도록 변경.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 이해하기 쉽게 모든 '전사' 문구를 '자막 추출'로 변경(메인 버튼은 '자막 추출(음성)')
- 좌측 영상 카드 position:sticky(top:1rem) → 스크롤해도 상단에 고정되어 항상 보임
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
말소리 없는 영상(음악·화면자막만)의 음성 패널을 빨강 오류가 아니라 회색 안내
('음성 없음 — 말소리가 없거나 전사 미실행')로 표시. renderAiPanel에 note 인자 추가.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- GeminiSubtitleService: 403(프로젝트 차단)/429(한도)/400(키 무효)을 한국어 메시지로
IllegalArgumentException throw → 'Internal Server Error' 대신 실제 사유 노출
- rework.html: 음성 패널 빈 경우 '말소리 없음(음악/화면자막만) 또는 전사 미실행'으로 정정
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
재가공 화면에 '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>
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>
저사양 서버(N150)에서 sample_fps=3은 프레임 과다로 10분 타임아웃 발생.
OCR 영역(crop %) 옆에 'fps' 입력을 추가하고 기본을 1로 낮춰 빠르게 끝나게 함
(자막이 빨리 바뀌면 2~3으로 올림). ocrScreen()이 sampleFps 를 함께 전달.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
캐시(받은 원본) 보유 시 '원본 다운로드'를 숨기고, 그 자리에 '전사' 버튼을 띄운다
(전사만 실패했을 때 재다운로드 없이 재시도). 캐시 없으면 다시 '원본 다운로드' 표시.
- updateCacheUI: downloadBtn 숨김/표시 대칭 토글 + transcribeCachedBtn 노출
- runTranscribeCached 헬퍼 추출(다운로드 2단계 전사와 '전사' 버튼이 공유)
- '전사' 버튼 onclick=transcribeCached()
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
기존엔 '원본 다운로드'가 다운로드+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>
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>
받은 원본 영상을 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>
자가호스팅 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>
원본 다운로드/캐시 보유 시 왼쪽 플레이어를 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>
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>
재가공 사용법 모달 상단에 실제 화면 캡쳐 3장으로 단계별 안내 추가:
1) 원본 다운로드 버튼 2) 자동 전사된 시간싱크 자막 3) 내보내기(업로드 불필요).
?help=1 딥링크로 사용법을 바로 펼칠 수 있게 함.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
재가공 화면 '원본 다운로드' 버튼으로 저장된 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>
모바일(≤768px)에서 사이드바를 펼쳐도 아이콘만 보이던 문제 수정.
태블릿용(≤1024px) 규칙이 .nav-text/.brand-text/.nav-section/.user-info를
display:none 처리하는데, 모바일 오버레이로 펼친 상태(.sidebar:not(.collapsed))
에서 그게 안 풀려 아이콘만 남았다.
→ 펼친 상태에서 라벨 다시 표시(display:revert) + nav 좌측 정렬. 모바일 사이드바를
전체폭 대신 280px 드로어 오버레이(그림자)로 정리.
CSS 변경에 맞춰 캐시버스팅 버전 20260617b 로 bump.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
폰에서 화면이 깨지던 문제 수정.
- 전역 반응형 표 '카드뷰'가 모든 table에 적용돼, data-label 없는 목록 표
(수집함·발굴·발행 등)가 거대한 행으로 변해 페이지가 32만px로 늘어남.
→ 카드뷰를 .rcards opt-in 으로 스코프. 그 클래스 가진 레거시 표
(multi_channel_videos·videos)만 카드뷰, 나머지는 모바일에서 가로 스크롤.
- 대시보드: 모바일(≤680px) 미디어쿼리 추가 — 마스트헤드 세로 정렬, 긴
브레드크럼 숨김, 떡상 리더보드 액션 버튼을 아랫줄 우측 정렬, 숫자 축소.
- base.html: CSS/JS 링크에 v= 캐시버스팅 — 배포 후 브라우저가 옛 CSS를
캐시해 스타일이 깨지는 것 방지(CSS 변경 시 버전 올리기).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
대시보드의 editorial 정체성을 공용 디자인 토큰으로 끌어올려 전 페이지에 전파.
- variables.css: 종이 캔버스 + 잉크 텍스트 + 버밀리언(#ff4d23) 단색 액센트,
Bricolage Grotesque(헤딩, 라틴) + Hanken Grotesk(본문) + JetBrains Mono,
샤프한 모서리(radius 축소). 라이트 기본 + 다크. 기존 변수 별칭 유지로
각 페이지가 자동 적응.
- style.css: 헤딩 디스플레이 페이스, primary 버튼 단색 버밀리언, 배지 각짐 등
편집풍 마무리 터치.
한글은 디스플레이/모노 폰트에 없으므로 시스템 폰트로 자연 폴백.
수집함·칸반 라이트/다크 Playwright 검증.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
수집함·발굴·대시보드처럼, 썸네일 있는 나머지 화면에서도 썸네일을 클릭하면
YouTube 9:16 미리보기 팝업이 뜨도록 통일.
- 칸반(board): 카드 썸네일 클릭(드래그와 분리 위해 stopPropagation)
- 채널상세(channel_detail): 영상 목록 썸네일(제목 링크는 YouTube 유지)
- 프로덕션상세(production_detail): videoUrl에서 11자리 id 추출해 임베드,
id 없으면 새 탭 폴백
각 페이지에 동일 모달(배경/ESC 닫힘, 닫을 때 iframe 비워 재생 중지) 추가.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
수집함처럼, 떡상 후보 리더보드의 썸네일을 클릭하면 YouTube 임베드(9:16)
미리보기 팝업이 바로 뜨도록 추가. 편집 정체성에 맞춘 모달(.fd-vmodal),
배경 클릭/ESC로 닫힘, 닫을 때 iframe 비워 재생 중지.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuild the dashboard with a distinctive editorial identity (Bricolage Grotesque
display + JetBrains Mono tickers + paper canvas + vermilion signal accent, ruled
layout, grain, staggered load) scoped to .fd-dash so other pages keep the SaaS
theme. UX is action-first:
- action bar: 미검토/떡상 후보/재가공 대기 with primary CTAs (real counts)
- 떡상 후보 leaderboard wired to real /api/dashboard/summary outperformers, with
working per-row 재가공(/rework/{id}) and 제외(status EXCLUDED) actions
- pipeline funnel with bottleneck callout + 칸반 CTA
- source/format + publish with empty-state guidance
Light/dark aware. Editorial fonts loaded in base.html.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge thumbnail+title+channel into one "영상" column like 수집함 so the table
fits without horizontal scroll. Promote .coll-table/.row-sel to global style.css
for reuse across list pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Category management collapsed into a slim single-row strip
- "사용법" button opens a reusable help modal (.modal-overlay/.modal-card +
.help-item) explaining categories, filters, list view, and row actions
- Merge thumbnail+title+channel into one "영상" column and tighten cells so the
whole table fits the viewport — no horizontal scroll on page or table card
- #mainContent min-width:0 to avoid flex overflow
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace raw checkboxes + stacked-label dropdowns with pill toggle chips
(.chip, accent when checked via :has) and a single aligned toolbar row;
result count as a badge. Adds reusable .toolbar/.chip/.field components.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tokenized dark-assuming inline colors (white title/dropdown text, translucent
white surfaces, #1e1e2d modal) so the collection table renders correctly in the
light theme; header uses shared page-header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase A+dashboard of the UI redesign:
- variables.css: light-default tokens + [data-theme=dark] overrides; old var
names aliased to theme tokens so existing markup adapts to both themes
- style.css: refined components (card/btn/nav/badge/bar/table/forms), tokenized
the dark-assuming rgba colors
- theme toggle: pre-paint init in base.html <head> + toggleTheme() in common.js,
persisted to localStorage; toggle button in sidebar
- sidebar.html: labeled nav with sections (분석/파이프라인/제작), active state,
account; Korean labels
- dashboard.html: tokenized inline colors; verified light & dark with real data
Spec: docs/superpowers/specs/2026-06-12-ui-redesign-design.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>