Communitygithub.com

promo-video — 零成本宣傳片 / 廣告管線

무료로 프로젝트를 프로모션 영상, 트레일러, 소셜 광고, 설명 영상으로 바꿔주는 Claude Code 스킬입니다. 화면 녹화, Ken Burns 몽타주, 터미널 내레이터 모드를 지원하며, 유료 API 없이 웹 프로젝트 출시 광고를 제작할 수 있습니다.

廣告管線란 무엇인가요?

이 스킬은 웹 앱, 게임, 이미지 갤러리, CLI/라이브러리 등 모든 유형의 저장소를 위한 무료 프로모션 비디오를 제작하는 Claude Code 스킬입니다. 핵심 아이디어는 '앱이 스스로 영화처럼 움직이면 화면을 녹화하고, 이미지 프로젝트면 Ken Burns 몽타주를 만든다'는 것입니다. Playwright, xvfb, ffmpeg, edge-tts, Gemini 등 무료 도구만 사용하여 16:9, 9:16, 1:1, 4:5 등 다양한 비율의 영상을 내레이션, 자막, 브랜드 태그, CTA와 함께 출력합니다. CLI/라이브러리 같은 비시각적 프로젝트를 위해 터미널 타이핑 애니메이션 녹화 모드도 제공합니다. 이 스킬은 실전에서 검증되었으며, 60분 분량의 '글리치와 블랙홀 씨' 전체 7장 플레이 영상(https://youtu.be/J9OMebCjr9Y)을 자동 녹화한 사례가 있습니다. 또한, 화면 녹화 시 발생할 수 있는 40개 이상의 실제 함정(GOTCHAS)을 문서화하여 영상 제작 과정에서 발생할 수 있는 문제를 사전에 방지합니다.

지원 대상✓Claude Code✓Codex CLI~Cursor✓Gemini CLI
npx skills add yazelin/promo-video-skill

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

廣告管線은(는) 무엇을 하나요?

웹 프로젝트를 프로모션 영상이나 소셜 광고로 바꿔줍니다. 핵심 인사이트: 앱이 스스로 영화처럼 연출된다면 화면을 녹화하고, '생성'하지 마세요. 이미지 프로젝트라면 엄선된 이미지를 다운로드하여 Ken Burns 몽타주를 만드세요. 나머지(내레이션 / 음성 검증 / 합성 / 브랜드 / CTA)는 두 모드가 공유합니다. 실전 검증: battlefield-editor/tools/trailer(화면 녹화) + catime 광고 '세상을 읽는 고양이'(Ken Burns + 뉴스→이미지 공개) + '글리치와 블랙홀 씨' 전 7장 전체 플레이(화면 녹음의 유성 변형, 60분, 700개 대사 자동 진행: https://youtu.be/J9OMebCjr9Y ).

의사 결정 트리: 먼저 캡처 모드를 선택하세요

  • 움직이는 앱(3D/canvas/자동 연출, auto-play 포함) → MODE A 화면 녹화(capture-app.mjs).
  • 이미지 출력 / 갤러리 / 사진 피드(AI 생성 이미지, 갤러리) → MODE B Ken Burns 몽타주(엄선 이미지 다운로드 + kenburns.sh).
  • 앱 자체에서 소리가 나는 경우(MODE A의 유성 변형; 무엇을, 얼마나 재생할지는 앱이 실시간으로 결정, 전체 오디오 트랙이 없음) → recordVideo를 포기하고 ffmpeg로 화면과 오디오를 동시에 캡처하세요. 자세한 내용은 '앱 자체에서 소리가 나는 경우: 실제 녹화' 섹션을 참조하세요. 전체 오디오 트랙이 있다면 이 방법을 사용하지 마세요. 사후에 먹싱하는 것이 음질이 더 좋습니다(GOTCHA 24).
  • 비시각적 저장소(CLI / 라이브러리 / API / 백엔드 / 데이터 파이프라인, 녹화할 화면 없음) → MODE C 터미널/해설(term-player.html + capture-term.mjs): '명령어 + 실제 출력'을 타이핑 애니메이션 터미널 녹화로 만듭니다. Dev 도구는 '빠른 컷 예고편'이 아닌 '내레이션 교육 버전'이어야 합니다(GOTCHA 22 참조). 구조 = 문제점 hook → 단계별(설치→실행→출력 이해→고급 명령어) + edge-tts 내레이션으로 단계별 설명 + 단계 자막 → CTA. 내레이션 타이밍은 term-player.html의 window.__STEPLOG(각 단계 출력이 나타나는 초)를 사용하며, compose는 각 내레이션 줄을 해당 단계에 adelay합니다. 실전: wip 교차-저장소 상태 CLI 교육 영상(~60초, 배경 음악 볼륨 0.13으로 내레이션 위주).
  • 라이브 녹화의 명언 숏폼(9:16 Reels/Shorts) → MODE E 템플릿 녹화(short-9x16.html + capture-short.mjs): course-pipeline이 먼저 대본에서 명언을 선별하고 16:9 클립을 자릅니다. 여기서는 그것을 9:16 HTML 템플릿에 삽입한 다음 녹화합니다. 왜 HTML을 사용하고 ffmpeg 필터를 사용하지 않는가?: 자르기는 ffmpeg로도 가능하지만, 키워드 애니메이션 효과는 CSS에서 10줄이면 끝나지만, ASS 자막 형식으로 동일한 효과를 구현하는 것은 지옥입니다. 레이아웃을 조정하는 것도 CSS를 한 번 보고 수정하면 되지만, filtergraph를 맹목적으로 수정해야 하는 것은 아닙니다. 경계: course-pipeline이 자르고, promo-video가 패키징합니다. 인터페이스는 '잘라낸 mp4 + 완성된 타임라인의 단어별 타임스탬프'입니다.
  • 혼합: screen-record를 본체로 하고 Ken Burns / 터미널을 보충 자료로 사용하는 것도 가능합니다.

4단계

  1. 캡처 — MODE A: xvfb-run으로 headed Chromium 실행(WebGL 필요), Playwright recordVideo로 자동 재생의 하이라이트 구간 녹화; MODE E는 headless로 진행(GOTCHA 47 참조). MODE B: 소스 목록 가져오기, 선별, 다운로드.
  2. 내레이션(선택 사항) — edge-tts zh-TW-YunJheNeural --rate=-5%로 내레이션 생성.
  3. 검증(선택 사항) — Gemini pro가 대신 들어서 단어별 어조 검증; Gemini flash가 배경 음악 선별.
  4. 구성 — brand.py로 브랜드 자료 생성(자막 카드/모서리 로고/CTA/QR), ffmpeg로 자막 오버레이/연결/오디오 믹싱, 여러 비율 + 요약 버전 출력.

여기 있는 여섯 개의 템플릿(상단 config / 매개변수만 바꾸면 사용 가능)

  • brand.py — PIL:cover/contain 비율 유지 합성기, 자막 카드, 상시 모서리 로고, CTA 카드, 미니 갤러리, QR(API 사용).
  • kenburns.sh — 떨림 없는 Ken Burns 클립 + 텍스트가 그림자에 닿지 않도록 + concat + 오디오 믹싱(MODE B 주력).
  • capture-app.mjs — Playwright+xvfb 화면 녹화(MODE A); 내장 http 서버, HUD 숨기기, 노드 클릭으로 구간 이동 / 키보드로 게임 구동.
  • term-player.html + capture-term.mjs — MODE C 터미널/해설: HTML 터미널 플레이어(타이핑 애니메이션+컬러 출력) + xvfb 녹화. asciinema/xterm 불필요.
  • short-9x16.html + capture-short.mjs — MODE E:9:16 템플릿(슬라이드 중앙 정렬, 상하 대자막, 명언 글자 확대 및 진동, 상시 모서리 로고, 진행 표시줄) + xvfb 녹화 + 사후 원본 오디오 먹싱. 슬라이드 한 칸은 자동 연출: 화면 자체에서 글자의 위치를 측정하여 2-3초마다 구도를 바꾸고, 키워드가 읽히는 순간에 클로즈업 및 밝기 증가(GOTCHA 32-37). 재녹화 시 --tag v2로 파일명을 변경하고, 이전 완성본을 덮어쓰지 마세요. 이전 영상이 비교 기준입니다. 녹화가 완료되면 자동으로 '밝기 이벤트 횟수/초'와 '완성품의 음량'을 검증하므로, 별도로 확인할 필요가 없습니다.
  • relevel.py — 이미 출시된 영상의 음량을 보정할 때 사용: 오디오만 수정하고, -c:v copy로 비디오 스트림은 그대로 유지합니다. 실측 결과 전후 비디오 스트림의 md5가 동일하며, 1초면 완료되므로 재녹화할 필요가 없습니다. 새로 녹화하는 영상에는 필요 없습니다(capture-short.mjs가 먹싱 시 이미 보정함).
  • audition.py — Gemini가 대신 듣기:music "<용도>" *.mp3(배경 음악 선별 fit 0-10), tone "<텍스트>" v.mp3(내레이션 어조 검증).

'앱 자체에서 소리가 나는 경우': 실제 녹화

GOTCHA 24는 전체 오디오 트랙이 있는 경우(음악 비디오, edge-tts 내레이션)에 대해 설명합니다: 원본 파일을 사후에 먹싱하는 것이 음질이 가장 좋습니다. 하지만 한 가지 경우는 그렇게 할 수 없습니다——무엇을, 얼마나 재생할지는 앱이 실시간으로 결정하는 경우입니다. 실전: 비주얼 노벨의 전체 플레이, 700개의 더빙 파일이 스토리 순서대로 재생되며, 정렬할 수 있는 '전체 오디오 트랙'이 전혀 없습니다.

그렇다면 Playwright의 recordVideo를 완전히 포기하고, ffmpeg로 화면과 오디오를 동시에 캡처하세요:

# 1. 별도의 오디오 수신 단자를 열고, 브라우저의 사운드를 거기로 보내고, 데스크탑의 다른 소리는 섞지 않음
MOD=$(pactl load-module module-null-sink sink_name=cap)
# 2. 자체 X 화면
Xvfb :77 -screen 0 1280x808x24 -nolisten tcp &
# 3. 화면 + 사운드를 함께 녹화, 분할 파일로 저장(중단되어도 한 부분만 손실)
ffmpeg -f x11grab -draw_mouse 0 -framerate 25 -video_size 1280x808 -i :77.0 \
       -f pulse -i cap.monitor \
       -vf crop=1280:720:0:88 \
       -c:v libx264 -preset veryfast -crf 24 -pix_fmt yuv420p \
       -c:a aac -b:a 160k \
       -f segment -segment_time 600 -reset_timestamps 1 seg/part-%03d.mp4 &
# 4. 브라우저 구동(DISPLAY=:77 PULSE_SINK=cap), 완료 후 concat으로 연결

5가지 실제 테스트에서 발견한 점:

  1. --kiosk는 Playwright에 적용되지 않습니다, Playwright가 자체적으로 창을 관리합니다. 탭 표시줄과 주소 표시줄이 고정 88px이므로, 화면을 88px 더 크게 열고 crop으로 잘라냅니다. 커서는 -draw_mouse 0으로 숨깁니다.
  2. 페이지 넘김에 고정 시간을 사용하지 마세요. 각 대사 길이가 다르므로, 너무 짧게 기다리면 끝부분이 잘리고, 너무 길게 기다리면 전체 영상 길이가 두 배로 늘어납니다. new Audio()를 가로채서, 음성이 끝난 후에야 다음 장을 클릭합니다(해당 요소들은 DOM에 없어서 querySelector로 찾을 수 없습니다).
  3. '내용이 진행되었는지' 확인하는 검사가 반드시 있어야 합니다. 그렇지 않으면, 클릭할 수 없는 카드를 만나면 계속해서 빈 클릭을 하게 되고, ffmpeg는 어떤 오류도 보고하지 않습니다: 실제로 58분 동안 녹화했고, 파일은 정상이었지만, 결국 책의 4분의 1만 진행된 것을 발견했습니다. 판단 기준은 '화면의 텍스트가 변경되었는지'이며, 텍스트가 비어 있는 경우도 진행되지 않은 것으로 간주해야 합니다——종료 화면에는 텍스트가 없으며, 이 조건을 놓치면 추가로 33분이 더 녹화됩니다.
  4. 분할 파일 저장(-f segment), 중간에 중단되어도 한 부분만 손실되며, 마지막에 concat -c copy로 무손실 연결합니다.
  5. 완료 후 네 가지를 확인해야 합니다. 파일이 생성된 것만으로는 안 됩니다: 전체 길이, 몇 장의 카드를 거쳤는지, 샘플링한 프레임의 표준 편차(0에 가까우면 = 빈 화면), volumedetect의 평균 볼륨(무음은 null sink가 연결되지 않았음을 의미하며, 역시 오류를 보고하지 않습니다).

실전: glitch-vn/tools/capture/(record.sh + play_through.mjs). 완성품은 '글리치와 블랙홀 씨' 전 7장 전체 플레이, 60분, 700개 더빙, 한 번에 모두 진행되며 옆에서 누르는 사람이 없음: https://youtu.be/J9OMebCjr9Y

GOTCHAS — 이 부분이 가장 가치 있는 자산입니다. 매번 이것을 확인하세요

  1. 비율 유지: 절대로 혼합 비율의 이미지에 scale=W:H를 사용하지 마세요(늘어나거나 찌그러집니다). 배경은 cover(비율 유지하며 확대 후 자르기), 본체는 contain(비율 유지하며 축소하여 배치)을 사용하세요. ffmpeg의 scale은 정수 배수(2x)이거나 force_original_aspect_ratio를 추가해야만 합니다. 완료 전에 '원본 비율 vs 표시 비율' 감사를 실행하세요(오차 <0.1%여야 통과).

  2. Ken Burns 떨림: 원본을 먼저 2x 확대한 후 축소(정수 픽셀 점프가 서브 픽셀로 변경됨) + 확대/축소에 선형 프레임 번호 z='1+K*on/(D-1)' 사용, 절대로 누적 반올림이 발생하는 z='min(zoom+inc,...)' 사용 금지.

  3. zoompan 프레임 폭발: -loop 1 -t 사용 금지(각 입력 프레임이 ×d로 곱해짐); 단일 이미지 -frames:v D + zoompan d=D.

  4. 텍스트가 그림자에 닿지 않도록: 이미지를 먼저 세로 중앙 정렬; 텍스트 위치 = 중심 ± (이미지 높이 × zmax)/2 ± 간격, 이미지별로 계산(PIL이 각 이미지 높이를 sidecar로 출력). 이렇게 하면 가로 이미지가 너무 멀리 떨어지지도 않고, 눌리지도 않습니다.

  5. file:// fetch 차단: 앱이 fetch(?pkg=)/JSON 로드 → python3 -m http.server로 http 실행(또는 capture-app.mjs 내장 서버 사용).

  6. headless WebGL에서 swiftshader 누락/완전 검은 화면 → xvfb-run -a --server-args="-screen 0 WxHx24"로 headed Chromium 실행.

  7. ES-module 앱: gotoPhase가 window에 없음 → DOM으로 타임라인 노드 #tlNodes .node[i].click()를 클릭하고, evaluate(gotoPhase) 사용 금지.

  8. QR:PEP668로 qrcode를 설치할 수 없음 → api.qrserver.com/v1/create-qr-code/?data=<공개 링크>(공개 링크만 사용); 출시 전에 휴대폰으로 한 번 스캔하여 확인.

  9. edge-tts 파자: 먼저 회피('没 mò'→단어 변경하여 피함); 어조 검증은 Gemini pro에 음높이 방향을 질문, whisper STT는 어조를 검증할 수 없음(잘못 읽어도 동일한 문자로 변환됨).

  10. 배경 음악:Gemini flash가 CC0 선별, 분위기에 맞는지 확인(전쟁 음악을 고양이에 적용하지 마세요); CC0 출처 기록.

  11. 다중 비율:16:9=YouTube, 9:16=Reels/Shorts/LINE, 1:1 & 4:5=FB/IG 피드. MODE A는 각 비율별로 네이티브 재녹화(세로 viewport); MODE B는 각 비율별로 재합성.

  12. 상시 모서리 로고(채널 로고):브랜드+URL이 전체 영상에서 화면 모서리에 위치(끝 카드뿐만 아니라), 전체 영상 위에 투명 PNG 한 장을 오버레이.

  13. 광고는 행동을 유도해야 함:처음 2초는 후크(음소거 자동 재생, 첫 프레임부터 화면/큰 글자가 보여야 함), 자막 내장(FB 음소거), CTA에 '지금 바로' 할 이유를 제시.

  14. 플랫폼 세부 사항:FB 영상은 게시물에, 링크는 첫 번째 댓글에(도달률 저하 방지); +movflags +faststart; LINE 임베디드 재생은 앨범/영상 버튼으로 전송해야 하며, '파일'로 전송하면 안 됩니다.

  15. 스크린샷은 로딩 완료 후 + 각각 빈 화면이 아닌지 확인:MODE A/gallery 항목 스크린샷 고정 시간 사용 금지(느린 것은 '로딩 중' 빈 화면을 캡처할 수 있음). 로딩 표시기가 사라질 때까지 폴링한 후에 캡처; 캡처 후 각각 목표 영역의 stdev 측정(≈0 = 빈 화면/렌더링 안 됨), 자동으로 누락 감지 및 재캡처. 실전: roll-formosa Miaoli가 고정 1.8초로 공백 캡처, wait-for-load + stdev 스캔으로 복구.

  16. 실제로 확대하여 확인하고, 썸네일만 믿지 마세요:검증 시 반드시 전체 해상도 단일 프레임을 자세히 확인(자르기/흐릿한 가장자리/잘림은 썸네일에서 보이지 않음). 좌표를 측정할 때 원본 이미지에 y/x 격자를 그리세요. 실전: 랜드마크 하단이 55px 잘리고, 하반부가 흐릿하게 나타났으며, 모두 썸네일에서는 보이지 않고 확대하여 격자를 그려야 발견할 수 있었습니다.

  17. 인터랙티브 게임에 auto-play 없음 → 키보드로 직접 플레이(MODE A의 하위 모드):Katamari/액션 게임은 Playwright keyboard.down/up으로 구동(방향키로 굴림). 게임은 로드 후 ~10초(precache/PREPARING) 후에야 실제로 시작됨: 먼저 wait-until-START(버튼 준비 상태 폴링)→ START 클릭 → 키 입력 전송. 블라인드 봇이 반드시 무언가를 먹거나 성장하는 것은 아님(roll-formosa 가로 모드는 2cm에서 멈춤)——'가장 화면이 좋은' 창(공이 충분히 크거나/객체가 많음)을 캡처하거나, 다음 쇼케이스로 대체.

  18. gameplay 촬영이 어려움 → 자동 쇼케이스/gallery를 자료로 사용:많은 앱에 '객체 도감/미리보기' 자동 페이지가 있음(예: preview.html?city=X), 플레이하지 않고도 모든 콘텐츠를 렌더링하므로 신뢰할 수 있는 몽타주 소스임(roll-formosa 20개 도시 랜드마크가 이에 해당). 이러한 어두운 배경 객체는 순수한 어두운 배경을 사용하고, 배경을 흐리게 하지 마세요(그렇지 않으면 유령 이미지가 발생).

  19. '준비 완료' 화면이 녹화 후반부에 나타남:recordVideo는 로드부터 녹화를 시작하므로, PREPARING→START의 준비 완료는 후반부에 나타남; 충분히 길게 녹화(START까지 기다린 후 + 3초 더 정지)하고, 후반부에서 준비 완료된 부분을 트림하고, 처음부터 자르지 마세요(로딩 화면이 잘림).

  20. 휴대폰 세로 viewport가 더 나은 경우가 많음:게임의 모바일 레이아웃이 때로는 더 꽉 차거나, 심지어 동작이 다를 수 있음(roll-formosa 세로 gameplay에서는 공이 자라고, 가로 모드에서는 멈춤). 각 비율을 네이티브로 재녹화할 때, 어떤 viewport가 가장 좋은 성능을 보이는지 함께 확인하세요.

  21. 카피의 정확성:주인공/객체 소개는 개성/기벽을 사용하고 외형은 사용하지 마세요(catime 캐스팅); 광고의 '뉴스→이미지' 한 줄은 소스 자료의 아이디어/스토리 필드에서 추론하고, 임의로 상상하지 마세요(잘못된 뉴스를 연결하면 창피해집니다).

  22. 예고편 ≠ 교육, 형식을 혼동하지 마세요:시각적 프로젝트(게임/앱)는 15-20초 빠른 컷 예고편(tease, 관심 유도)에 적합함; 하지만 CLI/dev 도구를 빠른 컷 예고편으로 만들면 매우 지루하고 교육적인 느낌이 없음——~60-90초 내레이션 교육으로 만들어야 함: 문제점 hook →설치→실행→출력 이해→고급 단계별 + edge-tts가 단계별로 설명(천천히, 각 단계에서 충분히 정지) + 단계 자막 → CTA, 배경 음악 볼륨을 매우 낮게(~0.13) 설정하여 내레이션이 주가 되도록 함. 실전: wip 첫 번째 버전 빠른 컷이 지루하다는 평을 받아, 내레이션 교육 버전으로 변경한 후에야 '이해했고, 사용할 수 있겠다'는 실질적인 느낌을 얻음. 내레이션 타이밍은 term-player의 __STEPLOG를 사용. term-player는 각 단계마다 화면을 지워야 함(GOTCHA 16의 동일 출처 문제: 내용이 누적되면 화면 밖으로 밀려나고, 설명되는 명령어가 밀려남).

  23. 먼저 전체 README/--help를 스캔하여 기능 목록을 만든 다음, 스크립트를 작성하세요:도구 교육/해설을 만들 때 처음 부분만 읽고 촬영을 시작하지 마세요——주요 기능을 놓칠 수 있음(wip에서 dashboard.py 시각적 대시보드, wip done, --root, install-skill, Codex 지원, see: 계획 파일 감지 누락). 먼저 모든 기능을 나열한 다음, 시연할 기능을 선택하세요. TTS로 계정/handle/드문 철자를 억지로 읽게 하지 마세요(예: yazelin은 사실 중국어 이름 '야쩌린'의 병음이며, 한 글자씩 읽는 것은 이상함)——화면에 이미 URL이 있으면 내레이션으로 '링크는 아래에 있습니다'라고 간단히 언급하세요; 시작하기 어려운 단어는 피하세요('먼저…' → '…는 매우 간단합니다'). edge-tts로 일반 영어 용어(cargo/markdown/Claude Code)를 읽는 것은 오히려 괜찮으므로, Gemini flash로 한 문장을 들어 확인하세요.

  24. MODE D 음악 비디오:전체 노래 + 인터랙션, 녹화하지 말고, 사후에 원본 파일을 먹싱하세요. Playwright의 recordVideo는 오디오 트랙과 커서를 포함하지 않습니다(전체 오디오 트랙이 없는 경우 '실제 녹화' 섹션 참조). 따라서: (a) 사운드는 원본 mp3를 사후에 먹싱(음질이 가장 좋으며, xvfb의 null sink 영향을 받지 않음); (b) 가짜 커서를 주입(pointermove를 따라 움직이는 이미지를 한 장 붙이고, 녹화 전용이며 저장소에는 포함하지 않음). 그렇지 않으면 '마우스 상호 작용'이 영상에서 단지 효과가 저절로 움직이는 것처럼 보입니다. 액션 타임라인은 audio.currentTime 폴링(waitForFunction(t => au.currentTime >= X))에 바인딩해야 하고, waitForTimeout 누적을 사용하지 마세요——녹화기가 프레임을 드롭하면 벽 시계가 흐르지만, 오디오 시계는 흐르지 않습니다.

  25. 타이밍은 화면 속의 시계에 의존하고, node의 Date.now()에 의존하지 마세요. webm의 첫 프레임과 newContext() 사이에는 불확실한 차이가 있으며, Playwright는 끝부분에 많은 양의 정지 프레임을 추가합니다(실측 160초 콘텐츠가 239초 파일로 기록됨). ffprobe의 duration을 길이로 사용할 수 없습니다. 방법: 진행 표시줄 부분을 crop하고, 프레임별로 슬라이더의 x를 측정(가장 높은 분홍색 픽셀 열을 피크로 취한 다음 중심을 구함——지나가는 입자도 같은 색이므로 단순 평균을 내면 값이 왜곡됨), 화면 시간 → 오디오 시간에 대한 선형 피팅을 수행하여 오디오 시작점을 해결합니다. 세 가지 추정 방법(슬라이더 피팅, 가사 줄바꿈 시점, 클릭 당시의 벽 시계)을 교차 검증하여 중앙값을 취함; 가사 줄은 CSS 전환으로 인해 약 0.1초 느리므로, 이 값을 빼야 합니다. 완료 전에 상호 상관을 사용하여 완성된 오디오 트랙과 원본 파일 간의 지연을 확인합니다(AAC 프라이밍으로 인해 일반적으로 약 40ms 고정 지연, 정상). 그런 다음 '줄바꿈에서 약간 떨어진' 세 개의 프레임을 추출하여 올바른 문장이 강조되었는지 확인합니다.

  26. 세로(9:16)는 브라우저 확대/축소로 해상도를 높이려고 하지 마세요. recordVideo는 'CSS 크기의 화면'을 원하는 프레임 크기에 붙여넣습니다: viewport 405×720에 recordVideo.size 1080×1920을 설정하면, 결과는 405×720 콘텐츠가 왼쪽 상단에 붙여지고 나머지는 회색으로 채워집니다; deviceScaleFactor를 사용하여 장치 픽셀을 높이려고 하면, Playwright의 viewport 시뮬레이션이 DSF를 1로 되돌립니다; --force-device-scale-factor + --window-size를 사용하세요. xvfb에는 창 관리자가 없으므로, --window-size, Browser.setWindowBounds, --kiosk는 모두 창 크기를 변경할 수 없습니다(innerWidth가 501로 고정됨). html{zoom:...}도 작동하지 않으며, media query는 여전히 원래 너비를 읽습니다. 실행 가능한 해결책: viewport를 네이티브 1080×1920(DSF 1)으로 직접 열고, '확대된 세로 스타일'을 주입합니다(모바일 버전의 레이아웃 + 글꼴 크기/컨트롤을 비율에 맞게 확대, !important로 데스크톱 버전의 grid를 재정의). 녹화 전용 스타일은 저장소에 포함되지 않으며, HIDE와 동일한 위치에 주입됩니다. 글자가 흐릿한지 여부는 확대하여 확인해야 합니다(GOTCHA 16): 405에서 1080으로 늘어난 글자의 가장자리는 부드럽고, 네이티브 렌더링은 선명합니다.

  27. 가능하면 '화면이 스스로 말하게' 하세요:인터랙티브 유형의 녹화에서는, 시연할 작업을 타임라인으로 편성(클릭, 드래그, 정지)하고, 중간에 의도적으로 완전히 움직이지 않는 구간을 남겨서 자동으로 숨겨지는 UI가 사라지도록 하세요——계속 움직이면 오히려 시청자가 요점을 보지 못합니다. 인터랙션 녹화 시 recordVideo는 커서를 포함하지 않으므로, 가짜 커서를 주입하세요(pointermove를 따라 움직이는 이미지). 그렇지 않으면 시청자는 단지 효과가 저절로 움직이는 것처럼 봅니다.

  28. 타이밍 앵커는 '하얀 섬광'을 사용하고, '완전 검정'을 사용하지 마세요. 아이디어는 '페이지를 먼저 완전 검정으로 만들고, 비디오가 실제로 재생되기 시작할 때만 표시하며, 첫 번째로 밝아지는 프레임을 t=0으로 스캔한다'는 것입니다——실측 결과 성립하지 않음:Playwright의 recordVideo는 첫 번째 프레임부터 수집하지 않으며, 전체 녹화에서 가장 어두운 프레임은 24.4/255로, 인식할 수 있는 검은 프레임이 전혀 없습니다. 대신 재생 시작 전에 400ms 동안 완전 흰색을 표시하고, 재생이 시작되는 순간에 제거한 다음, '흰색 섬광이 끝난 다음 프레임'을 스캔하세요. 흰색 250은 콘텐츠의 24-37과 명확히 구분됩니다. 오프셋은 전체적으로 ±0.05초로 검증되었으며, 드리프트가 없습니다.

  29. metadata=print는 기본적으로 stderr에 기록됩니다. ffmpeg -vf "...,metadata=print:key=X"의 출력이 stdout에 없으므로, 스크립트가 stdout만 수집하면 영원히 빈 값을 읽게 되고, '읽을 수 없음'을 '화면이 검은색'으로 잘못 판단합니다. :file=-를 추가하세요.

  30. 내장 http 서버는 먼저 파일을 읽은 다음 헤더를 전송해야 합니다. res.writeHead(200)이 await readFile 앞에 작성되면, 파일이 없을 때 200이 이미 전송되었고, catch에서 404를 보충하려고 하면 ERR_HTTP_HEADERS_SENT가 발생합니다—— 실제 원인(경로 오류)이 이 가짜 오류로 가려집니다. 이 저장소의 capture-app.mjs에는 원래 이 함정이 있었으며, 수정되었습니다.

  31. A/V 동기화 검증은 식별력 있는 측정 방법을 사용해야 합니다. '동일한 시점의 프레임을 추출하여 상관 관계 비교'는 합리적으로 들리지만, 슬라이드 형식의 콘텐츠는 수십 초 동안 화면이 바뀌지 않으므로, 음성 대조(의도적으로 6초 차이)도 0.996이 되어 검증 효과가 없습니다. 가능한 방법: 템플릿에 진행 표시줄(너비 = currentTime / duration)을 배치하고, 완성된 영상에서 표시줄 너비를 측정하여 플레이어의 내부 시간을 읽어와서, 출력 시간과 직접 뺍니다. 이 표시줄은 완성된 영상의 시각적 요소로도 사용되며, 테스트를 위해서만 추가되는 것은 아닙니다.

  32. 정지된 슬라이드의 9:16 숏폼 영상, 화면이 죽어 있습니다——그리고 측정 가능합니다. 슬라이드 한 칸의 프레임별 그레이스케일 차이는 실제로 0.072 / 0.092에 불과하며(정지 대조군 0.000), 사용자 피드백은 '너무 평범하고, 펀치감이 부족하다'입니다. 임계값을 감으로 정하지 말고, 직접 출시한 9:16 작품을 동일한 기준으로 측정하여 사용 가능한 범위를 찾으세요: neko-tensei 음악 비디오 0.233, k-rider 예고편 1.045, battlefield-editor 1.293, roll-formosa 1.912, catime 광고 3.965, catime 예고편 6.168(active_ratio 0.636 = 빠른 컷). 설명형 명언 숏폼은 빠른 컷으로 만들어서는 안 됩니다. 목표 범위 0.30 ~ 1.05, active_ratio 0.10 ~ 0.40 ——이 임계값 세트는 폐기됨: mean과 active_ratio 모두 프레임 레이트로 정규화되지 않았으며, 자신의 작품을 사용하여 자신의 임계값을 설정하는 것은 순환 논증입니다. 현재 판단 기준은 punch-score.py의 motion.slide.per_s ≥ 5.5 (초당 그레이스케일 변화량, mean × fps, 프레임 레이트에 따라 변하지 않는 유일한 숫자), 상한은 설정하지 않으며, 위험한 '섬광'은 GOTCHA 35의 밝기 이벤트로 차단합니다. 근거: 사용자가 '화면이 죽었다'고 말한 v1은 1.79와 2.30이었으며, 가장 조용한 출시 작품인 neko-tensei는 5.82였고 반드시 통과해야 합니다.

  33. 정지된 슬라이드의 카메라 움직임은 화면 자체에서 측정해야 하며, 고정 비율로 경도를 지정해서는 안 됩니다. 960×720 슬라이드의 실제 측정 결과 250개의 스캔 라인에만 텍스트가 있고, 나머지는 공백입니다; 고정된 상/중/하 자르기는 페이지 상단의 작은 글자 줄이나 완전한 공백을 자를 수 있습니다. 방법: canvas에서 한 프레임 추출 → 각 행의 잉크 양(배경 중앙값과의 차이) → 밴드로 연결 → 잉크 양이 12% 미만인 밴드 폐기 → 남은 밴드의 좌우 극값이 텍스트 범위 SP이며, 카메라는 아무리 움직여도 SP를 잘라서는 안 됩니다. 측정할 때 축소하지 마세요: 8배 축소(120×90)하여 측정하면, drawImage의 축소판은 면적 평균이 아니며, 가는 획이 직접 사라짐(텍스트 오른쪽 끝의 '的路径。'를 측정할 수 없어 SP가 96px 부족하게 계산됨). 배경 그라데이션은 가로로 관통하는 것으로 간주됩니다. 임계값을 아무리 조정해도 문제가 해결되지 않았으며, 원본 해상도를 직접 사용하면 이 문제가 없었고, '잉크'는 검증과 동일한 정의(배경 중앙값보다 28 높음)를 사용할 수 있었습니다. 960×720을 한 번에 스캔하는 것은 headless Chromium에서 수십 밀리초밖에 걸리지 않습니다.

  34. 구도 잡기는 '현재 재생 중인 프레임'을 측정해야 하며, 먼저 seek하여 측정하지 마세요. '명언이 말해지는 페이지'를 측정하려면 V.currentTime = 명언 중간然后 drawImage——seeked는 디코딩 완료만 보장하고, 화면은 아직 바뀌지 않았습니다, 실제로 전체 quote-06 영상이 0초(이전 페이지)의 레이아웃을 사용하여 구도를 잡았습니다: SP가 596으로 측정되었지만 실제로는 696이었습니다. 따라서 '글자를 자르지 않음'을 보장하는 프레임이 처음부터 잘못되었고, 플래그십 키워드의 2.8초 동안 절반의 프레임에서 텍스트가 잘렸습니다. 신뢰할 수 있는 방법은 재생 중에 측정하는 것입니다: 시작 시 0초의 프레임 사용(그것이 당시 화면에 있던 것입니다), 이후 장면이 변경될 때마다 다시 측정합니다. 또한 '한 문장이 두 페이지에 걸쳐 있는' 문제도 해결됩니다——페이지 전환 후 다음 장면 변경 시 따라잡힙니다.

  35. 명암 이벤트에는 측정 가능한 상한이 있어야 하며, '나는 매우 느리게 한다'고 말하는 것만으로는 충분하지 않습니다. 슬라이드 밴드의 프레임별 YAVG를 측정하고, 임계값은 상대 10%에 절대 하한 4/255를 추가합니다——슬라이드 배경색 Y가 약 20이므로, 순수 절대값을 임계값으로 사용하면 9% 밝기 증가도 감지할 수 없습니다; 순수 상대값을 사용하면 어두운 영역의 노이즈가 판을 칠 수 있습니다. '가장 밀집된 1초에 몇 번'을 세고, 3회를 유지합니다 (광과민성 간질의 위험 임계값). 실측 결과 완성품은 2회/초; 음성 대조 5Hz 깜박임 26회/초, 1Hz ±6% 느린 맥동 6회/초, 둘 다 차단 가능합니다. 이 검사는 이미 capture-short.mjs에 작성되어 있으며, 녹화 후 스스로 출력합니다.

  36. ffmpeg의 eq 필터, brightness 표현식은 기본적으로 한 번만 계산됩니다(eval=init). '의도적인 깜박임'을 위한 음성 대조를 만들 때 :eval=frame을 누락하면, 필터가 작동하지 않으며, 측정 결과가 완성품과 동일합니다——거의 좋은 지표를 '식별력 없음'으로 판단할 뻔했습니다. 음성 대조 자체도 먼저 실제로 고장 났는지 확인해야 합니다, 그렇지 않으면 도구가 두 개의 동일한 파일을 구분할 수 없다는 것을 증명할 뿐입니다.

  37. 프레임 차이는 '동기화'에 대해 항상 식별력이 없습니다. GOTCHA 31의 내용을 다른 템플릿에서 다시 검증: words.json을 의도적으로 6초 어긋나게 설정하면, 슬라이드 밴드의 프레임 차이는 동일한 0.718(words.json을 전혀 보지 않음)입니다. 폭발하는 것은 '자막 블록 변경 시점 vs 자막 영역 최대 프레임 차이 시점' 라인(0.091 → 0.567)입니다. 동적 지표는 동적임을 주장할 수만 있으며, 동기화는 별도로 측정해야 합니다.

  38. 자막 분할에서 '인접한 두 개의 키워드'를 만나면 하나의 블록으로 붙여버립니다. 분할은 '들어갈 수 없을 때'만 블록을 변경하며, 다른 키워드로 변경될 때도 여전히 들어갈 수 있는 경우가 많습니다——실제로 '큰 물음표' '검증되지 않음' 사이에 다른 글자가 없어서, 11자 블록 하나로 붙여지고, 블록 전체가 금색이 되어, 두 개의 폭발 지점이 하나로 줄어듭니다(밝은 블록이 2개만 남음). 수정 방법: 하나의 조건을 추가합니다:k >= 0 && k !== 이전 글자의 k이면 강제로 새 블록을 엽니다. 수정 후 밝은 블록 3개, 최대 블록이 11자에서 6자로 줄어듭니다. punch-score.py에도 동일한 분할이 이식되어 있으므로 함께 수정해야 합니다.

  39. 실행 중에 비밀리에 확대/축소가 곱해지므로, 구도를 계산할 때 먼저 그 여유를 빼야 합니다. 느린 푸시(+3.5%)와 키워드 푸시(+5%)를 합치면 8.7%: '딱 맞게' 잡은 구도가, 푸시되면 텍스트를 화면 밖으로 밀어내며, 특히 키워드가 있는 몇 초 동안 발생합니다——가장 문제가 없어야 할 부분에서 문제가 발생합니다. 두 가지를 모두 수행해야 합니다: 구도 잡기 시 z = 너비 허용치 / 1.087, 그리고 매 프레임마다 다시 클램프(z ≤ 너비 허용치, cx를 SP 양쪽 끝이 모두 프레임 안에 있도록 클램프). 불변량은 화면이 나가는 순간에 성립해야 하며, '구도를 잡을 때만 올바르게 계산된' 상태로는 안 됩니다.

  40. 슬라이드 자체의 페이지 머리글과 바닥글이 일단 화면 중앙에 배치되면, 좌우가 반드시 잘리며, 텍스트가 잘린 것으로 계산됩니다. 페이지 머리글의 작은 글자 줄(x 24 ~ 932)과 페이지 바닥글의 진행 표시줄(x 0 ~ 759)이 모두 텍스트 범위 밖으로 나갑니다; 카메라가 푸시되면 화면 양쪽을 벗어납니다. 검증 시 슬라이드 밴드의 상하를 각각 10%씩 '페이지 머리글/바닥글'로 수정하면, 카메라 움직임이 변경될 때 요소가 해당 선의 안쪽에 위치하게 되어, 텍스트가 잘린 것으로 계산됩니다. 수정 방법: '잉크가 SP 밖으로 나가는' 모든 행을 요소로 표시(판단 기준은 '가로로 완전히 관통'이 아님——페이지 바닥글의 진행 표시줄은 잉크 임계값에서 79% 너비만 남으므로, 전체 너비로 판단하면 놓칠 수 있음), 그런 다음 카메라 중심을 제한하여 요소가 화면 상하 가장자리의 10% 이내에만 위치하거나 완전히 프레임 밖으로 나가도록 합니다. 경계는 몇 픽셀 더 여유를 두어야 합니다: 정확히 10%에 걸쳐 있으면, 샘플링의 반올림으로 인해 요소의 마지막 한두 행이 텍스트 쪽으로 떨어질 수 있습니다(실제로 15프레임이 남았으며, 모두 81, 82행에 있었습니다).

  41. 구도 문제를 진단하려면 먼저 알고리즘을 오프라인에서 다시 실행하고, 매개변수를 조정하여 시도하지 마세요. 한 번의 녹화가 40초일 때, 블라인드 테스트로 임계값을 조정하면 '점점 더 나빠지는' 방향으로 갈 수 있습니다: 실제로 느린 푸시와 키워드 푸시를 끄면(이것이 주된 원인이라고 생각), 글자 잘림이 102프레임에서 189프레임으로 증가했습니다. ffmpeg로 동일한 감지 프레임을 추출하고, Python에서 동일한 band/SP 로직을 실행하면, 10초 만에 '브라우저가 측정한 SP가 0초 페이지와 완전히 동일하다'는 것을 알 수 있으며, 주된 원인이 즉시 드러납니다. 또한 구도 잡기의 전제 조건을 로그에 출력(SP, 너비 허용치의 z, 요소 위치)——구도가 무너질 때, 이 세 숫자를 보면 너비를 잘못 측정했는지, 아니면 통로가 막혔는지 알 수 있습니다.

  42. 완성품 숫자를 보고할 때는 '콘텐츠에서 온 것'과 '화면에서 온 것'을 구분해야 하며, 후자는 반드시 템플릿의 md5를 첨부해야 합니다. 콘텐츠에서 온 것(후크, 염색, 밝은 블록, 자막 블록, 무음, 말하기 속도)은 words.json에 바인딩되며, 해당 md5가 변경되지 않으면 숫자는 단 한 비트도 변경되지 않습니다; 화면에서 온 것(프레임 차이, 글자 잘림)은 녹화 당시의 템플릿 버전에 대해서만 유효합니다. 둘을 섞어서 보고하면, 다음 사람이 문서에 따라 재녹화할 때 일치하지 않는 숫자를 얻게 됩니다: 실제로 동일한 클립에서, 템플릿 변경 전에 녹화된 초당 변화량은 14.80이었고, 변경 후에는 27.19였습니다——1.8배 차이가 나며, 콘텐츠의 여섯 항목은 하나도 다르지 않았습니다. 따라서 상대방은 존재하지 않는 회귀를 찾기 위해 한 번의 반복을 소비했습니다. capture-short.mjs는 이제 매 녹화 시마다 템플릿 short-9x16.html md5 …를 출력하고, 숫자를 보고할 때 함께 첨부합니다.

  43. 녹화 자체에 떨림이 있으므로, 판단 기준은 이 떨림을 견딜 수 있어야 합니다. 템플릿과 입력을 md5로 고정하고, 연속 네 번 녹화하면, 슬라이드 밴드의 초당 변화량은 21.27 / 21.33 / 20.82 / 21.3입니다.

…

관련 스킬