비디오 오버레이
원본 영상(rough cut)을 가져와 화자가 각 내용을 말하는 정확한 프레임에 떨어지는 애니메이션 오버레이 세트와 Resolve 가져오기 파일을 생성합니다.
여기서 브랜드 설정
scripts/scene.html 파일(상단의 :root 블록)에서 한 번만 수정하세요. 나머지는 모두 여기서 값을 읽어옵니다.
| 설정 | CSS 변수 | 기본값 |
|---|---|---|
| 강조 색상 | --brand-accent (+ --brand-accent-rgb 는 r,g,b 형식) | #4a90e2 |
| 밝은 텍스트 색상 | --brand-light (+ --brand-light-rgb) | #f2f2f2 |
| 어두운 잉크 색상 | --brand-ink | #0d131a |
| 표시용 글꼴 (헤드라인, 숫자) | --font-display | 'Anton','Bebas Neue',Impact,sans-serif |
| 본문 글꼴 | --font-body | 'Inter',-apple-system,sans-serif |
표시용 글꼴은 로컬에 설치되어 있어야 합니다 (예: ~/Library/Fonts). 클립은 헤드리스 Chrome에서 오프라인으로 렌더링되기 때문입니다. Anton과 Bebas Neue는 Google Fonts에서 무료로 제공됩니다. 코드 내 이름 BLUE는 단순히 "강조 색상"을 의미합니다.
하우스 스타일: 인씬(in-scene) 타입. 카드는 사용하지 않습니다. 텍스트는 화자의 머리 옆 공간에 위치하며, 큰 제목은 머리를 양옆으로 감싸고, 질문은 말할 때 그대로 타이핑되어 나타납니다. 항상 브랜드 색상과 표시용 글꼴을 사용합니다.
가장 중요한 규칙 하나: 그래픽을 화자의 머리 뒤나 옆에 배치하세요. 얼굴 앞에는 아무것도 두지 말고, 배경색도 사용하지 마세요. 다이어그램도 화자 옆 벽에 위치합니다. 모든 것은 매트 처리되어 손이 그래픽 앞을 지나갈 수 있습니다.
한 번 실행하면 생성되는 것: N개의 투명 .mov 클립, PLACEMENT.txt, 두 개의 .fcpxml 파일, 짧은 PREVIEW.mp4 파일.
기본적으로 사운드는 없습니다. 요청하지 않는 한 음악이나 음향 효과를 추가하지 마세요.
스타일 기본값 (취향에 따라 변경 가능)
- 말하기 전에는 아무것도 나타나지 않습니다. 모든 클립을 해당 문구의 단어 수준 타임스탬프에 약 0.12초를 더해 단어 직후에 나타나도록 고정합니다. 섹션 수준 타이밍은 이르게 읽힙니다.
- 짧게. 2.5초에서 6.5초 사이. 대부분 3초 내외. 소셜 미디어처럼 앞뒤로 컷이 전환되어야 하며, 오래 머물지 않아야 합니다.
- 머리 뒤나 옆에, 절대 화자 위에 배치하지 않음. 카드, 배경, 어둡게 처리하는 것 없음. 이로 인해 새 비디오에서는 코너 칩, 모든
full-*카드 및define타입이 사용 중지됩니다. - 여전히 메커니즘을 보여줍니다. 문장보다 다이어그램을 먼저 사용하세요. 화자 옆 벽에 위치하는 인씬 다이어그램 타입(
dots,flow,bars,compare,math)을 사용하세요. - 두 부분으로 나뉘는 것은 분할합니다. 예전 방식과 새 방식은 두 개의 클립입니다. 10%와 20%는 두 개의 클립입니다. 세 가지 옵션은 세 개의 클립이며, 각각 이름이 언급될 때 나타납니다.
- 모든 라인은 해당 단어에 맞춰 나타납니다. 목록, 사이드 라인, 타이핑된 단어는 균등한 간격이 아닌 단어 타임스탬프의 라인별 오프셋(
at,t)을 사용합니다.scripts/plan.example.py에 그 방법이 나와 있습니다. 문구를 인용하고, 큐 단어를 지정하면 조회를 수행합니다. - 좋은 혼합 (약 11분 동안 48개 클립): 약 1/4은 인씬 다이어그램(주로
flow), 그다음side,pair,type,list2,num순.pair는 핵심 요점(punchline)에 사용하세요. - 큰 제목은 머리 주위에 배치하고, 뒤에는 배치하지 않습니다.
pair는 각 라인을 단어 경계에서 두 부분으로 나누어 머리를 양옆으로 감싸며, 얼굴 상자 너비(머리카락과 귀 포함)의 1.9배 간격을 두고, 두 부분 모두 화면에 남도록 크기가 조정됩니다. 따라서 모든pair라인은 2개 이상의 단어가 필요합니다 ("ONE HOUR"은 가능하지만 "HOUR"은 불가능). 단일 단어는 분할할 수 없어 숨겨집니다.data에서"around": false를 설정하면 중앙 정렬 모양으로 돌아갑니다. 외곽선 라인은 150px에 5px 스트로크와 밝은 강조 채우기(35%)를 사용하여 가독성을 유지합니다.
시작하기 전에: 컷이 확정되었나요?
오버레이는 정확한 단어에 고정되므로, 이후에 다시 트리밍하면 모든 클립이 이동합니다. 사용자가 원본 푸티지를 제공하거나 컷이 확정되지 않은 경우, 먼저 paper-edit 스킬을 실행하고 그것이 생성한 타임라인에 오버레이를 배치하세요.
파이프라인
1. 컷을 가져와서 프로브
Resolve에서 내보내기(예: ~/Movies/로) 또는 종이 편집을 위해 원본 푸티지를 <YOUR_FOOTAGE_DRIVE>에 요청하세요. 비디오 파일이면 충분하며, 오디오를 별도로 내보낼 필요는 없습니다. Resolve가 열려 있으면 내보내기를 요청하는 대신 MCP를 통해 타임라인을 읽으세요.
ffprobe -v error -show_entries format=duration -show_entries stream=codec_type,r_frame_rate -of default=nw=1 "<file>"
프레임 속도를 기록하고 (렌더러는 23.976 = 24000/1001을 가정합니다. 다른 속도는 render.mjs에서 FPSN/FPSD를 변경하세요) 타임라인이 00:00:00:00에서 시작하는지 01:00:00:00에서 시작하는지 물어보세요 (Resolve 기본값은 01).
2. 단어 수준 타임스탬프로 전사
ffmpeg -y -i "<file>" -ac 1 -ar 16000 -c:a pcm_s16le cut.wav
whisper-cli -m ~/.cache/whisper-models/ggml-small.en.bin -f cut.wav -oj -ml 1 -sow -of cut
-ml 1 -sow는 세그먼트당 하나의 토큰을 제공하므로 정확한 고정이 가능합니다. 그런 다음 cut.json (transcription[].offsets.from은 밀리초)에서 토큰 실행과 일치시켜 각 문구의 시작 시간을 찾으세요.
3. 순간 선택
전체 대본을 먼저 읽으세요. 비디오 15초당 약 하나의 클립을 목표로 하세요. 각각에 대해: 고정 문구, 타입, 애니메이션이 안착되고 여운을 남길 수 있는 가장 짧은 지속 시간을 결정하세요.
내용은 기록된 노트가 아닌 실제로 말한 내용에 기반하세요. 노트에 "30분"이라고 되어 있고 카메라에서는 "30분 정도"라고 말했다면, 녹음 내용이 우선이며 차이점을 사용자에게 알리세요.
4. clips.json 작성
scripts/clips.example.json을 모델로 복사하세요. 클립당 하나의 객체:
{"id":"06-math-10","start":52.96,"dur":3.6,"type":"dots","cue":"\"10끼 중 1끼는 집밥이라면\"",
"data":{"lit":10,"pct":"10%","count":"10끼"}}
스킬 폴더 외부에 보관하고 (임시 경로로 충분함) CLIPS=로 전달하세요. start는 초 단위의 원시 문구 시간이며, 렌더러가 자체적으로 0.12초 리드를 추가합니다. id 순서가 파일 순서를 결정하므로 번호를 매기세요.
5. 클린 컷 렌더링 (인씬 타입에 필요)
인씬 타입은 머리 위치를 읽고 오버레이가 없는 컷의 복사본에서 화자를 분리합니다. 모든 오버레이 클립을 비활성화한 상태로 Resolve에서 확정된 타임라인을 H.264 1080p로 임시 경로에 렌더링하세요. API에서 Resolve 19는 SetTrackEnable on V2를 지원하지 않았습니다. 트랙의 각 클립에 대해 item.SetClipEnabled(False)를 사용했습니다. 원본 푸티지는 온라인 상태여야 하므로 푸티지 드라이브가 마운트되어 있어야 합니다. 그렇지 않은 경우 사용자에게 연결하고 마운트를 확인하라고 요청하세요. 완성된 마스터를 렌더링하지 마십시오 (이전 오버레이가 구워질 수 있음).
6. 렌더링
cd <skill>/scripts && npm i # 첫 번째만, puppeteer-core 설치
swiftc -O person.swift -o person # 첫 번째만, 매트 + 머리 찾기 컴파일
TITLE="<비디오 이름>" CLIPS=/path/to/clips.json FOOTAGE=/path/to/clean.mp4 OUT_DIR="$HOME/Movies/<비디오이름>-overlays" node render.mjs
# 하나 또는 몇 개의 클립만:
ONLY=06-math-10,07-math-30 OUT_DIR=... node render.mjs
오버레이 1초당 약 10~15초의 실제 시간이 소요됩니다 (pair는 모든 프레임을 매트 처리하므로 더 느림). 백그라운드에서 실행하고 진행 상황을 확인하는 것이 좋습니다. 마지막에 PLACEMENT.txt와 두 .fcpxml 파일을 작성합니다.
전체 렌더링 전에 항상 실제 영상으로 검증하세요. node proof.mjs <clean.mp4> <clips.json> out.jpg [email protected] [email protected] ...는 클립당 한 프레임(접두사@클립 시작 후 초)을 실제 샷 위에 매트를 포함하여 렌더링하고 하나의 이미지로 타일링합니다. 확인하세요. 프레임을 벗어난 텍스트, 눈을 가로지르는 제목, 복잡한 배경 위의 텍스트 등은 코드에서는 보이지 않지만 이미지에서는 명확합니다. 다이어그램의 경우 회색 위에 합성해도 괜찮습니다.
증명 프레임: 먼저 PNG로 추출하세요. qtrle .mov를 lavfi 컬러 소스에 -ss로 직접 오버레이하면 빈 프레임이 나옵니다. 프레임을 추출하고 (ffmpeg -ss 2.8 -i clip.mov -frames:v 1 f.png), 그런 다음 PNG를 합성하세요.
Resolve에 배치: 확정된 타임라인을 YouTube Final로 복제하고, mov 파일을 Overlays 빈으로 가져온 다음, 각각을 trackIndex: 2와 PLACEMENT.txt의 IN 시간 코드(((h*3600+m*60+s)*24+ff), 따라서 01:00:00:00 = 86400)의 recordFrame과 함께 AppendToTimeline하세요. PLACEMENT.txt를 한 줄씩 구문 분석하세요 (line.split()[0]은 id, [1]은 시간 코드). 전체 파일 정규식은 "60-second"와 같은 큐 텍스트를 클립 ID로 읽고 다음 줄의 시간 코드를 가져와서 한 클립이 조용히 배치되지 않을 수 있습니다. 배치된 수가 클립 수와 같은지 확인하세요.
7. 미리보기, 그다음 전달
사용자가 적용 전에 판단할 수 있도록 실제 영상 위에 10-15초 미리보기를 빌드하세요:
ffmpeg -y -ss <t> -t 11 -i "<cut>" -itsoffset <a> -i "<clip1>.mov" -itsoffset <b> -i "<clip2>.mov" \
-filter_complex "[0:v][1:v]overlay=0:0:eof_action=pass[v1];[v1][2:v]overlay=0:0:eof_action=pass[vo];[vo]scale=1280:-2[v]" \
-map "[v]" -map 0:a -c:v libx264 -crf 20 -preset veryfast -c:a aac PREVIEW.mp4
원본 오디오는 영상과 함께 제공되며, 이것이 미리보기에 필요한 전부입니다.
그런 다음 가져오기 단계를 알려주세요 (파일 > 가져오기 > 타임라인 > AAF, EDL, XML 가져오기, starts-01 파일 선택, 클립을 01:00:00:00의 영상 위 트랙에 복사).
Resolve 내부에서 직접 작업 (Resolve가 열려 있을 때 선호)
resolve MCP 서버가 설치된 경우 (예: 타사 barckley75/resolve-claude-mcp), Resolve Studio의 스크립팅 API와 통신합니다. paper-edit 스킬에 ensure_resolve.sh가 포함되어 있으면 먼저 실행하여 Resolve가 닫혀 있으면 실행합니다. Preferences > System > General > External scripting using: Local 설정으로 내보내기 및 가져오기 과정보다 선호합니다.
변경 사항:
- 내보낸 컷이 필요 없습니다. 열린 타임라인에서 실제 시간 코드, 프레임 속도, 시작 시간 코드 및 클립 레이아웃을 읽은 다음 오버레이를 고정합니다. 이는 "트리밍할 때마다 시간 코드가 이동하는" 문제를 해결합니다.
- 수동 가져오기 불필요. 렌더링된
.mov클립을 비디오 트랙에 직접 배치합니다. - 더 나은 순간 선택. 이미 있는 컷이 어디에 떨어지는지 확인하면 오버레이가 싸우는 대신 편집과 함께 배치될 수 있습니다.
- 오버레이가 준비되면 렌더링을 시작할 수도 있습니다.
사용 규칙:
- 쓰기 전에 읽으세요. 프로젝트 이름, 타임라인 이름, 시작 시간 코드 및 프레임 속도를 확인하고 사용자에게 다시 말해주세요.
- 마스터 타임라인에 쓰지 마세요. 먼저 복제하고 복사본에서 작업하여 실수해도 문제가 없도록 하세요.
- 일반 도구로 해결할 수 없는 경우가 아니면
execute_resolve_code를 피하세요. 이는 사용자 프로젝트에 대해 임의의 Python을 실행합니다. 유일한 방법이라면 코드를 먼저 보여주세요. - 로컬 세션만 가능합니다. Resolve는 이 Mac에서 실행 중이어야 하므로 클라우드나 예약된 실행에서는 작동하지 않습니다. 해당 경우 FCPXML로 대체하세요.
- FCPXML 경로는 여전히 작동하며 여전히 대체 수단입니다. 두 파일을 계속 생성하세요. 몇 초밖에 걸리지 않습니다.
오디오 전사: 일부 Resolve MCP 서버는 자체 mlx-whisper 전사를 제공하지만, 위의 whisper.cpp 단계는 이 파이프라인에서 검증되었습니다.
클립 타입
아래의 모든 것은 새 비디오에 대해 사용 중지됩니다 (이전 비디오를 다시 편집할 수 있도록 유지). 인씬 타입은 다음 섹션을 사용하세요. 전체 프레임 타입은 단색 잉크 배경에 부드러운 강조 광선을 칠하므로 화자를 완전히 가립니다. 칩은 왼쪽 아래에 위치합니다.
| 타입 | 용도 | data |
|---|---|---|
full-dots | 사용 중지됨, dots 사용 | lit, pct, count |
full-flow | 사용 중지됨, flow 사용 | label, steps[], good |
full-compare | 사용 중지됨, compare 사용 | title, left{label,value,sub}, right{...,hi}, vs |
full-bars | 사용 중지됨, bars 사용 | title, bars[{label,value,value_label,hi}] |
full-math | 사용 중지됨, math 사용 | parts[] (연산자 x = + → 강조 색상으로 렌더링), sub |
full-pack | 사용 중지됨, bars 또는 side 사용 | name, note, size 0-1, hi |
full-stat | 사용 중지됨, pair 사용 | label, value, sub, hi |
full-title | 사용 중지됨, pair 또는 type 사용 | title (<br> 허용), size, sub |
full-list | 사용 중지됨, list2 사용 | title, items[], step 초 |
chip | 사용 중지됨, side 또는 num 사용 | num, title, sub, hi |
chip-stack | 사용 중지됨, list2 사용 | items[] |
사용 중지된 타입은 이전 비디오를 다시 편집하기 위해 여전히 렌더링됩니다.
인씬 타입 (기본값)
카드 없음, 텍스트는 화자의 머리 옆 공간에 있으며, 큰 제목은 머리 주위에 배치됩니다.
| 타입 | 용도 | data |
|---|---|---|
pair | 핵심 요점 제목: 머리 위 외곽선 라인, 이마 높이 실선 | top, bottom, outline (top/bottom) |
side | 머리 옆 2-3개의 짧은 라인, 각각 해당 단어에 맞춰 나타남 | lines[], accent (인덱스, 크게 + 강조 색상), at[] |
type | 인용문이나 질문이 말할 때 단어별로 타이핑되어 나타남 | words[{w,t,hi,br}] |
define | 사용 중지됨: 샷을 어둡게 함. side 또는 pair 사용 | word, sub |
num | 외곽선 숫자가 그려지고, 옆에 단어가 나타남 ("01 주간 계획") | num, lines[] |
list2 | 머리 왼쪽 및/또는 오른쪽에 항목이 쌓임 | left[], right[], at[] (순서대로, 왼쪽 먼저) |
인씬 다이어그램 (동일한 벽 배치 및 매트):
| 타입 | 용도 | data |
|---|---|---|
dots | 100개 중 비율, 화자 옆에 그리드 채워짐 | lit, pct, count |
flow | 프로세스, 단계가 위에서 아래로 연결됨, 마지막 단계는 강조 색상 | label, steps[], at[] 선택 사항 |
bars | 크기별 두세 개의 숫자 | title, bars[{label,value,value_label,hi}] |
compare | 두 옵션, 머리 양쪽에 각각 하나씩 | left{label,value,sub}, right{...,hi} |
math | 벽을 따라 읽는 방정식, 연산자가 각 줄을 시작 | parts[], sub |
at 및 t는 클립 시작부터의 초입니다. 단어 타임스탬프에서 빌드하여 모든 라인이 해당 단어에 맞춰 나타나도록 하세요.
이것들은 FOOTAGE=가 필요합니다. 오버레이가 없는 컷 (오버레이 클립을 비활성화한 상태로 Resolve에서 타임라인 렌더링). 렌더러는 각 클립에 대해 얼굴 상자와 배경 밝기 그리드를 읽어 텍스트가 빈 벽을 찾고, 프레임 밖으로 나가면 측면을 바꾸거나 크기를 줄이며, 밝은 배경에는 어두운 잉크, 어두운 배경에는 밝은 텍스트로 전환합니다. 모든 인씬 클립의 모든 프레임에서 화자를 매트 처리합니다 (Apple Vision person segmentation, person.swift). 따라서 제목은 머리 뒤에 앉고 손은 그래픽 앞을 지나갑니다. 한 번 컴파일: swiftc -O person.swift -o person. FOOTAGE 없이 pair를 렌더링하면 빨간색 배경에 텍스트가 표시되며, 기술적으로는 작동하지만 증명 용도로만 사용하세요.
시작하기 위한 명령어 요약
# 환경 설정 (처음 한 번)
cd <skill>/scripts && npm i
swiftc -O person.swift -o person
# 단어별 전사
ffmpeg -y -i "<input.mp4>" -ac 1 -ar 16000 -c:a pcm_s16le cut.wav
whisper-cli -m ~/.cache/whisper-models/ggml-small.en.bin -f cut.wav -oj -ml 1 -sow -of cut
# 증명 프레임 렌더링
node proof.mjs <clean.mp4> <clips.json> proof.jpg [email protected] [email protected]
# 최종 렌더링
TITLE="비디오 제목" CLIPS=/path/to/clips.json FOOTAGE=/path/to/clean.mp4 OUT_DIR="$HOME/Movies/오버레이" node render.mjs
# 미리보기 빌드
ffmpeg -y -ss 10 -t 11 -i "<cut>" -itsoffset 0 -i "<clip1>.mov" ... PREVIEW.mp4