Commit Graph

28 Commits

Author SHA1 Message Date
76714e4bde feat: 정치 숏폼 자동 생성 모드 추가 2026-08-13 14:06:31 +09:00
34ea42a5e4 fix: 파일·유튜브 구간 탭의 '무음 제거' 체크박스가 무시되던 문제
체크를 꺼도 무음 제거가 항상 돌았다. 세 층이 전부 끊겨 있었다:
- UI: titleFields()·ytAnalyze() 어디서도 #rmsilence 값을 읽지 않아 폼에 안 실림
- 서버: /upload·/yt/analyze 가 remove_silence 필드를 받지도 않음
  (받는 곳은 지금 아무도 안 부르는 옛 /paste 뿐이었다)
- 파이프라인: bg_analyze 에 remove_silence 인자 자체가 없어
  detect_speech_segments 가 무조건 실행

수정:
- bg_analyze / process_bg_template 에 remove_silence 인자 추가.
  끄면 keep=[(0,duration)] 통짜 → cut_plan·_remap_placements 가 항등 매핑이 돼
  자막·카드 좌표가 그대로 남는다(paste_analyze 와 같은 패턴).
- bg_steps(youtube, remove_silence) — 끄면 manifest 에서 silence 스텝 제외.
  안 그러면 화면에 영원히 대기 상태로 남는다.
- /upload·/yt/analyze 가 remove_silence 수신 → job/analysis 에 저장 후 전달.
  /yt/analyze 는 aid 해시에도 포함(설정 바꾸면 새 분석).
- UI 가 체크박스 값을 실제로 전송. 기본값은 '켬'으로 — 원래 기본 해제였는데
  그대로 연결하면 기존 동작이 조용히 뒤집힌다.

