From f66b396d7152c104c4c920356441a616242f1a40 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Mon, 10 Aug 2026 22:10:17 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=95=98=EB=8B=A8=20=EC=9E=90=EB=A7=89?= =?UTF-8?q?=EC=9D=84=20=EC=A0=9C=EC=A3=BC=EB=AA=85=EC=A1=B0=EC=B2=B4=C2=B7?= =?UTF-8?q?=ED=9D=B0=EC=83=89=C2=B7=EA=B2=80=EC=9D=80=20=ED=9A=8D=2040=20?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20(=EB=B0=B0=EA=B2=BD=C2=B7=EA=B7=B8?= =?UTF-8?q?=EB=A6=BC=EC=9E=90=20=EC=97=86=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 캡컷 인스펙터 실측 스크린샷과 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) --- ARCHITECTURE.md | 14 +++++++-- README.md | 4 +-- capcut_agent/draft.py | 68 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c871cf9..26fa868 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -240,11 +240,17 @@ title_top 서브제목 / title_main 메인제목 / channel 출처 / effect 효 핵심 구현 포인트: - **소재 길이 클램프**: ffprobe duration이 CapCut 소재 길이보다 수십 ms 길 수 있음 → 컷 끝을 `material.duration`으로 클램프(SegmentOverlap/초과 오류 방어). -- **자막 스타일**: **주황 `#ff8000`**(`CAPTION_COLOR`) + 볼드 + **그림자**, **배경박스 없음**, +- **자막 스타일**: **제주명조체** + **흰색**(`CAPTION_COLOR`) + 볼드 아님 + + **검은 획(외곽선) 두께 40**(`CAPTION_BORDER_W`), **배경박스 없음**, **그림자 없음**, 크기 **`CAPTION_SIZE = 12.0` 고정**(캡컷 폰트 크기 1:1). 배경은 `TextSegment(background=...)` 인자를 **생략**해서 끈다 → `background_style` 키 자체가 안 나가고 CapCut 이 읽으면서 0(없음)으로 채운다(제목 텍스트가 원래 이 방식). - 그림자는 저장 후 `_apply_shadow_to_track(draft_dir, "caption", _TEXT_SHADOW)` 로 주입. + 폰트는 저장 후 `_apply_font_to_track(draft_dir, "caption", JEJU_MYEONGJO)` 로 **자막 트랙에만** + 주입한다(전체 주입 `_apply_font_to_texts(…, KOTRA_BOLD)` 뒤에 덮어씀 → 제목·출처는 코트라 볼드체). + 획 두께는 pycapcut `TextBorder(width=…)` 가 캡컷 UI 값과 같은 0~100 스케일 + (JSON 엔 `width/100*0.2` = 0.08 로 나감). + ⚠ 하단 자막 그림자는 **끔** — `_apply_shadow_to_track` 은 남아 있지만 자막엔 호출하지 않는다 + (되돌리려면 `build_bg_template_draft` 끝의 주석 줄 참고). 예전엔 최장 줄 기준 `fit_caption_size()`로 7~13 자동이었으나 드래프트마다 크기가 달라져 고정으로 바꿈. 청킹 하드캡 14자 × 크기10(≈51.5px) ≈ 721px < 1080 → 안 넘침. `fit_caption_size()`는 남겨둠(미사용, 자동 맞춤으로 되돌릴 때 사용). @@ -265,6 +271,8 @@ title_top 서브제목 / title_main 메인제목 / channel 출처 / effect 효 모든 텍스트 styles[].font에 직접 주입**(`_apply_font_to_texts`). 경로는 `%LOCALAPPDATA%/CapCut/User Data/Cache/effect/7480846567709265157/...`(PC 무관). 캐시 없으면(그 PC CapCut에서 폰트 미사용) 주입 생략 → 기본 폰트로 안전 동작. + **하단 자막만 제주명조체**(`JEJU_MYEONGJO`, id `7480851064179133702`) — + 전체 주입 뒤 `_apply_font_to_track(…, "caption", …)` 으로 덮어쓴다. - **트랙 자동 잠금**(`_lock_tracks`, 저장 직후): `frame`·`comment`·`title_top`· `title_main`·`channel` 트랙을 잠근다(`attribute |= 4`; mute 비트1은 OR로 보존). `main`(영상)·`caption`(자막)·`effect`는 편집용이라 **안 잠금**. `bg`(흰 배경)도 @@ -407,7 +415,7 @@ CHANNEL_RATIO = 0.85 # 아래 띠에서 85% 지점 | title_top (주황 size14) | 109 | 0.8865 | Y 1702 | | title_main (흰색 size18) | 252 | 0.7375 | Y 1416 | | 효과자막 (#0dff63 size13, bold X) | 348 | 0.6375 | Y 1224 | -| 하단 자막 (size **12**, **#ff8000**, 그림자, 배경 없음) | 1050 | −0.0938 | Y −180 | +| 하단 자막 (제주명조체 size **12**, **흰색**, 검은 획 40, 배경·그림자 없음) | 1050 | −0.0938 | Y −180 | | 댓글 카드 (scale 0.89, X 0, 3초/장) | 윗변 1122 | 카드마다 계산 | — | | channel (size 10) | 1800 | −0.8753 | Y −1681 | | 영상 확대 기본 | — | — | 144% | diff --git a/README.md b/README.md index c469770..dc3f17e 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ h-lab 댓글 수집 → 컷별 댓글 카드 추천까지 자동으로 돌고, 5. **CapCut** 설치 6. `캡컷_에이전트_구간합치기.bat` 실행 -> **코트라 볼드체**: 새 PC의 CapCut 에서 그 폰트를 한 번 사용하면 캐시가 생겨 자동 적용됩니다. 없으면 기본 폰트로 나옵니다(에러 아님). +> **코트라 볼드체**(제목·출처) / **제주명조체**(하단 자막): 새 PC의 CapCut 에서 그 폰트를 한 번 사용하면 캐시가 생겨 자동 적용됩니다. 없으면 기본 폰트로 나옵니다(에러 아님). > **드래프트 저장 위치**: 그 PC의 CapCut 프로젝트 폴더를 자동 인식합니다. > **.gemini_key**: '파일'·'유튜브 구간' 탭 자막 교정용. 붙여넣기 탭만 쓰면 없어도 됩니다. @@ -116,7 +116,7 @@ h-lab 댓글 수집 → 컷별 댓글 카드 추천까지 자동으로 돌고, | 다운로드가 자꾸 깨짐 | `python -m pip install -U yt-dlp` (유튜브가 가끔 바뀜) | | 자막이 밀림 | 붙여넣기 탭은 컷·자막을 그대로 쓰므로 안 밀림. 파일/유튜브 탭은 Whisper 타이밍 사용 | | 자막 `\n` 이 글자로 박힘 | .bat 재시작하면 해결(앱이 `\n`·`\\n` 자동 분할) | -| 폰트가 다르게 나옴 | CapCut 에서 코트라 볼드체 한 번 사용해 캐시 생성 | +| 폰트가 다르게 나옴 | CapCut 에서 코트라 볼드체·제주명조체를 한 번씩 사용해 캐시 생성 | --- diff --git a/capcut_agent/draft.py b/capcut_agent/draft.py index d1139e1..f72aa8c 100644 --- a/capcut_agent/draft.py +++ b/capcut_agent/draft.py @@ -123,7 +123,9 @@ COMMENT_MAX_BOTTOM = 1760 # 자동 맞춤(fit_caption_size)은 드래프트마다 크기가 달라져서 고정으로 바꿈. # 청킹 하드캡이 14자라 크기 10 이면 한 줄 폭 ≈ 14×51.5 ≈ 721px < 1080 → 넘칠 일 없음. CAPTION_SIZE = 12.0 -CAPTION_COLOR = (1.0, 128 / 255, 0.0) # #ff8000 주황 +# 하단 자막 스타일(캡컷 인스펙터 실측과 1:1): 제주명조체 · 흰색 · 검은 획 40 · 배경/그림자 없음. +CAPTION_COLOR = (1.0, 1.0, 1.0) # 흰색 +CAPTION_BORDER_W = 40.0 # 획(외곽선) 두께 — 캡컷 UI 값과 동일 스케일 # 자막 한 줄 맞춤: 캡컷 글꼴크기 ↔ Malgun Bold 픽셀 보정(size13≈67px → 5.15px/unit) CAPTION_PX_PER_UNIT = 5.15 @@ -267,8 +269,9 @@ def build_bg_template_draft( t = t.replace(a, b) return [ln.strip() for ln in t.split("\n") if ln.strip()] - # 배경 박스 없음(background 인자 생략) + 주황 #ff8000 + 그림자(저장 후 주입). - cap_style = p.TextStyle(size=CAPTION_SIZE, bold=True, color=CAPTION_COLOR, align=1) + # 배경 박스 없음(background 인자 생략) + 흰색 + 검은 획 40. 폰트는 저장 후 제주명조체 주입. + cap_style = p.TextStyle(size=CAPTION_SIZE, bold=False, color=CAPTION_COLOR, align=1) + cap_border = p.TextBorder(color=(0.0, 0.0, 0.0), width=CAPTION_BORDER_W) for ts, te, text in captions: if _us(te) - _us(ts) <= 0 or not text: continue @@ -283,7 +286,7 @@ def build_bg_template_draft( continue script.add_segment(p.TextSegment( ln, p.Timerange(_us(a), seg), - style=cap_style, + style=cap_style, border=cap_border, clip_settings=p.ClipSettings(transform_y=caption_y), ), "caption") @@ -364,7 +367,8 @@ def build_bg_template_draft( script.save() draft_dir = os.path.join(draft_root, draft_name) _apply_font_to_texts(draft_dir, KOTRA_BOLD) # 모든 텍스트에 코트라 볼드체 주입 - _apply_shadow_to_track(draft_dir, "caption", _TEXT_SHADOW) # 하단 자막 그림자 + _apply_font_to_track(draft_dir, "caption", JEJU_MYEONGJO) # 하단 자막만 제주명조체 + # 하단 자막은 그림자 없음(획 40 으로 대체) — _apply_shadow_to_track 호출하지 않는다. _lock_tracks(draft_dir, LOCK_TRACKS) # 오버레이·제목 트랙 잠금(편집 중 레이어 꼬임 방지) if bg_white and title_top: # 서브제목에 그림자 주입(캡컷 실측 형식) _apply_shadow_to_text(draft_dir, title_top, _TEXT_SHADOW) @@ -624,6 +628,55 @@ KOTRA_BOLD = { } +# 제주명조체(JejuMyeongjo) — 하단 자막 전용. 코트라 볼드체와 같은 CapCut 폰트 캐시 방식. +# 캐시가 없는 PC 에서는 주입이 생략돼 코트라 볼드체(전체 주입분)가 그대로 남는다. +JEJU_MYEONGJO = { + "path": os.path.join( + os.environ.get("LOCALAPPDATA", ""), "CapCut", "User Data", "Cache", "effect", + "7480851064179133702", "6ab2b6b0bd71a41109ad5506d600f257", "font.ttf", + ).replace("\\", "/"), + "id": "7480851064179133702", +} + + +def _text_material_ids(j: dict, track_name: str) -> set: + """지정 텍스트 트랙 세그먼트들의 material_id 집합.""" + ids = set() + for tr in j.get("tracks", []): + if tr.get("type") == "text" and tr.get("name") == track_name: + ids.update(s.get("material_id") for s in tr.get("segments", [])) + return ids + + +def _apply_font_to_track(draft_dir: str, track_name: str, font: dict) -> None: + """지정 텍스트 트랙의 소재에만 폰트 주입(_apply_font_to_texts 의 트랙 한정판). + + 전체 주입(코트라 볼드체) 뒤에 호출해 하단 자막만 다른 폰트로 덮어쓴다. + """ + import json + if not font.get("path") or not os.path.isfile(font["path"]): + return + jf = os.path.join(draft_dir, "draft_content.json") + if not os.path.isfile(jf): + return + j = json.load(open(jf, encoding="utf-8")) + ids = _text_material_ids(j, track_name) + if not ids: + return + for m in j["materials"].get("texts", []): + if m.get("id") not in ids: + continue + try: + c = json.loads(m["content"]) + except Exception: # noqa: BLE001 — 파싱 안 되는 소재는 건너뜀 + continue + for st in c.get("styles", []): + st["font"] = dict(font) + m["content"] = json.dumps(c, ensure_ascii=False) + with open(jf, "w", encoding="utf-8") as f: + json.dump(j, f, ensure_ascii=False) + + def _apply_font_to_texts(draft_dir: str, font: dict) -> None: """저장된 draft_content.json 의 모든 텍스트 재질 스타일에 폰트 주입. @@ -688,10 +741,7 @@ def _apply_shadow_to_track(draft_dir: str, track_name: str, shadow: dict) -> Non if not os.path.isfile(jf): return j = json.load(open(jf, encoding="utf-8")) - ids = set() - for tr in j.get("tracks", []): - if tr.get("type") == "text" and tr.get("name") == track_name: - ids.update(s.get("material_id") for s in tr.get("segments", [])) + ids = _text_material_ids(j, track_name) if not ids: return for m in j["materials"].get("texts", []):