capcut-agent/capcut_agent/media.py
hehihoho3@gmail.com 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

172 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""ffmpeg 미디어 변환 — 레터박스(9:16 검은 띠) 굽기.
핵심: 캡컷 scale/transform 의미 추측(함정)을 피하려 영상을 미리 정확한 캔버스
비율로 구워둔다. 결과 mp4 는 이미 9:16 → 캡컷에서 scale 1.0 으로 정확히 일치.
부수효과: AV1/webm → h264 재인코딩으로 캡컷 미리보기 문제도 해소.
"""
from __future__ import annotations
import os
import subprocess
from typing import Tuple
def make_frame(out_png: str, *, top_bar: int, bottom_top: int,
band_color: Tuple[int, int, int] = (0, 0, 0),
canvas: Tuple[int, int] = (1080, 1920)) -> str:
"""명시적 좌표로 프레임 생성: 상단 띠(0~top_bar) + 투명 가운데 + 하단 띠(bottom_top~H).
하단 띠를 크게(bottom_top 낮게) 잡으면 영상이 위로 올라가고 하단에 댓글 캡쳐용
빈 공간이 생긴다. 띠는 band_color 불투명, 가운데는 투명(영상이 비침).
배경.png 흰밴드 자동감지(make_transparent_frame) 대신 이걸 쓰면 레이아웃을
코드 상수로 정확히 통제할 수 있다.
"""
from PIL import Image # 지연 import
W, H = canvas
out = Image.new("RGBA", (W, H), (0, 0, 0, 0))
band = (*band_color, 255)
px = out.load()
for y in range(H):
if y < top_bar or y >= bottom_top:
for x in range(W):
px[x, y] = band
out.save(out_png)
return out_png
def extract_audio(src: str, out_path: str) -> str:
"""오디오만 압축 mp3로 추출 (Gemini 받아쓰기 전송용, mono 16k 48k)."""
subprocess.run(
["ffmpeg", "-y", "-hide_banner", "-loglevel", "error", "-i", src,
"-vn", "-ac", "1", "-ar", "16000", "-b:a", "48k", out_path],
check=True, encoding="utf-8", errors="replace",
)
return out_path
def detect_white_band(template_png: str) -> Tuple[int, int]:
"""검정-흰색-검정 템플릿에서 흰 밴드(영상 영역) [top, bottom] 픽셀 반환."""
from PIL import Image # 지연 import
im = Image.open(template_png).convert("RGB")
W, H = im.size
x = W // 2
whites = [y for y in range(H)
if min(im.getpixel((x, y))) > 200]
if not whites:
return 0, H
return whites[0], whites[-1] + 1
def make_transparent_frame(template_png: str, out_png: str,
band_color: Tuple[int, int, int] = (0, 0, 0)) -> str:
"""검정-흰색-검정 배경 템플릿 → 상하 띠는 band_color 로, 가운데(흰색)는 투명으로.
영상 위에 '프레임'으로 올려 상하 띠로 가리고 가운데는 영상이 비치게 한다.
band_color=(0,0,0) 검은 띠(기본), (255,255,255) 흰 띠.
흰색 밴드 경계를 자동 검출(중앙 컬럼 스캔).
"""
from PIL import Image # 지연 import
im = Image.open(template_png).convert("RGB")
W, H = im.size
x = W // 2
def is_white(y: int) -> bool:
r, g, b = im.getpixel((x, y))
return r > 200 and g > 200 and b > 200
whites = [y for y in range(H) if is_white(y)]
if whites:
top, bot = whites[0], whites[-1] + 1 # 흰색 밴드 = 투명 처리 구간
else:
top, bot = 0, H
r, g, b = band_color
out = Image.new("RGBA", (W, H), (0, 0, 0, 0))
px = out.load()
for y in range(H):
opaque = (y < top) or (y >= bot)
if not opaque:
continue
for xx in range(W):
px[xx, y] = (r, g, b, 255)
out.save(out_png)
return out_png
def make_solid(out_png: str, color: Tuple[int, int, int] = (255, 255, 255),
canvas: Tuple[int, int] = (1080, 1920)) -> str:
"""단색 전체 캔버스 PNG 생성(흰 배경 레이어 등). 가운데 빈 곳까지 그 색으로."""
from PIL import Image # 지연 import
W, H = canvas
Image.new("RGB", (W, H), color).save(out_png)
return out_png
# ── 템플릿 레이아웃 (1080×1920) ──
# 상단 제목 띠 | 영상(좌우 살짝 크롭, 크게) | 하단 채널 띠
TPL_W, TPL_H = 1080, 1920
TPL_TOP_BAR = 410 # 상단 검은 띠(제목)
TPL_VIDEO_H = 1000 # 영상 영역 높이 (410~1410)
# 하단 띠 = 1920 - 410 - 1000 = 510 (채널/출처)
TPL_VIDEO_TOP = TPL_TOP_BAR
TPL_VIDEO_BOTTOM = TPL_TOP_BAR + TPL_VIDEO_H
def to_template(
src: str,
out_path: str,
*,
crf: int = 20,
) -> str:
"""영상을 템플릿 비디오 영역(1080×1000, 좌우 cover 크롭)에 채우고 상하 검은 띠로
1080×1920 합성. 영상 틀만 굽고(제목/자막/채널은 캡컷 텍스트로 별도), scale 함정 회피.
"""
vf = (
f"scale={TPL_W}:{TPL_VIDEO_H}:force_original_aspect_ratio=increase,"
f"crop={TPL_W}:{TPL_VIDEO_H},"
f"pad={TPL_W}:{TPL_H}:0:{TPL_VIDEO_TOP}:color=black,"
f"setsar=1"
)
cmd = [
"ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
"-i", src, "-vf", vf,
"-c:v", "libx264", "-preset", "veryfast", "-crf", str(crf),
"-pix_fmt", "yuv420p", "-c:a", "aac", "-ar", "44100",
out_path,
]
subprocess.run(cmd, check=True, encoding="utf-8", errors="replace")
return out_path
def to_letterbox(
src: str,
out_path: str,
*,
canvas: Tuple[int, int] = (1080, 1920),
crf: int = 20,
) -> str:
"""가로 영상을 canvas(기본 9:16) 가운데 두고 위아래 검은 띠로 채운 mp4 생성.
scale=...:force_original_aspect_ratio=decrease 로 캔버스 안에 비율 유지하며 축소,
pad 로 가운데 정렬 + 나머지 검정. 전체 클립을 그대로(타임라인 보존) 재인코딩.
"""
cw, ch = canvas
vf = (
f"scale={cw}:{ch}:force_original_aspect_ratio=decrease,"
f"pad={cw}:{ch}:(ow-iw)/2:(oh-ih)/2:color=black,"
f"setsar=1"
)
cmd = [
"ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
"-i", src, "-vf", vf,
"-c:v", "libx264", "-preset", "veryfast", "-crf", str(crf),
"-pix_fmt", "yuv420p", "-c:a", "aac", "-ar", "44100",
out_path,
]
subprocess.run(cmd, check=True, encoding="utf-8", errors="replace")
return out_path