같은 파일에 있던 자동 탭 작업도 함께 커밋:
- 자동 탭 전용 #autoRmsilence 체크박스 → /auto/prepare 로 전달
- '끝까지 진행' 옵션(#autoAutoRun) — 검토 화면 두 곳을 기본값으로 통과
- 체크 상태 localStorage 기억, frame_template.png 갱신

붙여넣기 탭은 종전대로 무음 제거 강제 켬(/paste/stream 고정) — 의도된 설계.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:11:20 +09:00
f66b396d71 feat: 하단 자막을 제주명조체·흰색·검은 획 40 으로 (배경·그림자 없음)
캡컷 인스펙터 실측 스크린샷과 1:1 로 맞춤:
- 글꼴 제주명조체(JEJU_MYEONGJO, 캐시 id 7480851064179133702) — 자막 트랙에만 주입
- 색상 주황 #ff8000 → 흰색, 볼드 해제
- 획(외곽선) 검정 두께 40 추가 (CAPTION_BORDER_W)
- 그림자 제거 (_apply_shadow_to_track 을 caption 에 안 부름)
- 크기 12·배경 없음은 기존 그대로

폰트는 전체 주입(_apply_font_to_texts, 코트라 볼드체) 뒤에 새 헬퍼
_apply_font_to_track 으로 caption 트랙만 덮어쓴다 → 제목·출처는 코트라 볼드체 유지.
캐시 없는 PC 에서는 주입이 생략돼 안전하게 코트라 볼드체로 남는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:10:40 +09:00
f38776d71f feat: 구간 JSON에 title_top/title_main 선택 필드 — 검토 화면 제목 자동 채움
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:13:19 +09:00
9b2cc96e3e fix: 긴 댓글 카드가 출처를 덮던 문제 — 캡처 5줄 말줄임 + 드래프트 축소 안전망
카드 캡처는 댓글 길이만큼 세로로 자라는데 배치 높이 한계(영상 아래~출처
사이 약 640px)가 어디에도 없어 장문 댓글이 출처(@채널)와 화면을 덮었다.
1차: 카드 본문을 최대 5줄 말줄임(검토 화면 = 캡처 결과라 WYSIWYG).
2차: 그래도 표시 높이가 한계를 넘는 카드는 draft 배치에서 비율 축소.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 15:59:43 +09:00
2266d0a3ee fix: h-lab 수집 단계에서 광고/홍보 댓글 제외
좋아요를 조작한 광고(할인 링크 등)가 좋아요 채우기·후보 상위로 올라와
드래프트 카드에 들어가는 사고가 실제로 났다. 오탐이 더 아프므로 보수적으로
URL·"n% 할인"·"최저가"만 걸러낸다("할인"·"%" 단독 일상 댓글은 통과).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 15:54:20 +09:00
b9d81acfce cuts_from_state: sec를 압축 길이 기준으로 고정, 순환 임포트 주석 정정
sec가 build_highlight_cuts 안에서 start/end(원본 시각)로 계산되어 quota(압축
길이 기준)와 좌표계가 어긋났다 — 구간 탭·붙여넣기 탭처럼 무음 제거가 항상 켜진
경우 "20초인데 카드 3장"처럼 화면 표시가 매번 어긋났다. build_highlight_cuts는
다른 탭도 쓰므로 건드리지 않고, cuts_from_state가 결과를 받은 뒤 sec만 places
기준으로 덮어쓴다. 겸사겸사 순환 임포트 주석도 사실대로 고쳤다 — pipeline은
지금 recommend를 임포트하지 않아 순환이 아니고, 앞으로를 위한 예방 조치다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:39:11 +09:00
3dd5eee684 recommend.py에 cuts_from_state() 추가 — 세 탭 공통 조립 함수
받아쓰기 상태(압축 타임라인 places/captions + 원본 시각 orig_ranges)와 댓글을
받아 검토 화면용 cuts[]를 만든다. 자동·유튜브구간·붙여넣기 세 탭이 각자 조립
코드를 갖던 걸 이 함수 하나로 모아 좌표계 실수(압축 vs 원본 시각)를 한 곳에서만
막으면 되게 했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:31:31 +09:00
58fab2b2a4 bg_analyze가 구간 위치를 ranges_sec/raw_places/places로 state에 담는다
구간 탭에서 댓글 카드를 컷 단위로 배치하려면 "구간 N이 압축 타임라인의
어디인지"를 알아야 한다. 지금까지는 유튜브 구간을 다운로드·병합만 하고
그 경계 정보를 버렸다. 원본 시각(ranges_sec), 병합본 누적 구간
(raw_places), 무음 제거 후 압축 타임라인 구간(places)을 state에 추가한다.
파일 탭은 세 키 모두 빈 리스트로 유지되어 동작이 바뀌지 않는다(회귀 검증 완료).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:28:30 +09:00
7c74fec079 fix: 컷별 댓글 배정을 컷 우선이 아닌 순위(라운드) 우선으로 변경
컷 하나를 4순위까지 다 채운 뒤 다음 컷으로 넘어가면, 앞 컷의 약한 근거(4순위
좋아요)가 뒤 컷의 강한 근거(3순위 단어 겹침)보다 먼저 댓글을 가로챈다. 라운드를
바깥 루프로 두어(전 컷의 ts → 전 컷의 ai → 전 컷의 word → 전 컷의 like) 순위가
컷 순서를 이기게 했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 18:47:55 +09:00
b1c08908ae 컷별 댓글 추천에 단어 겹침(3순위)·좋아요(4순위) 폴백 추가
Gemini 가 실패하거나(503 등) 문맥은 맞지만 겹치는 단어가 없어 못 잡는
경우("와인 뱉는 장면" ↔ "싱크대로 달려간 이유")가 각각 달라, 대체가 아니라
시각 » 🤖AI 뒤에 한 단계 더 얹었다. 자막에서 조사를 뗀 어간까지 키워드로
뽑아 댓글 본문에 부분 문자열로 걸리면 매칭한다("넉살이"→"넉살"이 "넉살님"에
걸리는 게 핵심 케이스). 네트워크 없이 항상 동작해 Gemini 가 죽어도 0건은
안 나온다. 그래도 자리가 남으면 분:초 없는 댓글 중 좋아요 상위로 채운다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 18:43:19 +09:00
9ea70d01a7 자동 탭 컷별 추천 모델 교체(gemini-3.5-flash) + 429/503 재시도
gemini-2.5-flash 가 HTTP 503("high demand")을 자주 반환해(실측 3연속 503)
ai_pick_cuts 가 재시도 없이 즉시 포기, 하이라이트의 컷 전체가 🤖 0개가 되는
증상의 근본 원인이었다. plan.py 가 이미 쓰는 gemini-3.5-flash 로 모델을
통일하고, 429/503 한정으로 3초·8초 간격 재시도(최대 3회 시도)를 추가했다.
그 외 오류와 재시도 소진 시에는 기존과 동일하게 None 을 반환한다(예외 미노출).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 18:42:57 +09:00
e79f2dfaa3 process_paste를 paste_analyze/paste_draft 두 조각으로 분할
댓글 매칭을 받아쓰기(asr_bottom) 다음에 넣으려면 파이프라인이
"받아쓰기까지" 상태에서 한 번 멈출 수 있어야 한다. 앞서 process_bg_template에
적용한 것과 같은 패턴(analyze/draft 분리 + state 이벤트 + 얇은 래퍼)을
process_paste에도 적용했다. 로직은 옮기기만 했고 바꾸지 않았다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:36:50 +09:00
475b7b0f71 process_bg_template을 bg_analyze/bg_draft로 분할
댓글 매칭을 받아쓰기 다음 단계에 끼워 넣으려면 파이프라인이 "받아쓰기까지"에서
한 번 멈출 수 있어야 한다. 다운로드~asr(bg_analyze)과 장면분할~draft(bg_draft)
두 조각으로 나누고, 기존 동작을 완전히 보존하는 얇은 래퍼(process_bg_template)로
다시 감쌌다. 이벤트 스트림을 분할 전/후로 떠서 문자 단위로 비교해 11개 이벤트가
완전히 동일함을 확인했다(state 이벤트는 밖으로 새지 않음).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:28:54 +09:00
aab2a3199b 컷별 자막 추출 헬퍼 함수 추가 (captions_for_places)
받아쓰기(ASR) 결과에서 컷 구간별로 자막을 이어붙이는 순수 함수.
- 시간축 겹침 판정: 닿기만(경계) 제외, 조금이라도 겹치면 포함
- 공백 정규화: 여러 칸 → 공백 하나
- 토큰 제한: cap 문자에서 자르기 (Gemini 효율)
- 테스트 모두 통과 (경계 규칙·공백·길이·엣지 케이스)

다음 단계(2단)에서 세 탭이 모두 호출할 예정.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:12:01 +09:00
a1f98b0b53 무음 제거 후 실제 길이 기준 카드 장수를 외부에서 주입 가능하게
build_highlight_cuts()에 quotas 파라미터 추가. 무음 제거 후 줄어든 타임라인에 맞춰
카드 장수를 계산한 후 넘길 수 있다. quotas를 넘기지 않으면 기존 동작(원본 길이 기준)
그대로여야 한다 — 기존 호출부에는 변화 없다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:06:35 +09:00
2525fb5231 시각 기반 판별을 컷 1개에서 모든 컷으로 일반화
- is_whole() → is_time_based(): 컷 개수와 무관하게 모든 자막이 비었으면 참
  * 통짜 모드(1개 컷) + 유튜브 구간 탭(2개 이상 자막 없는 구간)을 둘 다 지원
  * 자막 없으면 내용 추천 불가 → 시각 기반으로 배정
- _whole_picks() → _time_based_picks(): 여러 컷 반복 처리
  * used 집합을 컷 사이에 공유해 댓글 중복 금지 (앞 컷 우선)
  * 각 컷의 quota에 맞춰 슬롯 배정, 빈 슬롯은 좋아요 상위로 채움
- build_highlight_cuts() 호출부 업데이트
  * 기존 통짜 모드는 그대로 동작 (1개 컷 + 빈 자막 = 참)
  * 새 규칙이 유튜브 구간 탭으로 확장

검증:
- 자막 판별 조건 (empty/whitespace 포함 처리)
- 컷 간 중복 금지 확인
- 자동 탭 회귀 테스트 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:59:33 +09:00
c39b72518f 컷별 카드 장수 공식을 floor로 통일 — round와 어긋나 카드가 조용히 버려지던 문제 수정
recommend.quotas_for()는 round, pipeline._cards_by_cut()의 컷당 장수 상한은 floor를
써서 둘이 어긋났다. 5초 컷처럼 round가 floor보다 큰 쪽으로 갈리는 컷은 UI가 카드
2장을 고르게 하고도 빌드 단계에서 상한(1장)에 걸려 뒤 1장이 로그 없이 버려졌다.
사용자 결정에 따라 floor로 통일(카드 한 장이 항상 3초 이상 — 기존
_load_comment_cards 규칙과 동일)하고, 무음 제거로 컷이 압축돼 여전히 카드가
버려지는 경우(이건 불가피)를 컷별 배치 로그에 표시해 더 이상 조용히 사라지지
않게 했다. 관련 설계/계획 문서의 공식·테스트 기대값도 floor 기준으로 맞췄다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:05:14 +09:00
499223a125 카드 배치를 무음 제거 뒤로 옮겨 3초 약속을 지킨다
카드 시간을 무음 제거 **전** 타임라인에서 만들고 _remap_caps 로 압축하고 있었다.
자막은 그게 맞다(발화 시각을 따라가야 하니까). 하지만 카드는 다르다:

- cards_fixed=True 는 "카드는 정확히 3초"라는 절대 길이 약속이라 압축하면 깨진다.
  컷 한가운데가 무음이면 3.0초짜리가 2.0초로 눌린다.
- fixed=False 에서도 3초 하한이 사라져 있었다. 기존 _load_comment_cards 는
  n = max(1, int(dur // 3)) 으로 카드당 3초를 보장하는데 _cards_by_cut 엔 그게 없었고,
  quota 는 **원본** 컷 길이로 계산되므로 무음이 절반인 24초 컷은 8장 → 압축 12초 →
  장당 1.5초가 됐다.

고친 방식:
- _remap_placements() 추가 — 카드는 '시간'이 아니라 '컷 구간 자체'를 옮긴다.
- _cards_by_cut 호출을 무음 제거 블록 뒤로 옮기고 압축된 구간·timeline_dur 로 계산.
  무음 제거가 꺼져 있으면 card_places = placements 그대로다.
- _cards_by_cut 에 컷당 장수 상한 min(len, max(1, floor(컷길이/min_sec))) 추가.
  초과분은 버린다 — _load_comment_cards 와 같은 규칙.
- 이제 필요 없어진 cut_cards 의 _remap_caps 호출 제거.

기존 탭(process_bg_template)과 _load_comment_cards 는 손대지 않았다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:12:31 +09:00
1e43530933 Gemini 추천 실패를 화면에 알린다 (조용히 삼키지 않음)
ai_pick_cuts 가 429·타임아웃·JSON 파싱 실패를 전부 {} 로 삼켜서,
build_highlight_cuts 는 여전히 타임스탬프만으로 채운 cuts 를 돌려주고
app.py 의 `if cuts:` 가 참이 되어 except 가 절대 타지 않았다.
스펙 §7이 요구한 "⚠ 추천 실패 → 기존 방식" 경고가 화면에 안 떴다.

- ai_pick_cuts: 실패는 None, 성공은 dict(빈 dict 포함).
  키가 없는 경우는 실패가 아니다 — 키 없이 쓰는 것도 정상 사용이라 {} 를 준다.
  실패로 치는 건 HTTP·타임아웃·응답 파싱·프롬프트 치환 오류뿐.
- 응답 파싱을 _rows_to_picks() 로 떼어내 '깨진 응답'(None)과
  '어울리는 게 없어 빈 배열'({})을 구분한다. _parse_ai() 계약은 그대로 둔다.
- build_highlight_cuts → (cuts, need, ai_failed) 3-튜플.
  통짜 모드는 Gemini 를 안 부르므로 ai_failed=False.
- build_cut_picks 는 ai_picks=None 이어도 안전(`ai_picks or {}`).
- app.py: ai_failed 면 warnings 에 "AI 추천 실패 — 타임스탬프만으로 배정" 한 줄.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:09:19 +09:00
0a0b43efc2 댓글 카드를 컷 구간 안에 배치하는 경로 추가
_load_comment_cards를 _card_paths/_load_comment_cards/_cards_by_cut로 분리했다.
자동 탭은 카드가 어느 컷 소속인지(card_cuts)만 넘기면 그 컷 구간 안에서 균등
배치되어, 한 컷의 카드가 모자라도 다음 컷 카드가 앞으로 밀리지 않는다(기존
전체 균등 배치의 약점). 카드 시간은 무음 제거 전 placements 직후에 계산하고,
자막과 동일하게 _remap_caps()로 재매핑한다 — 카드 튜플(start,end,path)이
자막 튜플(start,end,text)과 모양이 같아 그대로 재사용 가능하다. card_cuts가
없으면 기존 _load_comment_cards 폴백으로 떨어져 파일/유튜브 구간 탭은 영향
없다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:18:33 +09:00
005beba4c3 하이라이트 조립 — build_highlight_cuts() 모드 분기 구현
통짜 모드(컷 1개 + 자막 없음)는 시각 슬롯으로 배정, 컷 있는 모드는 Gemini 추천.
is_whole() 판별, _whole_picks() 슬롯 채우기, build_highlight_cuts() 조립.
import에 match_slots, top_liked 추가.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:07:48 +09:00
ee30e87bb4 예외 처리 개선: ValueError와 UnicodeDecodeError 추가
- str.format() 예외 처리에 ValueError 추가 (템플릿 중괄호 불균형)
- .decode(utf-8) 예외 처리에 ValueError 추가 (UnicodeDecodeError는 ValueError 서브클래스)
- URLError/TimeoutError는 이미 OSError 서브클래스이므로 정리 (주석 추가)
- ai_pick_cuts는 모든 실패에 예외 없이 {} 반환하는 계약 이행

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:02:36 +09:00
7cb70cc33a Gemini 텍스트 호출로 컷별 댓글 추천 구현
correct.py의 텍스트 호출 패턴을 따라 ai_pick_cuts() 및 헬퍼 함수(_candidate_lines, _cut_lines, _parse_ai)를 추가했다.
- 목차 댓글 필터링(MAX_TIMES 기준)
- 좋아요순 정렬
- 응답 파싱 시 형식 오류는 조용히 버림
- 모든 실패(키 없음, 타임아웃, JSON 깨짐 등)에 {} 반환

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:56:08 +09:00
8367219b7f 컷별 댓글 추천 프롬프트 파일 로더 구현
- RECOMMEND_PATH 상수 추가 (프롬프트/댓글_추천.md)
- DEFAULT_RECOMMEND 기본 프롬프트 추가 ({cuts}, {comments}, {k} 치환자 포함)
- load_recommend() 함수 구현 (없으면 기본값으로 생성 후 읽기, load_step1()과 같은 패턴)

사용자가 메모장에서 프롬프트를 직접 수정할 수 있도록 설계.
Task 4에서 .format(cuts=…, comments=…, k=…)으로 채워져 Gemini에 전달됨.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:50:12 +09:00
df8bc7baec 컷별 댓글 배정 순수 로직 — quotas_for, build_cut_picks
배정 규칙(타임스탐프 우선·중복 제거·Gemini 폴백)을 댓글 수집·HTTP 처리와
분리해 순수 함수로 구현. 기준 카드 길이 3초(CARD_SEC)로 컷별 쿼터 계산.
타임스탠프 매칭은 match_ranges() 이용, Gemini 없이 검증 가능.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:44:59 +09:00
47362a814e match_slots() 함수 구현 — 구간 슬롯 배정 기능
통짜 모드에서 영상 구간을 n개 슬롯으로 나누고 각 슬롯에 댓글을 배정하는 순수 함수를 추가했다.
- match_ranges()를 활용해 슬롯 시간대와 매칭되는 댓글을 찾음
- 좋아요 높은 댓글부터 슬롯에 배정 (슬롯당 최대 1개)
- 한 댓글이 여러 슬롯에 중복되지 않도록 관리
- exclude 집합으로 사전에 제외된 댓글 처리

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:39:39 +09:00
bf1b387d6d chore: git 저장소 초기화 (기존 코드 스냅샷)
컷별 댓글 추천 작업을 태스크 단위로 되돌릴 수 있게 버전관리를 시작한다.
.gitignore 로 영상·캐시(.downloads 2.7G, .comments 72M, .media 28M)와
비밀키(.gemini_key)를 제외했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:36:05 +09:00