From 4927c61ac57708b80934bd7c118b5f6a51a71d08 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Tue, 18 Aug 2026 17:01:46 +0900 Subject: [PATCH] fix: stabilize YouTube cuts and draft generation --- README.md | 4 ++-- SETUP.md | 8 +++---- assets/intro_dingdong.mp3 | Bin 0 -> 16976 bytes capcut_agent/draft.py | 48 +++++++++++++++++++++++++++++++------- capcut_agent/youtube.py | 19 +++++++++++++-- requirements.txt | 2 +- server/static/index.html | 10 ++++---- 7 files changed, 68 insertions(+), 23 deletions(-) create mode 100644 assets/intro_dingdong.mp3 diff --git a/README.md b/README.md index 5b26cab..dcc1338 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,8 @@ h-lab 댓글 수집 → 컷별 댓글 카드 추천까지 자동으로 돌고, |------|------| | 수정한 게 반영 안 됨 | 검은 창 닫고 **.bat 재시작** | | `python not found` | Python 재설치 시 PATH 체크 후 재부팅 | -| 유튜브 `Video unavailable` | 영상이 실제 공개인지 확인 → `python -m pip install -U yt-dlp` | -| 다운로드가 자꾸 깨짐 | `python -m pip install -U yt-dlp` (유튜브가 가끔 바뀜) | +| 유튜브 `Video unavailable` | 영상이 실제 공개인지 확인 → `python -m pip install -U --pre "yt-dlp[default]"` | +| 다운로드가 자꾸 깨짐 / `HTTP 403` | `python -m pip install -U --pre "yt-dlp[default]"` (nightly + EJS 구성요소 업데이트) | | 자막이 밀림 | 붙여넣기 탭은 컷·자막을 그대로 쓰므로 안 밀림. 파일/유튜브 탭은 Whisper 타이밍 사용 | | 자막 `\n` 이 글자로 박힘 | .bat 재시작하면 해결(앱이 `\n`·`\\n` 자동 분할) | | 폰트가 다르게 나옴 | CapCut 에서 코트라 볼드체·제주명조체를 한 번씩 사용해 캐시 생성 | diff --git a/SETUP.md b/SETUP.md index 9663bc9..9d37164 100644 --- a/SETUP.md +++ b/SETUP.md @@ -135,7 +135,7 @@ python-multipart==0.0.20 pyCapCut==0.0.3 # import 이름은 pycapcut Pillow==10.4.0 pymediainfo==7.0.1 -yt-dlp==2026.7.4 +yt-dlp[default]>=2026.8.17.73947.dev0 faster-whisper==1.2.1 # ── faster-whisper / fastapi 전이 의존성(자동 설치되지만 버전 고정용) ── @@ -150,7 +150,7 @@ pydantic==2.11.7 ``` > **참고** -> - `yt-dlp`는 pip로 깔면 `yt-dlp` **콘솔 스크립트가 PATH에 생겨** 코드가 subprocess로 호출합니다. (유튜브는 자주 막히니 **주기적으로 `python -m pip install -U yt-dlp` 업데이트 권장** — 버전 고정하지 말 것.) +> - `yt-dlp`는 pip로 깔면 `yt-dlp` **콘솔 스크립트가 PATH에 생겨** 코드가 subprocess로 호출합니다. (유튜브는 자주 바뀌므로 문제가 생기면 **`python -m pip install -U --pre "yt-dlp[default]"`** 로 nightly와 EJS 구성요소까지 업데이트하세요.) > - SSE(진행상황 스트림)는 FastAPI 내장 `StreamingResponse` 사용 → `sse-starlette` **불필요**. > - Gemini 교정은 표준 라이브러리 `urllib`로 REST 직접 호출 → `google-generativeai` **패키지 불필요**. > - `pymediainfo`는 내부적으로 **MediaInfo DLL**을 씀. 대개 wheel에 포함되나, 안 되면 https://mediaarea.net/en/MediaInfo 의 DLL을 PATH에 두면 됨. @@ -272,7 +272,7 @@ yt-dlp --version | `ffmpeg`/`ffprobe` not found | PATH 미등록 | ffmpeg `bin`을 시스템 PATH에 추가, 창 새로 열기 | | 유튜브 다운로드가 ffmpeg 크래시(exit 3436169992) | JS 런타임 없음 | Node.js 또는 deno 설치 | | "Video unavailable" | 영상 자체 없음/지역제한 | `curl "https://www.youtube.com/oembed?url=&format=json"` 404면 영상 문제 | -| 유튜브가 갑자기 다 실패 | yt-dlp 구버전 | `python -m pip install -U yt-dlp` | +| 유튜브가 갑자기 다 실패 / `HTTP 403` | yt-dlp 구버전·YouTube 클라이언트 정책 변경 | `python -m pip install -U --pre "yt-dlp[default]"` | | 자막이 기본 폰트로 나옴 | 코트라 볼드체 캐시 없음 | §6 — CapCut에서 1회 사용 or font.ttf 복사 (없어도 동작은 함) | | ASR 첫 실행이 매우 느림/멈춘 듯 | Whisper medium(1.5GB) 다운로드 중 | 최초 1회. 인터넷 확인, 기다리기 | | 자막 교정이 안 됨 | Gemini 키 없음/429 | 선택 기능. 없으면 Whisper 원문 사용(정상) | @@ -306,7 +306,7 @@ python-multipart 0.0.20 pyCapCut 0.0.3 (import pycapcut) Pillow 10.4.0 pymediainfo 7.0.1 -yt-dlp 2026.7.4 (최신 유지 권장) +yt-dlp 2026.8.17 nightly 이상 (`yt-dlp[default]`, 최신 유지 권장) faster-whisper 1.2.1 ├ av 16.0.1 ├ ctranslate2 4.6.2 diff --git a/assets/intro_dingdong.mp3 b/assets/intro_dingdong.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..62b204e69e045cb22404600e5a57da3a18599cd9 GIT binary patch literal 16976 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 literal 0 HcmV?d00001 diff --git a/capcut_agent/draft.py b/capcut_agent/draft.py index b248231..26517bb 100644 --- a/capcut_agent/draft.py +++ b/capcut_agent/draft.py @@ -22,6 +22,9 @@ Clip = Tuple[float, float, Optional[str]] DEFAULT_DRAFT_ROOT = os.path.join( os.environ["LOCALAPPDATA"], "CapCut", "User Data", "Projects", "com.lveditor.draft" ) +INTRO_SFX_PATH = os.path.abspath(os.path.join( + os.path.dirname(__file__), "..", "assets", "intro_dingdong.mp3" +)) def _us(seconds: float) -> int: @@ -230,6 +233,8 @@ def build_bg_template_draft( script.add_track(p.TrackType.text, "channel", relative_index=4) if effect_captions: script.add_track(p.TrackType.text, "effect", relative_index=5) + if os.path.isfile(INTRO_SFX_PATH): + script.add_track(p.TrackType.audio, "intro_sfx") # 영상 소재 로드 + 실제 소재 길이(µs)로 컷 끝 클램프 # ffprobe 가 보고한 길이가 CapCut 이 보는 소재 길이보다 살짝 길 때(마지막 프레임) @@ -247,6 +252,17 @@ def build_bg_template_draft( # 타임라인 길이 = 보존 비디오 구간 합(클램프 반영) total_us = sum(e - s for s, e in clips_us) + # 완성 영상 시작(0초)에 인트로 효과음을 한 번 재생한다. + # 극단적으로 짧은 영상은 타임라인 끝까지만 사용해 소재 범위 초과를 막는다. + if os.path.isfile(INTRO_SFX_PATH) and total_us > 0: + intro_sfx = p.AudioMaterial(INTRO_SFX_PATH) + intro_dur = min(total_us, intro_sfx.duration) + if intro_dur > 0: + script.add_segment( + p.AudioSegment(intro_sfx, p.Timerange(0, intro_dur)), + "intro_sfx", + ) + # 흰 배경(맨 아래, 전체 길이) — 선택 if has_bg: bg_mat = p.VideoMaterial(bg_image_path) @@ -278,7 +294,8 @@ def build_bg_template_draft( cap_style = p.TextStyle(size=18.0 if politics_style else 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: + caption_end_us = 0 + for ts, te, text in sorted(captions, key=lambda x: (x[0], x[1])): if _us(te) - _us(ts) <= 0 or not text: continue lines = _cap_lines(text) @@ -287,14 +304,19 @@ def build_bg_template_draft( # 구간을 줄 수만큼 균등 분할 → i번째 줄이 i번째 시간대에 표시(같은 위치) a = ts + (te - ts) * i / n b = ts + (te - ts) * (i + 1) / n - seg = _us(b) - _us(a) + # Whisper 구간이 수 ms 겹치는 경우 pycapcut은 같은 텍스트 트랙의 + # SegmentOverlap으로 드래프트 전체를 중단한다. 앞 자막 끝으로 클램프한다. + a_us = max(_us(a), caption_end_us) + b_us = min(_us(b), total_us) + seg = b_us - a_us if seg <= 0: continue script.add_segment(p.TextSegment( - ln, p.Timerange(_us(a), seg), + ln, p.Timerange(a_us, seg), style=cap_style, border=cap_border, clip_settings=p.ClipSettings(transform_y=caption_y), ), "caption") + caption_end_us = b_us # 효과 자막(중앙): 하단 자막 '바로 위'. 주황 볼드 + 검은 외곽선(배경박스 없음). if effect_captions: @@ -302,15 +324,19 @@ def build_bg_template_draft( eff_style = p.TextStyle(size=13.0, bold=False, color=(13/255, 255/255, 99/255), align=1) # #0dff63 녹색 eff_border = p.TextBorder(color=(0.0, 0.0, 0.0), width=18.0) - for ts, te, text in effect_captions: - dur = _us(te) - _us(ts) + effect_end_us = 0 + for ts, te, text in sorted(effect_captions, key=lambda x: (x[0], x[1])): + start_us = max(_us(ts), effect_end_us) + end_us = min(_us(te), total_us) + dur = end_us - start_us if dur <= 0 or not text: continue script.add_segment(p.TextSegment( - text, p.Timerange(_us(ts), dur), + text, p.Timerange(start_us, dur), style=eff_style, border=eff_border, clip_settings=p.ClipSettings(transform_y=eff_y), ), "effect") + effect_end_us = end_us # 프레임(상하 검은 띠 + 투명 가운데) — 영상 위, 전체 길이 frame_mat = p.VideoMaterial(frame_image_path) @@ -330,8 +356,11 @@ def build_bg_template_draft( # CapCut scale 1.0 = contain. 댓글 카드는 캔버스(9:16)보다 가로로 넓으므로 # 가로가 먼저 맞아 표시 높이 = 1080 × (h/w) × scale. if comment_cards: - for ts, te, img in comment_cards: - dur = _us(te) - _us(ts) + comment_end_us = 0 + for ts, te, img in sorted(comment_cards, key=lambda x: (x[0], x[1])): + start_us = max(_us(ts), comment_end_us) + end_us = min(_us(te), total_us) + dur = end_us - start_us if dur <= 0 or not img or not os.path.isfile(img): continue eff = COMMENT_SCALE @@ -346,10 +375,11 @@ def build_bg_template_draft( else: cy = comment_y if comment_y is not None else round(-1162/1920, 4) script.add_segment(p.VideoSegment( - p.VideoMaterial(img), p.Timerange(_us(ts), dur), + p.VideoMaterial(img), p.Timerange(start_us, dur), clip_settings=p.ClipSettings(scale_x=eff, scale_y=eff, transform_x=0.0, transform_y=cy), ), "comment") + comment_end_us = end_us full = p.Timerange(0, total_us) # 제목 두 줄: 윗줄 = 주황 포인트색, 아랫줄 = 흰색. 둘 다 크게+볼드+검은 외곽선. diff --git a/capcut_agent/youtube.py b/capcut_agent/youtube.py index 6c489a6..2318b5f 100644 --- a/capcut_agent/youtube.py +++ b/capcut_agent/youtube.py @@ -30,6 +30,21 @@ TIME_RE = re.compile(r"^(?:\d{1,2}:)?\d{1,2}:[0-5]\d$") TIMEOUT_SEC = 1800 +def _yt_dlp_error(proc: subprocess.CompletedProcess) -> str: + """Keep the useful upstream error instead of only ffmpeg's final exit line.""" + output = (proc.stderr or proc.stdout or "").strip() + if not output: + return f"exit code {proc.returncode}" + lines = output.splitlines() + diagnostic = [ + line for line in lines + if ("ERROR:" in line or "HTTP error" in line + or "Error opening input" in line or "Forbidden" in line) + ] + selected = diagnostic[-12:] if diagnostic else lines[-20:] + return "\n".join(selected)[-4000:] + + def valid_time(t: str) -> bool: return bool(TIME_RE.match(t.strip())) @@ -171,7 +186,7 @@ def cut_youtube(url: str, start: str, end: str, out_dir: str) -> Tuple[str, str, proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=TIMEOUT_SEC, env=env) if proc.returncode != 0: - tail = (proc.stderr or proc.stdout or "").strip()[-800:] + tail = _yt_dlp_error(proc) raise RuntimeError(f"yt-dlp 실패:\n{tail}") # stdout 에서 채널명 파싱 (경로는 폴더 스캔으로 확정 — 인코딩 의존 제거) @@ -349,7 +364,7 @@ def _dl_section(url: str, start_sec: float, end_sec: float, out_dir: str, if proc.returncode != 0: proc = _run([]) if proc.returncode != 0: - tail = (proc.stderr or proc.stdout or "").strip()[-800:] + tail = _yt_dlp_error(proc) raise RuntimeError(f"yt-dlp 실패:\n{tail}") channel = "" diff --git a/requirements.txt b/requirements.txt index cd7b94d..25dd688 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,5 +12,5 @@ python-multipart==0.0.20 pyCapCut==0.0.3 Pillow==10.4.0 pymediainfo==7.0.1 -yt-dlp==2026.7.4 +yt-dlp[default]>=2026.8.17.73947.dev0 faster-whisper==1.2.1 # 파일/유튜브 구간 탭의 자막 받아쓰기용(붙여넣기 탭만 쓰면 불필요) diff --git a/server/static/index.html b/server/static/index.html index d759594..d53930e 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -362,14 +362,14 @@
- + - +
-
+