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>
영상 박힌 자막 → SRT/세그먼트(POST /ocr_video). 응답을 /transcribe 와 동일 형식
({language, segments[{start,end,text}], srt})으로 맞춰 h-lab 연동 재사용 가능하게 함.
h-python 서비스에 배포할 참고 코드.
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>
재가공 화면 '원본 다운로드' 버튼으로 yt-dlp 로컬 실행 → 서버 캐시 →
기존 Python 전사/렌더 파이프라인 재사용. 전사·렌더 양쪽에서 수동 업로드 제거.
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>
Replace the outdated React/Vite/8080/H2 README with an accurate, shielded
overview: Spring Boot 3.4 / Java 21 Thymeleaf SSR, the collection→discovery→
curation→rework→publish pipeline, subtitle studio (Whisper+ffmpeg), light/dark
UI, getting-started, env vars, and project structure.
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>
Light-default + dark toggle, refined SaaS components, phased rollout across
all SSR pages. Approved dashboard mockup as the visual reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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) <noreply@anthropic.com>
Add .playwright-mcp/ and verify-*.png to .gitignore (transient scratch).
Untrack .claude/settings.local.json and scheduled_tasks.lock, which were
already listed in .gitignore but still indexed; settings.local.json stays
on disk as a local-only file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dashboardSummary surfaced the earliest-scheduled packages as "recent"
(it reused the queue's scheduledAt-asc sort); query updatedAt-desc instead.
Build byStatus from an explicit ordered list since Set.of iteration order
is undefined, so the dashboard renders DRAFT/READY/PUBLISHED consistently.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove hardcoded DB credentials and YouTube API key fallbacks from
application.yml; resolve them from env vars or an optional, git-ignored
application-local.yml (spring.config.import). Add a tracked
application-local.yml.example template and ignore the real local file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- publish.html: status column uses shared .badge (badge-muted/warning/
success) instead of its own .st-badge inline-color span
- collection.html / discover.html: drop .filter-sel rules now that the
shared `select` styling covers them (row-sel kept for compact inline)
- style.css: .gap-2 uses var(--space-2) for token consistency
No behavior change; follow-up cleanup from the design-uplift review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>