도구별 파이프라인 — 악역영애 말포이 영상 5편
- 도구 단위로 "무엇을, 어떤 명령으로, 몇 개 동시에, 어디서 막혔고 어떻게 풀었나"를 재현 가능하게 정리
- 섹션마다: 입력→출력 / 흐름도(Mermaid) / 실행 절차 / 동시성·속도 / 함정과 해결 표 / 다시 돌리는 명령
- 전체 그래프는 03_파이프라인_그래프.md, 에이전트 배선도는 05_배선도/
| 순서 | 섹션 |
|---|---|
| 1 | Claude 오케스트레이션 (지휘·분할·병합·장애 복구) |
| 2 | Aside 브라우저 (레퍼런스·핀터레스트·Flow 자동화) |
| 3 | codex 이미지 생성 |
| 4 | 매팅 (사람만 오려내기, GPU) |
| 5 | 오디오 (ElevenLabs) |
| 6 | 합성·render |
Claude 오케스트레이션 파이프라인
- 이 문서는 "무엇을 어떤 순서로 만들었나"(→
03_파이프라인_그래프.md)가 아니라 "메인 Claude가 워커를 어떻게 띄우고·나누고·말 걸고·되살리고·돈을 지켰나"를 정리한 운영 관점 문서 - 근거: 메인 세션 transcript(
~/.claude/projects/-home-, Agent·SendMessage·TaskStop 호출과 완료 알림을 스크립트로 집계),/1fbc69d1-….jsonl STORYBOARD.md·V1-EDL.md·V4-BRIEF.md, 공통 양식 4종(_effects/SCHEMA.md·_layers/SCHEMA.md·설명/_prompts/SCHEMA.md·설명/_pipeline/SCHEMA.md),_tools/heavy.sh·matte_queue.sh·RENDER-FAST.md,설명/_newsletter/, 메모리malfoy-meme-project.md - 시각 표기: 전부 KST(transcript는 UTC라 +9시간 환산)
1. 정의 · 입력 → 출력
- 한 줄 정의: 메인 Claude가 지시서(정본 문서)를 쓰고 → 의존 없는 일을 서브에이전트로 동시에 띄우고(fan-out) → 진행 중엔 SendMessage로 함정·지시 변경을 퍼뜨리고 → 산출 파일을 판정·병합(fan-in)하는 지휘 단계. 무거운 계산은 셸 큐·semaphore(동시에 들어갈 수 있는 수를 정해 둔 문지기)에 맡김
- 입력: 사용자 요청·반려 원문, 리서치 결과, 각 단계 산출 폴더 상태
- 출력: 지시서(
STORYBOARD.md·V1-EDL.md·V3-STORYBOARD.md·V4-BRIEF.md·*_jobs.json·SCHEMA.md4종), 워커 기동·중단·재개 기록, 병합본(EFFECTS.md·01_코드vsAI.md·00_뉴스레터.md·이 폴더), 메모리 교훈 - 규모(transcript 집계, 10-07 17:21 ~ 10-08 12:03)
| 항목 | 수 | 내역 |
|---|---|---|
| 서브에이전트 기동(Agent) | 53 | 제작 22 · 효과 정리 7 · 설명 문서 24 |
| SendMessage | 29 | 지시 변경 5 · 재개 8 · 함정 공유 4 · 긴급 부하 규칙 6 · 산출 도착 릴레이 3 · 렌더 실측 결론 3 |
| TaskStop | 2 | 단일 효과 정리 워커(분할 전환), 프롬프트 원문집 워커(사용자 "일단 멈춰") |
| 사용자 질문(AskUserQuestion) | 1 | 기획 초반 |
| 메인 직접 Bash | 137 | 상태 확인·컨택트시트·이름 교정·C 드라이브 점검·프로세스 정리 |
역할 분담
| 주체 | 맡은 일 | 실제 예 |
|---|---|---|
| 메인 Claude(오케스트레이터) | 지시서 작성, 워커 분할·기동, 판정(컨택트시트·이미지 이름 대조·편 승인 전 확인), 병합 지휘, 사용자 보고·ETA, 사고 대응 | 1편 EDL 확정(18:46), 3편 이미지 8장 이름 교정(18:30), ffmpeg 30개 종료(20:10), 효과 정리 5분할 결정(21:43) |
| 서브에이전트: 리서치 | 밈 기원·Flow 정책/단가 조사, 핀터레스트 레퍼런스 수집(Aside 브라우저) | Research Flow Gemini Omni workflow, Research villainess Malfoy meme, Pinterest reference collection |
| 서브에이전트: Flow operator(Aside로 Flow 화면 조작) | UI 정찰·360p 테스트, 720p 배치 제출·다운로드·판정·LOG.md | Flow operator: UI recon + 360p test, Flow operator: V1 720p batch(이후 ref·extra·v3 큐까지 이어 받음), V4 Flow batch |
| 서브에이전트: 매팅 | 매팅 파이프라인 시험, Windows GPU(DirectML) 포팅 | Matting pipeline test, GPU (DirectML) matting port |
| 서브에이전트: 배경 블록 | HyperFrames 코드 블록 7종 | 워커 A(Motion background lab) · B(kinetic wall + cutin) · C(ending + clip wall) |
| 서브에이전트: 오디오 | ElevenLabs 곡·VO·SFX 생성과 실측 | ElevenLabs audio for V1, V3 audio, V4 audio: 170BPM edit track |
| 서브에이전트: 합성 | 편별 compositor, 렌더 | V1 compositor, V2 compositor, V3 overlays + V3 scene assembly, V4 compositor, V3 English song swap(→ 5편) |
| 서브에이전트: 실측 | 렌더 GPU 가속 가능성 검증 | Enable GPU for HyperFrames render → _tools/RENDER-FAST.md |
| 서브에이전트: 문서 | 효과 사전 섹션 5 + 병합 1, 레이어 편별 4, 프롬프트 4, 파이프라인 도구별 6, 그래프 1 | Effects A~E, Layers V1~V4, Prompts P1~P4, Pipeline T1~T6 |
| 서브에이전트: 문체 작성·판정 | 뉴스레터 섹션 작성(gn-voice-composer 6기), 문체 판정(gn-voice-judge 2기) | Compose S1~S6, Judge newsletter first half / second half |
| codex(서브 아님, 셸 프로세스) | 이미지 생성만 | _tools/imagegen_runner.py PARALLEL=4 |
| 셸 큐 | 매팅·무거운 렌더 | matte_queue.sh 2줄, heavy.sh 2슬롯 |
2. 흐름도
flowchart TD
U[사용자 요청] --> M1[메인 Claude<br/>지시서 작성<br/>스토리보드·편집 결정표·브리프·잡 파일·공통 양식]
M1 --> F{의존 없는 일인가}
F -- 예 --> W[서브에이전트 동시 기동<br/>백그라운드 실행]
F -- 아니오 --> Q[앞 산출 대기<br/>도착 알림 받으면 기동]
Q --> W
W --> W1[리서치·레퍼런스 수집]
W --> W2[Flow 화면 조작 워커]
W --> W3[배경 블록 워커 세 명]
W --> W4[오디오 워커]
W --> W5[편별 합성 워커]
W --> W6[문서 섹션 워커]
W2 --> K1[매팅 큐 두 줄]
W5 --> H[무거운 작업 문지기<br/>동시 두 개 슬롯]
K1 --> O[산출 폴더]
W1 & W3 & W4 & H & W6 --> O
M1 -. 진행 중 메시지<br/>함정 공유·지시 변경·긴급 규칙 .-> W
O --> J[메인 판정<br/>컨택트시트·파일명 대조·병합 누락 대조]
J -- 결함 --> W
J -- 통과 --> R[사용자 확인]
R -- 반려 --> I[원인 노드 판단<br/>규칙 누적·메모리 저장]
I --> M1
R -- 승인 --> E[인계·설명 문서]
X[세션 끊김<br/>재부팅] --> S[산출 폴더 상태 확인]
S --> T[멈춘 워커에 이어서 진행 메시지]
T --> W
3. 실제 실행 절차
-
지시서부터 고정(공유 재료는 워커가 아니라 문서가 들고 다님) - 공통 규칙은
STORYBOARD.md공통 절 한 곳에만: 화풍·의상 7벌·머리 3종·레이어 3층·IP 단어 금지·블루 #1E3CFF - 편별 결정은 별도 문서:V1-EDL.md(레이어 L0~L5·시간·소스 구간·SFX 시각),V3-STORYBOARD.md,V4-BRIEF.md. 합성 워커는 이 표만 보고 조립 - 부하 규칙까지 브리프에 박음(V4-BRIEF.md마지막 줄) ```- 부하 규칙: 렌더·대량 ffmpeg는 _tools/heavy.sh, render --workers ≤2(최종은 _tools/render-win.sh 권장), /mnt/d 전체 find 금지, TMPDIR=/mnt/d로 렌더 금지(Chrome SIGILL) ```
- 생성 잡은 실행 파일로:
06_flow/{v1,ref,extra,v3,v4}_jobs.json,v3_images.jsonl→ 워커는 jsonl/json만 읽고 제출
-
fan-out: 의존 없는 갈래는 즉시 동시 기동 - 첫 15분(17:22~17:37)에 7기 동시: 리서치 2 + 핀터레스트 + 배경 모션 + V1 오디오 + 매팅 시험 + Flow 정찰 - 이유: 곡(박 시각)·배경 블록·매팅 방식은 서로 기다릴 필요가 없음. 합류는 합성 단계 하나 - 백그라운드 기동 → 메인은 완료 알림(task-notification) 받을 때마다 판정·다음 기동
-
분할 계약: "섹션 워커 N기 + 공통 SCHEMA + 병합" - 같은 패턴을 4번 씀
| 묶음 | 공통 양식 | 워커 | 각자 쓰는 파일 | 병합 |
|---|---|---|---|---|
| 효과 사전 | _effects/SCHEMA.md(카테고리 10개·표 열 고정·slug 조회법) |
5기(A 블록 / B 1·2편 / C 3·5편 / D 4편 / E 매팅·오디오·생성) | _effects/<담당>.md |
병합 워커 1기 → EFFECTS.md |
| 코드 vs AI 레이어 | _layers/SCHEMA.md(라벨 4개·비교 이미지 규격·부하 규칙) |
편별 4기 | _layers/<편>/ |
→ 설명/01_코드vsAI.md |
| 프롬프트 원문집 | 설명/_prompts/SCHEMA.md(원문 그대로·details 접기) |
4기(P1 codex / P2 Flow / P3 ElevenLabs / P4 규칙) | 섹션 파일 | → 02_프롬프트_원문집.md |
| 도구별 파이프라인 | 설명/_pipeline/SCHEMA.md(6절 순서 고정) |
6기(T1~T6) | T<n>_*.md |
이 폴더 |
| 뉴스레터 | _newsletter/00_FACTS.md(숫자는 여기 있는 것만) |
작성 6기 + 판정 2기 | S<n>_*.md |
draft_merged.md → apply_A.py·apply_B.py |
- 모든 SCHEMA의 공통 제약 3줄: 지정 파일 하나만 Write /
/mnt/d전체 find 금지 / 다른 워커에게 메시지 금지제약: 읽기 전용, 지정된 출력 파일 하나만 Write. 무거운 프로세스(렌더·ffmpeg 대량·브라우저 조작) 실행 금지. /mnt/d 전체 find 금지. 다른 워커에게 메시지 금지. - 뉴스레터는 섹션 브리프(
S<n>_input.md) = 섹션 지시 +00_FACTS.md전문을 한 파일로 합쳐 넘김 → 워커가 다른 파일을 찾아다니지 않음 - 같은 폴더를 나눠 쓸 땐 파일 단위로 소유권 분리: 배경 블록 3분할 메시지(18:44)
너는 ① gacha_build ② srank_flash ⑤ nameplate_slam + 공용 compositions/mm-lib.js 유지보수만 맡아 (mm-lib.js API를 바꾸면 하위호환 유지). ③ kinetic_wall ④ beat_cutin 은 워커 B, ⑥ ending_stamp ⑦ clip_wall 은 워커 C가 같은 03_motion/compositions/ 에 각자 파일로 만든다 — 그 파일들은 건드리지 마. index.html 루트 조립은 나중에 메인이 함. README.md에 네 블록 섹션만 써(다른 워커는 README-B.md, README-C.md).
-
병합 + 누락 대조(스크립트로) - 효과 사전: 섹션 5개 약 790줄·367행 → 같은 효과 54묶음을 한 행으로 → 278개 용어, 누락 0(섹션 용어가 병합본에 다 있는지 스크립트 대조). 예: white flash 6행 → 1행, punch-in 7행 → 1행. 섹션 원본은
_effects/에 그대로 두고 병합본에서 링크 - 뉴스레터: 판정 워커 2기의 지적을 "원문 → 고친 문장" 치환표로 받아 스크립트로 적용, 못 찾은 문장은 MISS로 출력python miss=0 for a,b in R: if a in t: t=t.replace(a,b) else: miss+=1; print("MISS:",a[:50]) open(p,"w").write(t); print("applied", len(R)-miss)(설명/_newsletter/apply_A.py) -
통신: 진행 중 워커에 SendMessage로 전파(29통)
| 종류 | 시각 | 받는 워커 | 내용 요지 |
|---|---|---|---|
| 지시 변경 | 18:34 | Flow operator | 사용자 "5~6개 돌려도 됨" → "60초 간격·1건씩 규칙 폐기, 동시에 최대 6개", 배치 크레딧 상한 110 유지, 이어서 ref_jobs 4건(상한 +60) |
| 지시 변경 | 18:34 | 배경 워커 A | 사용자 "클립이 배경에 쫙 깔리게" → 블록 ⑦ clip_wall 추가 |
| 큐 추가 | 18:44 | Flow operator | "항상 동시 5개 렌더 유지", extra 9건 + v3 7건, 잔액 650 아래로 내려가지 않게 |
| 분할 | 18:44 | 배경 워커 A | 블록 3분할(위 인용), 동시에 B·C 신규 기동 |
| 함정 공유 | 19:58 | 합성 워커 4기 | 워커 B가 실측한 렌더 함정 3개: ① 알파 webm을 HyperFrames에 넣으면 멈춤 → 배경·전경 패스 + ffmpeg overlay ② TMPDIR=/mnt/d면 렌더용 Chrome이 안 뜸 ③ S4 전신은 높이 84~86%로 줄여야 머리가 상단 자막대에 안 걸림 |
| 긴급 규칙 | 20:10 | 진행 워커 6기 | 로드 160 사고 → heavy.sh 경유·--workers ≤ 2·/mnt/d 전체 find 금지·ffmpeg 순차 |
| 산출 릴레이 | 20:29 / 20:57 | 3편 합성 2기 / 4편 합성 | 오디오 워커 산출(beats.json의 브레이크·훅·드롭 시각)을 그대로 전달 → 합성 워커가 곡 완성을 기다리며 폴링하지 않음 |
| 실측 결론 | 20:33 | 합성 워커 3기 | RENDER-FAST.md: GPU 이득 거의 0, 최종 렌더는 render-win.sh 권장, worker 수가 진짜 레버 |
| 범위 변경 | 20:57 | 영어 곡 교체 워커 | "영어판은 3편 교체가 아니라 별도 5편" → 07_render/v3/는 수정 금지, cp -a로 v5/에 복사해서 작업, 이미 고쳤으면 원상 복구 |
| 재개 | 19:53 / 10-08 10:57 | 멈춘 워커 4기씩 | "세션 재시작으로 중단됐었어. 이어서 진행" + 남은 일 목록 |
- 긴급 규칙 원문(20:10, 렌더 워커에게)
긴급: WSL 로드 160·RAM 2GB까지 터져서 한 번 재부팅됐음. 지금부터 규칙: ① 렌더·hyperframes check·대량 ffmpeg는 반드시 `/mnt/d/malfoy-meme/_tools/heavy.sh <명령>`으로 감싸 실행(전체 동시 2개 상한 세마포어) ② hyperframes render --workers 2 이하 ③ /mnt/d 전체 find/bfs로 폰트 찾기 금지 ④ ffmpeg 동시 다발 실행 금지(순차). - 원칙: 워커끼리는 직접 말하지 않음. 한 워커가 발견한 함정은 메인이 받아서 필요한 워커에게만 다시 보냄(허브형)
-
부하 사고와 semaphore 도입 - 사고: 10-07 19:5x 과부하로 WSL 재부팅(메인이 20:09에 가동 시간 15분으로 역산) → 재개 후 20:09 로드 160 재발. 메인이 짚은 원인 3개 = clip_wall 확인 ffmpeg 30개 동시 + 워커 둘이 폰트 찾으려고
/mnt/d전체 검색 + 렌더 워커 여러 기가 Chrome 동시 기동. 20:09 시점 서브에이전트 8기 동시 → 로드 160·RAM 2GB - 메인 조치(20:09~20:11): 원인 ffmpeg 직접 종료 →heavy.sh작성 → 6기에 긴급 규칙 전파 → clip_wall은 클립 8~12개로 축소 -_tools/heavy.sh핵심bash while true; do for s in 1 2; do exec {fd}>/tmp/malfoy-heavy-$s.lock if flock -n "$fd"; then nice -n 5 "$@"; rc=$? flock -u "$fd"; exec {fd}>&- exit $rc fi; exec {fd}>&- done; sleep 5 done- 이후 모든 SCHEMA·브리프에 부하 규칙을 복사해 넣음(_layers/SCHEMA.md"부하 규칙" 절,V4-BRIEF.md마지막 줄) -
장애 복구: 재부팅 2회 → 산출 폴더 확인 후 멈춘 워커만 재개 - 신호: 세션 재접속 직후 "N background agents didn't finish before the previous session ended" 알림(진행 중 transcript는 보존됨) - 1차(10-07 19:52 알림): 배경 A·B·C + GPU 매팅 포팅 4기 정지 → 메인이 C 드라이브·산출 폴더 확인 → 19:53 배경 A·B·C + 3편 오디오 4기에 재개 메시지, 매팅 큐는 셸로 직접 재기동 ``` 세션 재시작으로 중단됐었어. 이어서 ⑥ ending_stamp ⑦ clip_wall 완성(lint/check·540p 미리보기·프레임 Read·1회 개선)
- README-C.md 후 보고. ... 임시 파일은 TMPDIR=/mnt/d/_cache/tmp. ```
- 2차(10-08 10:57 알림): Layers V1~V4 4기가 거의 처음에서 정지 → 4기 모두 "SCHEMA 양식, 출력 _layers/v
/, 무거운 ffmpeg는 heavy.sh로 순차" 재개 - 재개가 안전한 이유: 산출이 파일 단위·멱등(이미 있으면 건너뜀).
matte_queue.sh는.part.webm으로 쓰고 끝나면 이름 변경 → 반쪽 파일을 완성본으로 오인 안 함,imagegen_runner.py는 있는 파일 건너뜀 - LangGraph의 checkpointer 역할을 "산출 파일 존재 확인"이 대신함
-
사용자 피드백 = interrupt(흐름을 멈추고 사람 입력으로 다음 길을 고르는 지점) - 처리 방식: 결과물만 고치지 않고 원인 노드를 찾아 거기 담당 워커에 전파하거나 새로 기동 +
rules(STORYBOARD 공통 절·메모리)에 누적
| 시각 | 사용자 | 메인의 처리 | 되돌아간 노드 |
|---|---|---|---|
| 17:26 | "flow에는 이미지 기반 제작해야지" | 진행 중 도구 호출 중단, codex 이미지 → Flow 에셋 순서 고정, 메모리 저장 | 캐릭터 디자인 |
| (시각 기록 없음) | "옷이 다 고정이잖아" | 의상 7벌·머리 3종을 STORYBOARD 공통 절에 기본값으로 | 첫 프레임·의상 |
| 18:33~18:43 | "레퍼런스로도", "5~6개 돌려도 됨", "막 굴려 한 번에 5개" | Flow operator에 SendMessage 2통(동시 6 → 5 유지, ref·extra·v3 큐) | Flow 배치 |
| 18:36 | "그냥 GPU 쓰면 되잖아", "D 드라이브에 깔아" | GPU 매팅 포팅 워커 신규 기동, 설치·캐시 D:\_cache |
매팅 |
| 20:47 | 2편 하단 "악역영애 말포이" 자막 삭제 | 2편 재출력(자막판은 _withsub로 보존) |
합성 |
| 20:51 | "도는 거 멋없다, 확확 바뀌는 거, v4로" | V4-BRIEF.md 작성 + Flow·오디오·합성 3기 동시 기동 |
스토리보드 |
| 20:55 / 20:57 | 한국어 보컬 반려 → "5편이어야지" | 영어 곡 교체 워커 기동 → 2분 뒤 범위 변경 SendMessage(3편 보존, v5 복사) | 곡 후보 생성 |
| 21:43 | "멀티에이전트로 정리시키고 합치면 되잖아" | 단일 효과 정리 워커 TaskStop → 5기 + 병합 1기 | 문서 |
| 10-08 10:58 | "멀티에이전트 넉넉히" | gn-voice 작성 6기 + 판정 2기 동시 | 문서 |
| 10-08 11:10 / 12:02 | "일단 멈춰" → "동시성 올려" | 원문집 워커 TaskStop → 프롬프트 4기 + 파이프라인 6기 동시 | 문서 |
- 크레딧·비용 관리
- 계획:
STORYBOARD.md예산표 Flow 1000 = 360p 테스트 ~100 / 1편 ~150 / 2편 ~60 / 3편 ~400 / 예비 ~290 - 집행 장치: 배치마다 워커 지시서에 상한(v1 110, ref +60, extra·v3는 "잔액 650 아래 금지", v4 상한 90), 생성 직전 화면 비용 표시로 단가 확인, 재생성은 컷당 1회(_r2) - 실적
| 도구 | 사용 | 비고 |
|---|---|---|
| Flow | 386 크레딧(1,050 → 664) | 테스트 10 + v1 88 + ref 88 + extra 71 + v3 59 + v4 70. v4 로그 "734 → 664, 재생성 0, 상한 90 이내" |
| ElevenLabs | 약 19,500 character_count(19,475) | 1편 3,101 / 3편 9,938 / 4편 1,324 / 5편 5,112. 편별 상한(1편 25,000 등), 곡은 동시 요청 2개 |
| codex 이미지 | 약 35장 | 배치 5회, PARALLEL=4. 크레딧·요금 수치 기록 없음 |
| - API 키는 워커 지시서에 값으로 넣지 않고 로컬 secrets 파일 경로만 지정([REDACTED]) |
- 교훈은 메모리 파일로
memory/malfoy-meme-project.md: 이미지 기반 Flow 강제, 의상 다양화, Flow 동시 5~6, 노래는 영어 보컬 기본, 과부하 사고와 heavy.sh, 재부팅 후 SendMessage 재개memory/wsl-disk-on-c-drive.md: 10-07 vhdx 75GB(내부 27GB) 재발 → 임시 폴더 D, 10-08 fstrim 정리memory/multi-agent-default-rule.md: 큰 덩어리를 단일 워커에 몰지 않기(효과 정리 사례와 같은 계열)- 다음 편에서 메모리 → STORYBOARD 공통 절 → 워커 지시서 순으로 규칙이 내려감
4. 동시성 · 속도
| 대상 | 동시 수 | 그 수인 이유 | 근거 |
|---|---|---|---|
| 서브에이전트(제작) | 최대 8기 확인(19:56~19:58: 배경 B·C, 3편 오디오, 1·2편 합성, 3편 오버레이·조립, 렌더 GPU 실측) + 매팅 큐 2줄 → 당시 보고 "워커 9기" | 의존 없는 갈래 전부 동시. 이 시점에 부하 사고 | transcript 기동·완료 알림 집계 |
| 서브에이전트(문서) | 순간 10~12기(10-08 11:00 레이어 4 + 원문집 + 그래프 + 작성 6, 12:02 프롬프트 4 + 파이프라인 6) | 읽기 위주·출력 파일 1개라 가벼움. 작성 워커는 약 1분 만에 끝남 | 같음 |
| 무거운 프로세스(렌더·check·대량 ffmpeg) | 2 | 12코어·WSL RAM 11.7G에서 렌더 3기 동시가 CPU 경합의 실제 병목 | heavy.sh, RENDER-FAST.md |
HyperFrames --workers |
2(긴급 규칙) / 4(슬롯 2개일 때) / 6(단독) | worker 병렬이 약 2배, GPU는 이득 거의 0 | RENDER-FAST.md 실측표 |
| 매팅 큐 | 2줄 | GPU 추론은 빠르지만 색차 키가 CPU라 한 줄로는 GPU 점유율 낮음 | 메인 보고 19:56, matte_queue.sh |
| Flow 제출 | 5~6 | 사용자 지시. 동시 제출해도 정책·레이트 경고 0 | SendMessage 18:34·18:44 |
| ElevenLabs 곡 | 2 | 3개 이상이면 429 | 03_파이프라인_그래프.md ⑥ |
| codex 이미지 | 4 | 러너 기본값, 8장 3.9분 | imagegen_runner.py |
- 실측 속도
- 제작 5편: 10-07 17:21 요청 → 21:57 효과 정리 병합까지 약 4시간 36분(1~5편 렌더 포함)
- 효과 사전: 5기 동시 21:43 기동 → 21:46~21:49 전원 완료 → 병합 21:49~21:56. 단일 워커였다면 5섹션 순차(추정)
- 뉴스레터 작성 6기: 11:00 기동 → 11:01 전원 완료
- 사용자 ETA 질문 5회("얼마나 걸릴 것 같아"), 21:22에 "아직 멀었어?" 재촉 1회 → 메인 ETA가 낙관적이었음
5. 함정과 해결 표
| 증상 | 원인 | 해결 | 근거 파일 |
|---|---|---|---|
| WSL 재부팅, 세션 끊김, 로드 160·RAM 2GB | (20:09 시점) 워커 8기 동시 + clip_wall 확인 ffmpeg 30개 + /mnt/d 전체 폰트 검색 + Chrome 다중 기동 |
heavy.sh 2슬롯, --workers ≤ 2, 전체 find 금지, ffmpeg 순차를 진행 워커 6기에 긴급 전파·이후 모든 SCHEMA에 복사 |
_tools/heavy.sh, _layers/SCHEMA.md, memory/malfoy-meme-project.md |
| 백그라운드 워커가 재부팅 후 멈춤(2회, 4기씩) | Claude Code 프로세스 종료 | "didn't finish" 알림 → 산출 폴더 확인 → SendMessage로 이어서 진행. 산출이 멱등이라 재개 안전 | transcript 19:52·10-08 10:57 알림, matte_queue.sh .part.webm |
| 같은 렌더 함정을 워커마다 따로 밟을 위험 | 워커끼리 정보 공유 경로 없음 | 발견한 워커 → 메인 → 해당 워커만 다시 전파(허브형). 실측 결론은 RENDER-FAST.md 한 파일로 |
SendMessage 19:58·20:33 |
| 영어판 워커가 3편 원본을 덮어쓰려 함 | 첫 지시가 "3편 곡 교체"(한국어판을 _KR로 옮기고 덮어쓰기) |
사용자 정정 2분 만에 범위 변경 메시지: v3 수정 금지·cp -a로 v5 복사·이미 바꿨으면 원상 복구 |
SendMessage 20:57, 07_render/V5_ROFAN_OP_EN.mp4 |
| 같은 compositions 폴더 동시 작성 충돌 위험 | 배경 블록을 1기 → 3기로 늘림 | 블록 단위 파일 소유권, README 분리(README-B.md·README-C.md), 공용 mm-lib.js는 A만·하위호환, 루트 조립은 메인 |
SendMessage 18:44, 03_motion/README*.md |
| 큰 정리를 단일 워커에 몰아 느림·사용자 질책 | 메인이 효과 정리를 1기에 맡김 | TaskStop → 카테고리·편 기준 5분할 + 병합 1기 + 누락 대조 스크립트 | _effects/SCHEMA.md, EFFECTS.md |
| 병렬 이미지 파일명이 서로 바뀜(4/4, 3/8, 4/7, 8/15) | 러너가 공용 폴더의 "가장 최근·미회수 PNG"를 집음 | 메인이 컨택트시트로 내용 대조 후 이름 교정(판정은 메인이 직접) | 00_SHEETS/, T2 문서 |
| C 드라이브가 7~9GB까지 줄어듦 | WSL vhdx가 75GB로 부풂(내부 27GB) | 매팅 venv·모델·pip 캐시·렌더 도구를 D:\_cache, 산출은 처음부터 /mnt/d. 10-08 fstrim으로 회수 |
memory/wsl-disk-on-c-drive.md |
| 문서 워커가 숫자를 지어낼 위험 | 섹션 워커가 소스를 각자 해석 | 공용 사실 시트 00_FACTS.md(숫자는 여기 있는 것만) + 섹션 브리프에 전문 동봉 |
설명/_newsletter/S*_input.md |
| ETA를 여러 번 물음·재촉 | 메인이 단계별 예상치를 낙관적으로 줌 | (미해결) 산출 파일 수 기준 진행률 보고가 필요(추정) | transcript 사용자 메시지 |
6. 다시 돌리는 법
cd /mnt/d/malfoy-meme
# 0) 지시서 확인: 공통 규칙·편집 결정표·브리프·잡 파일·공통 양식
cat STORYBOARD.md V1-EDL.md V4-BRIEF.md 설명/_pipeline/SCHEMA.md
# 1) 무거운 작업은 전부 semaphore 경유(동시 2), 렌더는 TMPDIR 해제
env -u TMPDIR HYPERFRAMES_EXTRACT_CACHE_DIR=/mnt/d/_cache/tmp/hf-frames _tools/heavy.sh npx hyperframes render --workers 2 --quality draft --output out.mp4
# 2) 매팅 큐 2줄(배치별), 이미지 배치 4병렬
nohup _tools/matte_queue.sh 06_flow/v4 05_matte/v4 > 05_matte/v4_queue.log 2>&1 &
PROMPTS=v3_images.jsonl OUTDIR=/mnt/d/malfoy-meme PARALLEL=4 python3 _tools/imagegen_runner.py
# 3) 재부팅 후: 최근 산출부터 보고 멈춘 워커에 "이어서 진행" 메시지
ls -t 05_matte/*/*.webm 07_render/*.mp4 설명/_pipeline/*.md | head -20
- 메인에게 주는 한 줄 지시 예: "SCHEMA.md 양식으로 섹션 워커 N기 동시 기동, 각자 지정 파일 하나만 Write·전체 find 금지·워커끼리 메시지 금지, 끝나면 병합 워커 1기 + 누락 대조 스크립트"
다음에 이 파이프라인을 1명령으로 돌리려면
- 지시서 묶음을 실행 단위로:
STORYBOARD·EDL·*_jobs.json·SCHEMA를 하나의run.yaml(편·노드·담당·입력·출력·상한)로 합치고, 메인은 이 파일만 읽어 워커를 띄우게 - 부하 규칙을 지시서가 아니라 도구에 강제: 렌더·ffmpeg·check 래퍼가
heavy.sh를 자동 경유하고,/mnt/d전체 find는 훅으로 차단(이번 원인 3개는 모두 도구 수준에서 막을 수 있던 것) - checkpoint를 파일로: 노드마다
state.json(완료·진행·실패, 산출 경로)을 남겨 재부팅 후resume한 번으로 멈춘 워커만 다시 띄우게(지금은 메인이 폴더를 눈으로 확인) - 사용자 반려 → 규칙 자동 누적: 반려 원문을
rules표에 행으로 추가하면 STORYBOARD 공통 절과 이후 워커 지시서에 자동 삽입(이번엔 메모리·문서에 손으로 옮김) - 예산·ETA를 계측으로: Flow·ElevenLabs 잔액과 산출 파일 수를 주기적으로 읽어 상한 도달 시 정지·진행률 기반 ETA를 자동 보고
Aside 브라우저 파이프라인
1. 한 줄 정의
- 정의: Windows 앱 Aside 브라우저를 WSL Claude Code에 MCP로 붙여서, 사용자의 로그인 세션(구글·핀터레스트)을 그대로 쓰며 브라우저를 코드로 조작하는 단계. 레퍼런스 확보, 핀터레스트 수집, Google Flow 영상 생성·다운로드 세 갈래로 씀
- 도구:
mcp__aside__repl= Playwright식 JS 실행기(결과는console.log로만 받음),mcp__aside__exec= 브라우저 에이전트(이번엔 안 씀). 로드는ToolSearch "select:mcp__aside__repl" - 입력 → 출력
- ① 인스타 릴스 URL →
00_ref/ref_DeKsEh0sBPy.mp4(720x1280, 30fps, 10.03초, 3,027,839B) - ② 핀터레스트 검색어 25개 →
00_ref/pinterest/<카테고리>/*.jpg122장 +_sheet_<카테고리>.jpg5장 +NOTES.md - ③ 첫 프레임 이미지(
02_frames/*) + 잡 목록(06_flow/<배치>_jobs.json) →06_flow/<배치>/<id>.mp4+<id>_tile.jpg+LOG.md(본편 44개(재생성 3 포함) + 테스트 2개)
2. 흐름도
flowchart TD
시작[사용자 요청: 레퍼런스 릴스 링크] --> 앱확인{Aside 앱 켜져 있나}
앱확인 -- 아니오 --> 앱실행[WSL에서 Aside.exe 실행]
앱실행 --> 앱확인
앱확인 -- 예 --> 갈래{작업 종류}
갈래 --> 릴스1[릴스 원본 주소 열기]
릴스1 --> 로그인벽[로그인 화면으로 넘어감 / 다운로드 도구도 실패]
로그인벽 --> 임베드[임베드 페이지 열기 /reel/아이디/embed/captioned/]
임베드 --> 영상주소[video 요소의 주소 추출]
영상주소 --> 내려받기1[WSL curl로 저장]
내려받기1 --> 릴스결과[00_ref 레퍼런스 영상]
갈래 --> 핀1[검색어별 새 탭 열기]
핀1 --> 핀2[스크롤 3회 하며 i.pinimg.com 주소 수집]
핀2 --> 핀3[탭 닫기 / 카테고리별 고르게 뽑기]
핀3 --> 핀4[WSL curl로 원본 또는 736 크기 저장]
핀4 --> 핀5[깨진 파일·무관 이미지 삭제]
핀5 --> 핀6[컨택트시트 생성 후 눈으로 확인]
핀6 --> 핀결과[NOTES.md 122장]
갈래 --> 플1[첫 프레임을 Aside 세션 폴더로 복사]
플1 --> 플2[Flow 프로젝트 열기]
플2 --> 플3[설정 팝업: 동영상 / 프레임 / 9:16 / 720p / 길이 / x1]
플3 --> 플4[시작 버튼 → 이미지 업로드 또는 애셋 검색 → 선택]
플4 --> 플5[프롬프트 입력 후 글자 일치 검증]
플5 --> 플6{칩 수·설정·단가 일치}
플6 -- 아니오 --> 플3
플6 -- 예 --> 플7[생성 시작 / 최대 5~6개 연달아 제출]
플7 --> 플8[진행률 사라질 때까지 대기]
플8 --> 플9[프롬프트 앞 35자로 타일 찾기 → 더보기 → 다운로드 → 원본 크기]
플9 --> 플10{제안 파일명이 잡 내용과 맞나}
플10 -- 아니오 --> 플9
플10 -- 예 --> 플11[세션 폴더 → D 드라이브 복사 / Windows 다운로드 폴더 사본 삭제]
플11 --> 플12[프레임 5장 타일·컷 검출로 판정]
플12 -- 명백한 실패 --> 플3
플12 -- 채택 --> 플결과[LOG.md 기록]
3. 실제 실행 절차
3-0. 공통 준비
- Aside 앱 확인:
tasklist.exe | grep -i aside. repl이 "Aside isn't running on this machine" 내면 WSL에서 직접 띄움bash nohup "/mnt/c/Program Files/Aside/Application/Aside.exe" >/dev/null 2>&1 &(근거: v4 오퍼레이터 기록, 띄우고 15초 뒤 repl 정상) - repl 규칙: 한 스코프라 같은
const재선언 불가, 실패한 호출의const도 남음 →globalThis.Z = {}식 namespace 객체에 담고 배치마다 새 이름(Z,Z4,Q,W) page.waitForTimeout없음("not a function") →Z.sleep = ms => new Promise(r=>setTimeout(r,ms))- 결과는
console.log로만 돌아옴. 스크린숏은display(await p.screenshot()), 단 CDP timeout 잦음 →bringToFront()후 3회 재시도 헬퍼
3-1. ① 레퍼런스 릴스 확보 (10-07 17:2x)
- 원본 주소
https://www.instagram.com/reels/DeKsEh0sBPy/를openTab→ 로그인 페이지로 리다이렉트,video0개 - yt-dlp 시도 → 실패
yt-dlp -o "ref_%(id)s.%(ext)s" --write-info-json "https://www.instagram.com/reels/DeKsEh0sBPy/" ERROR: [Instagram] DeKsEh0sBPy: Requested content is not available, rate-limit reached or login required.(자체 embed 재시도도 HTTP 404) - 임베드 페이지를 Aside로 열고
videosrc 추출js const embTab = await openTab('https://www.instagram.com/reel/DeKsEh0sBPy/embed/captioned/'); await sleep(4000); const embInfo = await embTab.evaluate(() => ({ text: document.body.innerText.slice(0,1500), vids: [...document.querySelectorAll('video')].map(v=>v.src||v.currentSrc)})); console.log(JSON.stringify(embInfo, null, 1));- 같은 호출의 스크린숏은 CDP timeout 났지만 src 추출은 성공 - 브라우저 안fetch→base64(4,037,120자)도 됐으나 실제 저장은 아래 curl로 함 - WSL curl로 CDN 주소 저장 → ffprobe 확인
bash cd /mnt/d/malfoy-meme/00_ref && curl -sL -A "Mozilla/5.0" -o ref_DeKsEh0sBPy.mp4 '<scontent-*.cdninstagram.com/...mp4?...&oh=[REDACTED]&oe=...>'- 결과: 720x1280 30fps, 10.025초, 오디오 포함. 이후 ffmpeg로frames/contact.png·audio.wav·장면 전환 검출(다른 섹션)
3-2. ② 핀터레스트 레퍼런스 수집 (10-07 17:4x~17:5x, 서브에이전트 1기)
- 수집 헬퍼 정의(탭 1개씩 열고 반드시 닫기, 썸네일 크기는 736x로 치환)
js globalThis.pinCollect = async (q) => { const t = await openTab('https://www.pinterest.com/search/pins/?q=' + encodeURIComponent(q)); await slp(3500); const set = new Set(); const grab = async () => { const srcs = await t.evaluate(() => Array.from(document.querySelectorAll('img')) .map(i => i.currentSrc || i.src).filter(s => s && s.includes('i.pinimg.com'))); for (const s of srcs) { if (/\/(75x75|30x30|60x60|140x140)/.test(s)) continue; set.add(s.replace(/i\.pinimg\.com\/[^/]+\//, 'i.pinimg.com/736x/')); } }; try { await grab(); for (let k=0;k<3;k++){ await t.evaluate(()=>window.scrollBy(0,2500)); await slp(1800); await grab(); } } finally { await closeTab(t); } return Array.from(set); };- 검색어당 수집 수 실측: 44~55개(예외 "slytherin anime girl" 5개) - 검색어 → 카테고리 폴더: char 6(
fem draco malfoy·malfoid·platinum blonde villainess anime등), showcase 4, gacha 3+보충 4(zenless zone zero gacha pull screen·genshin wish 5 star animation·honkai star rail warp 5 star·zzz s rank agent obtained screen), bg 3, romfan 3 - 카테고리별 round-robin(검색어 번갈아)으로 최대 28장 골라
카테고리 약칭 경로목록을console.log→ scratchpadlist.txt - WSL curl 다운로드(originals 먼저, 실패 시 736x, 둘 다 실패면 삭제)
bash while read c ab p; do mkdir -p "$c"; k="$c-$ab"; cnt[$k]=$(( ${cnt[$k]:-0} + 1 )); out="$c/${ab}_$(printf %02d ${cnt[$k]}).jpg" curl -sfL -A 'Mozilla/5.0' "https://i.pinimg.com/originals/$p" -o "$out" || curl -sfL -A 'Mozilla/5.0' "https://i.pinimg.com/736x/$p" -o "$out" || rm -f "$out" done < list.txt - PIL로 깨진 파일(최소 변 80px 미만 포함) 삭제 + 7열 컨택트시트
_sheet_<카테고리>.jpg→ Read로 눈 확인 - 무관 이미지 삭제(Gacha Life 앱·음식·방·광고) → gacha 보충 검색 → 시트 재생성 →
NOTES.md카테고리별 5줄 - 최종: char 19 · showcase 20 · gacha 28 · bg 28 · romfan 27 = 122장
3-3. ③ Google Flow 자동화 (핵심, 10-07 18:08~21:06)
- 프로젝트:
https://flow.google.com/project/(이름 malfoy-meme), 계정 PRO, UI 한국어, 한국 거주라 가시 워터마크 강제 - 정본 매뉴얼:
06_flow/FLOW-UI.md(1차 오퍼레이터가 쓰고 2차가 §9 추가). 이후 오퍼레이터는 "이 문서대로" 지시만 받음
- 첫 프레임 업로드 준비 — D: 경로는 거부되므로 Aside 세션 폴더로 복사
bash D=/mnt/c/Users/- 세션 ID는 repl 출력의 "Displayed image saved to" 경로 또는/.aside/u/0/sessions/2026-10-07_UEt3yVScOrtuSVDz mkdir -p $D/artifacts/mm && cp /mnt/d/malfoy-meme/02_frames/S4_fullbody.png $D/artifacts/mm/ ls -t .../sessions/로 확인 - WSL→/mnt/c 복사 느림(2MB 2장에 1~2분) →run_in_background - 프로젝트 열기·배너 —
openTab(프로젝트 URL)→ 쿠키 배너getByRole('button',{name:'나중에'}) - 프로젝트 설정(톱니, 우상단 (1223,37), 이름 "타일 그리드 설정") —
무음 동영상 반환 전환 버튼ON. 토글은locator.click()이 "Checkbox click did not change checked state" 에러 → 좌표mouse.click후aria-checked재확인.제출 시 프롬프트 지우기기본 ON 유지(제출하면 상자·시작 프레임 비워져 재료 섞임 방지) - 생성 설정 팝업 —
getByRole('button',{name:'설정 트리거'}). 기본값이 이미지/Nano Banana 2.1/16:9/x2라 매번 전환. 팝업 라디오는 snapshot에 잘 안 잡혀 evaluate로 좌표 구해 클릭js Z.pick = async (label)=>{ const b = await Z.p.evaluate((label)=>{ const el=[...document.querySelectorAll('[role=radio]')] .find(e=>{const t=e.innerText.replace(/\n/g,' ').trim(); return t===label||t.endsWith(' '+label)||t.startsWith(label+' ');}); if(!el) return null; const r=el.getBoundingClientRect(); return {x:r.x+r.width/2,y:r.y+r.height/2,c:el.getAttribute('aria-checked')}; }, label); if(!b) return 'NOTFOUND'; if(b.c!=='true'){ await Z.p.mouse.click(b.x,b.y); await Z.sleep(800);} return 'ok'; }; for (const n of ['동영상','프레임','9:16','720p','4초','x1']) await Z.pick(n); // 동영상 → 프레임 순서 먼저- 다른 다이얼로그 닫은 직후 첫 클릭은 포커스만 먹음 →[role=radio]개수 0이면 최대 3회 재클릭(Z4.openSettings) - 팝업 하단 "N 크레딧" 링크로 단가 검증(v4 실측 "Google Flow 크레딧 734개 / 7 크레딧") - 첫 프레임 지정(프레임 모드)
- 처음 업로드:
button "시작"(exact) → 다이얼로그 "프레임 이미지 선택" →waitForEvent('filechooser')+button "미디어 업로드"→setFiles('C:\\Users\\→ "업로드 중 <파일명>" 사라질 때까지 대기 - 함정: 목록 첫 항목(이전 이미지)이 기본 선택돼 있고 "프롬프트에 추가"가 이미 활성 → 반드시\\.aside\\u\\0\\sessions\\<세션>\\artifacts\\mm\\S4_fullbody.png') getByRole('option',{name:'<파일명>'}).click()- 재사용(2회차부터): 업로드분은 프로젝트 애셋으로 남음 →input[placeholder="애셋 검색"]에 파일명 입력 → option 클릭만으로 상자에 들어가고 다이얼로그 닫힘js Z4.setFrame = async(f)=>{ await Z4.p.getByRole('button',{name:'시작',exact:true}).click(); await Z4.sleep(1800); const s=Z4.p.locator('input[placeholder="애셋 검색"]'); await s.click(); await Z4.p.keyboard.insertText(f.replace('.png','')); await Z4.sleep(2000); await Z4.p.getByRole('option',{name:f}).first().click(); await Z4.sleep(1800); return await Z4.p.getByRole('button',{name:'이미지 소재'}).count(); }; // 1이면 성공- 첫+끝 프레임(V2_morph):button "시작"·button "종료"각각 선택 →이미지 소재칩 2개면 성공. 단가 동일(8초 12) - 프롬프트 입력·제출 —
[contenteditable=true]클릭 →keyboard.insertText(prompt)→innerText.trim()===prompt검증 → 칩 수·트리거 라벨(동영상 · 720p · 4초 … x1) 확인 →button "생성 시작". 제출 전 페이지에정책|Error 253|오류가 발생있으면 중단js Z4.run = async(i)=>{ const j=Z4.jobs[i]; if(await Z4.p.evaluate(()=>/정책|Error 253|오류가 발생/.test(document.querySelector('main')?.innerText||''))) return j.id+' ABORT'; if(await Z4.setFrame(j.f)!==1) return j.id+' frame fail'; if(await Z4.typePrompt(j.p)!==j.p) return j.id+' prompt mismatch'; await Z4.p.getByRole('button',{name:'생성 시작'}).click(); Z4.sub[j.id]=new Date().toISOString(); await Z4.sleep(2500); return j.id+' submitted'; }; - 참조(소재) 모드 + 캐릭터 @Malfoy (ref 배치)
- 좌측
캐릭터→ 새 캐릭터 → 하단업로드(단일 파일,02_frames/turnaround.png4면 턴어라운드) → 제목 연필(382,183) → Ctrl+A →Malfoy→ Enter → 우상단완료. 등록 0크레딧 - 설정 팝업소재라디오 → 상자에@입력 →Malfoy타이핑 → optionMalfoy 캐릭터→ 칩(span.mention-chip[data-reference-type=entity]) → 추가 이미지·동영상은button "프롬프트 상자에 소재 추가"(+) →Ctrl+End후 나머지 문장insertText- 동영상 참조(06_flow/ref_motion_spin3s.mp4, 레퍼런스 릴스 4.3초부터 3초 잘라 만든 것) 업로드 시 "이 동영상을 사용할 권리" →동의 - 진행 대기 — main innerText의
NN%사라지면 완료. 그리드는.cdk-virtual-scrollable가상 스크롤이라window.scrollBy안 먹음 → 이 요소scrollTop조작, 화면 밖 타일 진행률은 안 잡힘 - 다운로드 — 동시 생성이라 "맨 위 타일" 가정 금지. 프롬프트 앞 35자로 행 찾기 → 가장 가까운
옵션 더보기(x≈392) →다운로드hover →720p 원본 크기클릭 →saveAs(세션 폴더)→suggestedFilename()로 내용 대조js const m = await Z4.openMenu(j); // 35자 매칭 + 가장 가까운 ⋮ 클릭 for(let k=0;k<4 && !o;k++){ const dl = await Z4.stable(/다운로드/); await Z4.p.mouse.move(dl.x-60,dl.y); await Z4.sleep(250); await Z4.p.mouse.move(dl.x,dl.y); await Z4.sleep(1000); o = await Z4.stable(/720p 원본 크기/); } // 좌표가 2번 연속 같을 때만 신뢰 const dp = Z4.p.waitForEvent('download',{timeout:30000}); await Z4.p.mouse.click(o.x,o.y); const d = await dp; await d.saveAs(Z4.dir + j.id + '.mp4'); return j.id+' | '+d.suggestedFilename(); // 예: V4_O3_flick | Character_flips_broomstick_…mp4- 하위메뉴: 720p 생성분 =270p 애니메이션 GIF/720p 원본 크기/1080p 업스케일링됨. 우측 "일괄 다운로드" 아이콘은 해상도 선택 없어 안 씀 - 회수·정리·판정
bash S=/mnt/c/Users/- v1~v3은 scratchpad/.aside/u/0/sessions/2026-10-07_cQIOnm63VsrEl5Ws/artifacts/mm4; D=/mnt/d/malfoy-meme/06_flow/v4 cp $S/V4_*.mp4 $D/ # 크기 대조 후 cd /mnt/c/Users/ /Downloads && rm -v -- Anime_character_*_20261007210*.mp4 ... # 자동 사본 10개 삭제(C 여유 확보) ffmpeg -v error -y -i $f -vf "fps=5/$d,scale=270:-1,tile=5x1" -frames:v 1 ${b}_tile.jpg # 5장 타일 → Read로 판정 ffmpeg -i $f -vf "select='gt(scene,0.3)',showinfo" -f null - 2>&1 | grep -c pts_time # 컷 수 qc.sh NAME(세션 폴더→D 복사 + ffprobe + 5장 타일) 한 줄로 처리 - 판정 기준: 컷 분할·블루 붕괴·얼굴 붕괴·기괴한 손/몸만 1회 재생성(<id>_r2.mp4) - 세션 끊김 복구 — Aside 데몬 자동 업데이트 → repl "REPL session not found" + 탭 닫힘 + 세션 폴더 ID 변경(
UEt3yV…→50vvOH…) -listBrowserTabs()로 상태 확인 → 새 namespace(globalThis.W)로 탭 재오픈·헬퍼 재정의 → WSL에서 새 세션artifacts/mmmkdir →W.dir·qc.sh경로 교체 - 업로드한 프레임·캐릭터는 프로젝트 애셋이라 재업로드 불필요
3-4. 배치별 실측 (각 06_flow/<배치>/LOG.md)
| 배치 | 시간(10-07) | 건수 | 모드 | 사용 크레딧 | 재생성 | 잔액 |
|---|---|---|---|---|---|---|
| test | 18:08~18:14 | 2 (T1 8초·T2 4초, 360p) | 프레임 | 10 | 0 | 1,050 → 1,040 |
| v1 | 18:17~18:39 | 11 | 프레임 | 88 | 0 | 1,040 → 952 |
| ref | 19:02~19:26 | 4 + 재 2 | 소재 + @Malfoy | 88 (본 56 + 재 32) | R1·R4 | — |
| extra | 19:17 | 9 | 프레임(V2_morph만 첫+끝) | 71 | 0 | — |
| v3 | 19:20~19:26 | 7 + 재 1 | 프레임 | 59 (본 52 + 재 7) | F4 | 큐 종료 734 |
| v4 | 제출 20:54~20:59, 다운 21:02~21:06 | 10 | 프레임 | 70 | 0 | 734 → 664 |
- 합계 386크레딧(1,050 → 664), 클립 46개(테스트 2 + 본편 44, 재생성 3 포함), 정책 차단·Error 253·unusual activity 0
- 단가(Omni 1.1 Flash, x1): 360p 4초 4·8초 6 / 720p 4초 7·6초 10·8초 12 / 소재 모드 동영상 참조 포함 8초 20 / 캐릭터 등록 0
- 출력 사양: 720p = 720x1280 h264, v4 전부 4.01초·컷 0(scene score 최대 0.13). 360p = 360x640 24fps. 오디오 트랙 항상 포함(무음 설정 ON인데 T1엔 효과음 성분 → 후반
-an전제)
4. 동시성·속도
- Aside 탭: 항상 1개. 핀터레스트도 "탭 2개 이상 금지"로 검색어 순차(탭 열고 닫기 반복). 이유: repl 한 세션·
page하나 공유, Flow 탭과 섞이지 않게 - Flow 동시 제출
- v1 시작: 1건씩, 최소 60초 간격(코디네이터 초기 지시, 정책·레이트 차단 우려)
- 18:34 지시 변경: 동시 최대 6개, 60초 간격 폐기(LOG 기록). extra 9건은 19:17에 연달아 제출, 실측 5~9개 동시 렌더 문제없음
- v4: 사용자 허용 "동시 5개까지" → 5개 + 5개(20:54~20:59)
- 제출 간격 약 20~30초 = 설정 확인·프레임 선택·프롬프트 검증 시간(Flow 쪽 제한 아님)
- 생성 시간: 360p 8초 영상 약 45초, 4초 약 28초(test). 720p는 다운로드 시점 기준 제출 후 수 분 내 완료(v4: 마지막 제출 20:59 → 다운로드 21:02 시작)
- 다운로드: v4 10개 21:02~21:06 (개당 약 25초, 메뉴 hover 재시도 포함)
- 2차 오퍼레이터 전체: 4개 큐(v1·ref·extra·v3) 34클립을 18:17~19:26(약 70분)에 끝냄
- 핀터레스트 curl: 직렬 루프 약 140장이 Bash 600초 timeout을 넘겨 백그라운드로 넘어감(17:5x 완료). 보충분 28장은
&로 병렬 → 즉시 끝남 - WSL→/mnt/c 복사: 2MB 2장에 1~2분 → 백그라운드 실행
5. 함정과 해결
| 증상 | 원인 | 해결 | 근거 파일 |
|---|---|---|---|
릴스 URL 열면 로그인 페이지, video 0개 |
인스타 로그인 벽 | /reel/<id>/embed/captioned/ 임베드 페이지에서 video.src 추출 → WSL curl |
메인 세션 기록(10-07 17:2x), 00_ref/ref_DeKsEh0sBPy.mp4 |
| yt-dlp "login required" | 로그인 벽 + 자체 embed 404 | 위 임베드 우회(쿠키 전달은 안 씀) | 메인 세션 기록 |
| repl "Aside isn't running on this machine" | Aside 앱 꺼짐 | WSL에서 nohup ".../Aside.exe" & 후 재시도 |
v4 오퍼레이터 기록 |
t.waitForTimeout is not a function |
Aside repl에 해당 API 없음 | setTimeout 기반 sleep 헬퍼 |
06_flow/FLOW-UI.md §0, 핀터레스트 기록 |
같은 이름 const 재선언 에러 |
repl 한 스코프, 실패 호출의 const도 남음 | globalThis.Z namespace + 배치마다 새 이름 |
FLOW-UI.md §0 |
| 스크린숏 CDP timeout | 탭이 뒤에 있음 | bringToFront() 후 3회 재시도 |
FLOW-UI.md §0 |
setFiles('D:\\...') "escapes the session directory" |
Aside가 세션 폴더 밖 파일 거부 | C:\Users\로 복사 후 그 경로 |
FLOW-UI.md §0·§4 |
| 엉뚱한 이미지가 첫 프레임으로 들어감 위험 | 선택 창이 이전 이미지를 기본 선택, "프롬프트에 추가" 이미 활성 | getByRole('option',{name:파일명}) 명시 클릭 + 칩 수 확인 |
FLOW-UI.md §4 |
| 애셋 목록 아래 항목 안 보임 | 지연 로드 | input[placeholder="애셋 검색"]에 파일명 입력 |
FLOW-UI.md §9 |
| 설정 팝업이 안 열림 | 다이얼로그 닫은 직후 첫 클릭은 포커스만 | [role=radio] 개수 0이면 재클릭(최대 3회) |
FLOW-UI.md §3 |
| 생성이 이미지/16:9/x2로 나갈 위험 | 팝업 기본값 = 이미지 Nano Banana 2.1 16:9 x2 | 매번 동영상→프레임→9:16→해상도→길이→x1 선택, 비용 링크로 단가 검증 |
FLOW-UI.md §3 |
| 토글 클릭 에러 "did not change checked state" | 스위치가 checkbox 클릭에 반응 안 함 | 좌표 mouse.click + aria-checked 재확인 |
FLOW-UI.md §0 |
| 스크롤이 안 됨, 화면 밖 타일 없음 | .cdk-virtual-scrollable 가상 스크롤 |
그 요소 scrollTop 조작 |
FLOW-UI.md §9 |
| O7 자리에 S6 영상 저장(1회) | 프롬프트 박스 내부 스크롤로 텍스트 y가 위로 튐, "y 이하" 조건이 한 행 위를 잡음 | 35자 매칭 후 가장 가까운 ⋮ 선택 + suggestedFilename()으로 즉시 대조 |
FLOW-UI.md §9, v1 오퍼레이터 보고 |
| 다운로드 하위메뉴 안 뜸 / 좌표 어긋남 | 숨은 옛 메뉴까지 잡힘, 첫 행 메뉴 좌표 약 1.25배 어긋남 | elementFromPoint로 보이는 항목만, 좌표 2회 연속 같을 때만 클릭, hover 4회 재시도 |
FLOW-UI.md §9, v4 기록 |
| 결과 영상이 상자에 소재로 들어가고 모드가 소재로 바뀜(1회) | 메뉴 이동 중 프롬프트에 추가 눌림 |
상자 X로 비우고 프레임 재선택, 매 제출 전 설정 재확인 |
FLOW-UI.md §9 |
| C 드라이브에 mp4 사본 쌓임 | saveAs와 별개로 Windows Downloads에 제안 파일명 사본 생김 |
D 복사본과 크기 일치 확인 후 Downloads 사본 삭제(v4 10개) | 06_flow/v4/LOG.md |
| "REPL session not found", 탭 사라짐 | Aside 데몬 자동 업데이트, 세션 폴더 ID도 바뀜 | 탭 재오픈·헬퍼 재정의·새 세션 artifacts/mm mkdir·저장 경로 교체 |
FLOW-UI.md §9 Aside 함정 |
| 소재 모드에서 블루 단색 대신 하늘색 그라데이션+그림자 | 첫 프레임이 없어 배경 지시가 약함 | 키잉용은 프레임 모드, 소재 모드 R1·R4 1회 재생성 | 06_flow/ref/LOG.md |
| 8초 설정인데 3초 출력(R4_r2) | 동영상 참조 길이(3초)를 따라감 | 동영상 참조 쓸 땐 길이 확인 후 채택 판단 | 06_flow/ref/LOG.md |
| 회전 중 배경이 다른 장소로 바뀜(V3_F4) | 회전 연출 중 배경 일관성 붕괴 | 1회 재생성, r2 우선 | 06_flow/v3/LOG.md |
| 핀터레스트 결과가 무관 이미지 | "slytherin anime girl"은 5장 뒤 음식·방 추천, "gacha banner anime"은 Gacha Life | 전량 삭제, 검색어 교체(zzz pull·genshin wish·hsr warp·zzz s rank) | 00_ref/pinterest/NOTES.md |
| 컨택트시트에 파일명 라벨 넣기가 막힘 | 이미지 위 글자 합성 금지 hook | 라벨 없이 이미지만, 파일명 정렬 순으로 대응 | 핀터레스트 오퍼레이터 기록 |
python3 -I에서 PIL import 실패 |
-I가 user site-packages 제외 |
스크립트 디렉토리에서 python3 sheet.py로 실행 |
핀터레스트 오퍼레이터 기록 |
6. 다시 돌리는 법
nohup "/mnt/c/Program Files/Aside/Application/Aside.exe" >/dev/null 2>&1 & # Aside 켜기(꺼져 있을 때)
SESS=$(ls -t /mnt/c/Users//.aside/u/0/sessions/ | head -1); A=/mnt/c/Users//.aside/u/0/sessions/$SESS/artifacts/mm; mkdir -p $A
cp /mnt/d/malfoy-meme/02_frames/<새 첫 프레임>.png $A/ # 새 이미지만(기존 애셋은 검색으로 재사용)
# Flow 오퍼레이터 서브에이전트 지시: "06_flow/FLOW-UI.md 그대로, 잡=06_flow/<배치>_jobs.json, 720p 9:16 x1, 동시 5개, 크레딧 상한 N, 결과 06_flow/<배치>/<id>.mp4, 파일명으로 id 대조, Downloads 사본 삭제, LOG.md 기록"
cp $A/<배치접두>_*.mp4 /mnt/d/malfoy-meme/06_flow/<배치>/ && rm -v /mnt/c/Users//Downloads/<제안 파일명들>.mp4 # 크기 대조 후
curl -sL -A "Mozilla/5.0" -o /mnt/d/malfoy-meme/00_ref/ref_<id>.mp4 '<임베드 페이지 video.src>' # 릴스 레퍼런스 새로 받을 때
codex 이미지 생성 파이프라인
1. 정의 · 입력 → 출력
- 한 줄 정의: jsonl 한 줄 = 이미지 한 장.
codex exec(비대화형 codex CLI)를 4개 병렬로 띄워$imagegen(gpt-image-2)으로 그리고, codex 공용 폴더에 떨어진 PNG를 목적지 파일명으로 회수(move)한 뒤, 메인 Claude가 컨택트시트(여러 장을 한 장에 타일로 붙인 확인용 이미지)로 내용↔파일명을 대조해 바로잡는 단계 - 입력: 프롬프트 jsonl(
{"id","prompt","size","output_path","images":[참조 이미지 절대경로]}), 참조 이미지(레퍼런스 프레임·정본 캐릭터 시안·의상 일러) - 출력: 캐릭터 시안 4종(
01_design/canon_{pony,drill,straight}.png,alt_slick.png), 1편 첫 프레임 8장(02_frames/S*.png,turnaround.png), 의상 7벌(02_frames/outfits/O1~O7), 3편 이미지 15장(02_frames/v3/V3_C02~C22) → 뒤 단계(Flow 에셋·첫 프레임, 정지+모션 컷)의 재료 - 배치 5회 · 총 35장 생성(+타임아웃 재시도 1회)
| 배치 | jsonl | 장수 | PARALLEL | 참조 이미지 | 로그 |
|---|---|---|---|---|---|
| 캐릭터 시안 | 01_design/design_r1.jsonl |
4 | 4 | 00_ref/frames/ref_9.5.png 1장 |
01_design/r1.log |
| 1편 첫 프레임 | 02_frames/frames_r1.jsonl |
8 | 4 | canon_* 1~2장 |
02_frames/r1.log |
| 의상 7벌 | 02_frames/outfits_r1.jsonl |
7 | 4 | canon_pony.png |
02_frames/outfits_r1.log |
| S6 로코코 재시도 | 02_frames/s6_r2.jsonl |
1 | 1 | canon_drill + O4_rococo |
02_frames/s6_r2.log |
| 3편 이미지 | v3_images.jsonl |
15 | 4 | 정본 시안 + 의상 0~2장 | 02_frames/v3.log |
2. 흐름도
flowchart TD
A[컷 표·정본 설정] --> B[파이썬 heredoc으로 프롬프트 jsonl 작성<br/>id·prompt·size·output_path·참조 이미지]
B --> C[imagegen_runner.py 실행<br/>PARALLEL=4, TIMEOUT 300 또는 600]
C --> D1[codex exec 잡 1<br/>gpt-5.6-terra 저노력 + $imagegen]
C --> D2[codex exec 잡 2]
C --> D3[codex exec 잡 3]
C --> D4[codex exec 잡 4]
D1 & D2 & D3 & D4 --> E[~/.codex/generated_images/세션폴더/*.png 에 저장]
E --> F[회수: 시작 시각 이후 가장 최근이면서 아직 안 집은 PNG 1장을 output_path로 이동<br/>claimed 집합 + 락]
F --> G{시간 초과?}
G -- 예 --> H[TIMEOUT=600으로 그 잡만 재실행<br/>이미 있는 파일은 건너뜀]
H --> C
G -- 아니오 --> I[ffmpeg hstack·vstack으로 컨택트시트 작성]
I --> J{내용↔파일명 대조<br/>메인 Claude 육안}
J -- 어긋남 --> K[mv로 파일명 교정<br/>임시 이름 거쳐 순환 교체]
K --> I
J -- 일치 --> L[다음 단계 재료로 확정<br/>Flow 첫 프레임·의상 참조·정지 컷]
3. 실제 실행 절차
- 프롬프트 jsonl 작성 — 메인 Claude가
python3 - <<'EOF'heredoc으로 공통 스타일 문장(style/face변수) + 컷별 문장을 이어 붙여 jsonl을 씀 - 크기: 시안1024x1536, 세로 컷1024x1792, 턴어라운드만1792x1024- 규칙: IP 단어 금지, 이미지에 글자 넣지 않음(글자는 코드 층에서 얹음) —V3-STORYBOARD.md177행 - 참조 이미지는images배열에 절대경로. 뒤 배치일수록 앞 배치 결과(canon_*,O*)를 참조로 씀 → 앞 배치 파일명이 틀리면 뒤 배치 전체가 엉뚱한 얼굴을 참조하게 됨 - 러너 실행 —
_tools/imagegen_runner.py(codex-imagegen 스킬 러너에-i참조 이미지 지원을 붙인 판)bash cd /mnt/d/malfoy-meme && TIMEOUT=600 PROMPTS=v3_images.jsonl OUTDIR=/mnt/d/malfoy-meme \ PARALLEL=4 nohup python3 _tools/imagegen_runner.py > 02_frames/v3.log 2>&1 &- 잡 하나가 띄우는 명령(러너 52~56행 요약):python cmd = ["codex", "exec", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox", "-m", os.environ.get("MODEL", "gpt-5.6-terra"), "-c", "model_reasoning_effort=low"] for r in refs: cmd += ["-i", r] # 참조 이미지 첨부 cmd += ["--", instr] # "Use $imagegen to generate ONE image. ... do NOT run any shell commands"- 감싸는 모델 =gpt-5.6-terra+ effortlow(지시문 전달만 하면 되므로 저노력), 실제 그림 = codex 빌트인$imagegen(gpt-image-2) - 참조가 있으면 지시문 앞에 "참조 이미지는 스타일·디자인 안내용, 새 이미지를 만들 것" 문장 자동 삽입 - 재실행 안전:output_path가 이미 있으면 건너뜀(main()에서 todo 필터) - 회수(move) — codex는 PNG를
~/.codex/generated_images/{세션 uuid}/*.png에 떨굼. 러너는 잡 종료 후 30초 동안 1초 간격으로 아래 함수를 불러 1장을 집어shutil.movepython for png in CODEX_IMG.glob("*/*.png"): # 세션 구분 없이 전 폴더 if p in _claimed: continue # 이미 집은 파일 제외 if m > after_ts and (best is None or m > best[1]): best = (png, m) # 내 시작 시각 이후 "가장 최근" 1장 _claimed.add(str(best[0])) # 락 안에서 등록 → 이중 회수 방지- claimed 집합 = 이미 회수한 파일 경로 목록. 같은 PNG를 두 잡이 동시에 가져가는 것만 막음. 어느 잡의 그림인지는 모름 - 컨택트시트 작성 — ffmpeg로 결과를 한 장에 타일링
bash cd 02_frames/v3 && L=$(ls V3_*.png); ffmpeg -v error -y $(for f in $L; do echo -i $f; done) \ -filter_complex "...scale=-1:420,pad=240:420...[v0]..[v4]hstack=5[a];...[a][b][c]vstack=3" ../v3_sheet.jpg- 시트 보관:00_SHEETS/1_캐릭터시안4종.jpg,3_의상7벌.jpg,5_3편이미지15장_이름교정전순서.jpg(교정 전 순서 그대로 보존) - 내용↔파일명 대조 노드(스왑 교정) — 메인 Claude가 시트를 보고 프롬프트와 그림 내용을 대조, 어긋난 파일을
mv로 교정. 순환 스왑이라 임시 이름(tmp_turn.png,t,_t/)을 거쳐 옮김 - 1편 첫 프레임(3장 순환):bash mv S3_closeup.png tmp_turn.png && mv S5a_fan.png S3_closeup.png && mv turnaround.png S5a_fan.png && mv tmp_turn.png turnaround.png- 3편:_t/폴더에 내용 기준 이름으로 빼 두었다가 일괄 복귀(C03·C04·C06 3장 순환 + C11↔C12 맞스왑 + C13·C14·C15 3장 순환) - 교정 후 시트 다시 만들어 재대조 - 타임아웃 재시도 — 실패분만
TIMEOUT=600으로 다시 실행. 이미 있는 파일은 건너뛰므로 같은 jsonl 그대로 써도 됨
4. 동시성 · 속도
- 동시 4개: 러너 기본값
PARALLEL=4. 이유(추정): codex 동시 8개 상한 규칙 안쪽 + 한도가 ChatGPT 계정 단위라 스폰을 늘려도 처리량이 복제되지 않음(SKILL.md 19행) + 4장 시안·7벌 의상 규모엔 4로 충분. S6 재시도만 1장이라 1 - 장당 소요(로그 실측, 잡 시작~회수)
| 배치 | 장당 | 배치 전체 | 실효 처리량 |
|---|---|---|---|
| 캐릭터 시안 4장 | 117~140초 | 2.3분 | 약 35초/장 |
| 1편 첫 프레임 8장 | 98~121초 | 3.9분 | 약 29초/장 |
| 의상 7벌 | 84~181초 | 5.0분 | 약 43초/장 |
| S6 로코코 1장 | 1차 302초 timeout → 재시도 269초 | 4.5분 | - |
| 3편 15장 | 82~107초 | 6.6분 | 약 26초/장 |
- 실패 0(타임아웃 1회 제외). 거부(refusal)·무이미지 0건
- 오래 걸리는 쪽: 참조 2장 + 무거운 의상(로코코 드레스 269초, 아이돌 181초). 300초 상한은 이런 컷에 빠듯함
5. 함정과 해결
| 증상 | 원인 | 해결 | 근거 파일 |
|---|---|---|---|
| 결과 그림이 다른 잡 이름 파일에 들어감(오귀속·스왑). 배치마다 발생: 시안 4종 중 4, 첫 프레임 8장 중 3, 의상 7벌 중 4, 3편 15장 중 8 | 회수가 세션 uuid로 거르지 않고 "내 시작 시각 이후 가장 최근·미회수 PNG"를 집음. 4개가 비슷한 시각에 끝나면 먼저 회수 루프를 돈 잡이 남의 그림을 가져감. claimed는 이중 회수만 막고 오귀속은 못 막음 | 컨택트시트로 내용 대조 후 mv 교정 → 재대조. 시안은 4장 전부 이름 재부여(r1_A_straight→canon_pony, r1_B_slick→canon_drill, r1_C_drill→canon_straight, r1_D_pony→alt_slick) |
_tools/imagegen_runner.py newest_unclaimed(), 00_SHEETS/5_3편이미지15장_이름교정전순서.jpg, codex-imagegen SKILL.md §3(72·76행, 50컷 중 8컷 스왑 실측) |
| 시안 이름이 틀린 채 넘어가면 뒤 배치 참조가 전부 오염 | 첫 프레임·의상·3편 jsonl이 canon_pony.png 등 파일명으로 참조 |
시안 대조·교정을 다음 배치 jsonl 작성 전에 끝냄(대조 노드가 다음 배치의 선행 조건) | 02_frames/frames_r1.jsonl, outfits_r1.jsonl, v3_images.jsonl의 images |
S6_rococo 1차 실패 [fail] S6_rococo (302s) timeout |
참조 2장 + 로코코 드레스 디테일로 생성이 300초 상한 초과 | TIMEOUT=600으로 그 1장만 재실행 → 269초 성공. 3편 15장도 처음부터 TIMEOUT=600 |
02_frames/s6_r2.log, 02_frames/s6_r2.jsonl |
| 타임아웃 뒤 고아 PNG 위험 | 시간 초과로 끊긴 잡의 그림이 뒤늦게 떨어지면 다음 잡이 "최신·미회수"로 집어 갈 수 있음(추정) | 재시도 전에 find ~/.codex/generated_images -name "*.png" -mmin -8로 최근 파일 확인 후 실행 |
세션 기록(재시도 명령) |
| 이미지에 글자·IP 단어 | gpt-image-2가 프롬프트 단어를 그림에 새길 수 있음 | 프롬프트에서 IP 단어 제거, 글자는 코드 오버레이 층에서만. 생성 이미지 위 글자 합성 금지 | V3-STORYBOARD.md 177행 |
| C 드라이브 차오름 | 회수는 move라 공용 폴더에 원본이 안 남지만, 타임아웃·미회수분은 ~/.codex/generated_images(C 쪽 WSL 디스크)에 남음. 현재 세션 폴더 383개 |
배치 후 du -sh ~/.codex/generated_images 확인, 필요 시 정리 |
세션 기록(디스크 점검 명령) |
개선안
- 세션 id로 회수: codex exec --json 출력(또는 stderr)에서 세션 uuid를 파싱해 generated_images/{uuid}/만 보게 하면 오귀속 0. SKILL.md는 "버전 의존적이라 비권장"으로 적었으나 현재 CLI 0.159.2 기준 재실측 가치 있음(추정)
- 차선: 잡마다 시작 전 세션 폴더 목록 스냅숏 → 종료 후 새로 생긴 폴더 중 내 프로세스가 연 것만 회수
- 대조를 쉽게: 컨셉이 확 다른 컷끼리 한 배치에 묶기(같은 캐릭터·같은 배경 컷은 서로 다른 배치로). 이번 스왑도 내용이 뚜렷이 달라 육안 판별이 가능했음
- 대조 자동화: 회수 직후 시트를 자동 생성하고 jsonl id·프롬프트 앞 문장을 타일 밑에 붙여, 메인은 "어긋난 칸만 지목"하게
- 오래 걸리는 컷(참조 2장·화려한 의상)은 처음부터 TIMEOUT=600
6. 다시 돌리는 법
cd /mnt/d/malfoy-meme
# 1) jsonl 한 줄 = {"id","prompt","size":"1024x1792","output_path":"02_frames/v3/X.png","images":["/mnt/d/malfoy-meme/01_design/canon_pony.png"]}
TIMEOUT=600 PROMPTS=v3_images.jsonl OUTDIR=/mnt/d/malfoy-meme PARALLEL=4 nohup python3 _tools/imagegen_runner.py > 02_frames/v3.log 2>&1 &
tail -f 02_frames/v3.log # "=== done: N ok / M fail" 나오면 끝, 실패분은 같은 명령 재실행(있는 파일 건너뜀)
# 2) 컨택트시트 만들고 내용↔파일명 대조, 어긋나면 임시 이름 거쳐 mv 교정 → 시트 다시 만들어 재대조
cd 02_frames/v3 && L=$(ls V3_*.png); ffmpeg -v error -y $(for f in $L; do echo -i $f; done) -filter_complex "$(i=0; for f in $L; do echo -n "[$i]scale=-1:420,pad=240:420:(ow-iw)/2:0[v$i];"; i=$((i+1)); done)[v0][v1][v2][v3][v4]hstack=5[a];[v5][v6][v7][v8][v9]hstack=5[b];[v10][v11][v12][v13][v14]hstack=5[c];[a][b][c]vstack=3" ../v3_sheet.jpg
매팅 파이프라인
1. 한 줄 정의 · 입력 → 출력
- 정의: Flow가 만든 블루 배경(#1E3CFF) 캐릭터 영상에서 사람만 오려 내 투명 배경 영상으로 만드는 단계. matte = 피사체만 남기는 투명도 마스크
- 입력:
06_flow/<배치>/*.mp4(720x1280, 블루 배경 컷만) - 출력:
05_matte/<배치>/<컷>.webm(VP9 alpha WebM) +<컷>_qc.jpg(품질 확인 시트) +<컷>.log(실제 사용 프로바이더·초/프레임) - 실적: 34컷 전부 webm·QC 시트 짝 맞음, 남은
.part파일 0 (v1 10 · extra 9 · ref 4 · v3 1 · v4 10) - 인월드 컷(S6 대연회장·castle·ballroom)은 매팅 안 하고 원본 그대로 합성으로 넘어감
2. 흐름도
flowchart TD
A[Flow 블루 배경 영상 mp4] --> Q{큐: 인월드 컷인가}
Q -- 예 --> SKIP[건너뜀, 원본 그대로 합성]
Q -- 아니오 --> E{이미 완성 webm 있나}
E -- 예 --> SKIP2[건너뜀]
E -- 아니오 --> K[WSL 키어 keyer.py 시작]
K --> W[Windows 파이썬 워커 aimask_dml.py 기동]
W --> WD[워커가 같은 영상을 직접 디코드]
WD --> G[AMD 그래픽카드 DirectML로 isnet-anime 추론]
G --> M[마스크만 표준출력으로 WSL에 회수]
K --> D[WSL ffmpeg 디코드 rgb24]
D --> S[시드: 파랑이 확실한 배경 픽셀]
M --> S
S --> P[로컬 플레이트: 화면색 지도 push-pull 채움]
P --> AL[색차 키 알파: 머리카락 가닥 담당]
M --> GB[가비지 매트: 마스크 40픽셀 팽창 바깥은 투명]
M --> CO[코어 보호: 마스크 14픽셀 침식 안쪽은 불투명]
AL --> MIX[알파 합치기 = 최대값 알파 곱 가비지, 코어]
GB --> MIX
CO --> MIX
MIX --> C[색 복원: 언믹스, 블루 디스필, 가장자리 색 번짐]
C --> ENC[VP9 알파 인코딩, 이름 끝 part.webm]
ENC --> QC[품질 확인 시트 25 50 75 퍼센트 지점]
QC --> R[완료 시 이름 변경 컷.webm]
W -- 그래픽카드 실패 --> CPU[그 컷만 WSL CPU rembg로 재시도]
CPU --> S
3. 실제 실행 절차
3-1. 방식 비교 → 하이브리드 선택
- 근거: 05_matte/README.md 비교표(720x1280, CPU 12스레드, 다른 작업과 CPU 공유 상태 실측), 비교 이미지 review/cmp_S4_clean.jpg·cmp_S4_deg.jpg·cmp_S4_clean_hf.jpg·tune_S4_deg.jpg·ref_ai_compare_f36.jpg
- 테스트 소스: src/fake_*_clean.mp4(블루 정지 이미지에 줌·회전·motion blur) / fake_*_degraded.mp4(세로 광량 감쇠·바닥 그림자·보라 색 틀어짐·crf28 압축 = 스트레스 테스트), 생성 스크립트 bin/make_fake.py. 모델별 속도는 bin/bench_ai.py, 크롭 비교는 bin/crops.py
| 방식 | 머리카락 가닥 | 망토 가장자리·초록 안감 | 그림자·그라디언트 배경 | 초/프레임 | 판정 |
|---|---|---|---|---|---|
| (a) ffmpeg chromakey + despill | 보라 프린지 | 안감이 청록으로 변색 | 바닥 보라 그림자 남음 | ~0.02 | 탈락 |
| (b) rembg isnet-anime 단독 | 가닥 살지만 파란 프린지(디스필 없음) | 양호 | 지움 | 1.1~1.4 | 단독은 탈락, AI 가이드로 재사용 |
| (b) rembg u2net_human_seg | 머리카락 대량 소실 | 치마 소실 | - | 0.3 | 탈락 |
| (b) rembg birefnet-general-lite | - | - | - | >300, RAM 7GB | 불가 |
| (c) HyperFrames remove-background | 머리 안쪽 파란 덩어리(u2net_human_seg 기반) | 파란 헤일로 | - | 0.42 | 탈락 |
키 단독(--fast) |
깨끗, 가닥 끝까지 | 안감 무손상 | 발밑 그림자가 반투명 얼룩 | 0.07~0.10 | 깨끗한 화면 전용 |
| (d) 하이브리드 | 깨끗 | 무손상 | 지움 | 1.25~1.46 (CPU) | 채택 |
- 선택 이유: 색차 키는 머리카락 가닥·안감을 살리지만 그림자 낀 배경을 못 지움, AI는 배경 판정은 잘하지만 가장자리가 파랗고 거침 → 둘의 장점만 나눠 맡김
- ffmpeg chromakey는 매팅에선 탈락했지만 3·5편 C18 Flow 원본 직합성에만 별도 사용(
07_render/v3/tools/charcomp.sh, 이 섹션 범위 밖)
3-2. 하이브리드 구조 (bin/keyer.py, 250줄)
- 역할 분담
- 색차 키 = 머리카락 가닥·망토 가장자리 (반투명 경계 담당)
- isnet-anime AI 마스크 = 쓰레기 제거(가비지 매트) + 몸통 보호(코어) + 어두운 배경도 시드로 잡게 기준 완화
- 프레임당 5단계
1. 시드(seed, 확실한 배경 픽셀): AI 없으면 d > 60 & d > 0.55·B, AI 있으면 마스크 13x13 팽창 바깥에서 d > 25 & d > 0.3·B (d = B − max(R,G))
2. 로컬 플레이트(local plate, 화면색 지도): 1/4 해상도 push-pull 채움, 시간축 평활 0.5 (반짝임 방지)
3. 알파 = 1 − d / 플레이트의 d, clip black 0.12 · clip white 0.85
4. AI 가이드 합치기
5. 색: 플레이트 기준 언믹스 → 블루 디스필(B ≤ max(R,G), 초록 안감은 G가 높아 무손상) → 투명 영역 RGB를 가장자리 색으로 번지게 채움(4:2:0 인코딩 파란 테 방지)
- AI 가이드 합치기 핵심 (keyer.py 4단계 인용)
g = cv2.resize((m > 0.05).astype(np.float32), (ws, hs), interpolation=cv2.INTER_AREA) > 0
g = cv2.dilate(g.astype(np.uint8), k(max(a.garbage_px // 4, 1))).astype(np.float32)
g = cv2.GaussianBlur(g, (0, 0), a.garbage_px / 12)
garbage = cv2.resize(g, (f.shape[1], f.shape[0]), interpolation=cv2.INTER_LINEAR)
core = cv2.erode((m > 0.9).astype(np.uint8), k(a.core_px)).astype(np.float32)
core = cv2.GaussianBlur(core, (0, 0), a.core_px / 3)
alpha = np.maximum(alpha * garbage, core)
- 가비지 매트는 1/4 해상도에서 계산(max-pool로 줄여 바깥으로만 커짐) → 키어 CPU 몫 약 3배 빨라짐, 알파 차이 최대 1/255. 옛 전해상도 계산은
-- --garbage-full - 기본 파라미터:
--garbage-px 40 --core-px 14 --despill max --plate-smooth 0.5 --despeckle 1(알파 0.08 미만 → 0)
3-3. 워터마크·바람 입자 제거
- 문제: Flow 우하단 ✦ 워터마크(반투명 흰색, 360x640 기준 x≈300·y≈580, 지름 약 20px)는 블루 위에서 또렷하고, 키만 쓰면 남음(06_flow/FLOW-UI.md 7절). 겨울 코트 컷 V1_S5_O5는 0~1.5초에 흰 바람 구름·낙엽 입자가 블루 위를 지나감(06_flow/v1/LOG.md)
- 처리: 별도 delogo 없이 하이브리드 매팅 단계에서 지워짐. 결과 보고: T1 QC "우하단 ✦ 워터마크도 지워짐"(10-07 18:32), "겨울 코트 컷의 낙엽·바람 잔여물은 깨끗하게 빠짐"(10-07 19:44)
- 원리(코드 기준 추정): 흰 워터마크·입자는 파랑이 우세하지 않아 색차 키만으론 불투명으로 남음 → AI 마스크 40px 팽창 바깥이라 가비지 매트(×0)에서 지워짐. 캐릭터 40px 안쪽에 겹치면 남을 수 있음(추정, 실측 사례 없음)
- 반대로 살린 것: 배틀 컷 마법진·아이돌 컷 반짝이 VFX는 캐릭터 가까이에서 반투명으로 남겨 합성 때 이득으로 씀
3-4. WSL CPU → Windows GPU(DirectML) 이전
- 계기: 처음 워커가 "WSL에 CUDA 없음"에서 CPU로 감 → 사용자 "걍 gpu쓰면되잔아"(10-07 18:36) → AI 마스크만 Windows 쪽으로 포팅
- 구조
- 키어 본체 keyer.py는 WSL venv /mnt/d/_cache/venvs/matte(rembg 2.0.85)에 그대로
- AI 마스크만 Windows Python 프로세스 bin/aimask_dml.py(104줄)로 분리, venv D:\_cache\venvs\dml(Python 3.11, onnxruntime-directml 1.24.4, imageio-ffmpeg, numpy, pillow, opencv-headless)
- 그래픽카드 = AMD RX 6600, DirectML = Windows의 범용 GPU 추론 API(CUDA·ROCm 없이 AMD 사용)
- WSL↔Windows 프로세스 연결 방식
- keyer.py --ai-backend dml → WSL에서 Windows python.exe를 subprocess.Popen으로 직접 실행(WSL interop), 경로는 wslpath -w로 변환, 환경변수는 WSLENV="U2NET_HOME/w"로 경로 변환 전달
- 프레임을 보내지 않음: WSL→Windows 파이프가 ~0.8MB/s라 720p 한 장에 3.5초(실측). 그래서 워커가 같은 영상 파일을 직접 디코드(imageio-ffmpeg 내장 ffmpeg.exe, 같은 rgb24 인자)
- 돌아오는 방향(Windows→WSL, 30MB/s 이상)으로 uint8 마스크(H×W 바이트)만 stdout 전송
- 워커 stderr 첫 JSON 줄에 실제 프로바이더 기록, DML이 없으면 즉시 에러(조용한 CPU 폴백 금지)
cmd = [WIN_PY, winpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "aimask_dml.py")),
winpath(os.path.abspath(a.input)), "--every", str(max(a.ai_every, 1)), "--model", a.ai_model]
self.p = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
cwd="/mnt/d", env=env)
...
if a.ai_backend == "dml" and "DmlExecutionProvider" not in info.get("providers", []):
raise RuntimeError(f"DirectML provider not active: {info}")
- 워커 내부 3스레드 파이프라인: 디코드·전처리 스레드 → 메인 GPU 추론 → 후처리·쓰기 스레드, 큐 크기 4. 키어 쪽 수신 큐 48장 → 워커가 키어보다 앞서 달림
- rembg를 import하지 않음(Windows에서
import rembg만 ~40초): rembg 2.0.85DisSession.predict와 같은 전·후처리를 직접 구현(LANCZOS 1024 리사이즈, /max, mean 빼기, min-max 정규화). CPU rembg 대비 마스크 차이 최대 1/255, 임계값 0.05·0.3·0.9 뒤집힘 0픽셀(4프레임 실측) - 세션 옵션
ORT_ENABLE_BASIC(ALL은 시작 +8초, 속도 동일), 워커 시작 ~7초, 첫 실행은 Windows Defender 스캔으로 1분 넘을 수 있음 - 진단용
--ai-backend wincpu= 같은 워커를 CPU 프로바이더로
3-5. 단일 컷 실행 (matte.sh)
cd /mnt/d/malfoy-meme/05_matte
./matte.sh in.mp4 out/S4.webm # 기본 = 하이브리드 + GPU, QC 시트 out/S4_qc.jpg 자동
./matte.sh in.mp4 out/S4.webm --cpu # AI 가이드를 WSL CPU rembg로
./matte.sh in.mp4 out/S4.webm --fast # 키만, 깨끗한 화면용
- 환경:
U2NET_HOME=/mnt/d/_cache/u2net(Windows 쪽D:\_cache\u2net),HF_HOME=/mnt/d/_cache/hf,ORT_LOGGING_LEVEL=3 - 기타 옵션:
--ai-every N(N프레임마다 AI, 2면 약 1.6배 빠름·빠른 동작은 위험),--mov PATH(ProRes 4444),--png DIR(RGBA PNG 시퀀스),--crf N,--start/--duration,-- <keyer 인자>
3-6. 출력 형식
- VP9 alpha WebM 기본: libvpx-vp9 -pix_fmt yuva420p -crf 18 -b:v 0 -auto-alt-ref 0 -row-mt 1 -deadline good -cpu-used 4 -metadata:s:v:0 alpha_mode=1
- 이유: HyperFrames <video>에서 Chrome 네이티브로 알파 재생. ProRes 4444 MOV(prores_ks 4444 yuva444p10le)도 되지만 2초에 60MB로 무거움. 실제 크기 예: V1_S4.webm 7.8MB
- HyperFrames 배치: <video id="char" class="clip" src="S4.webm" data-start=".." data-duration=".." muted playsinline>, 불투명도 애니메이션은 감싼 div에
- QC 시트: 25·50·75% 지점을 체커 | 빨강 | 알파 흑백으로 나란히(bin/composite.py sheet, 축소 0.35). 알파 디코드는 -c:v libvpx-vp9 강제(ffmpeg 네이티브 vp9 디코더는 알파를 버림)
3-7. 큐 일괄 처리 (_tools/matte_queue.sh [SRC] [DST])
- 규칙
- 인월드 컷 건너뜀: case "$b" in *S6*|*castle*|*ballroom*) continue;;
- 완성 DST/<컷>.webm 있으면 건너뜀, 20초 안에 수정된 파일(다운로드 중일 수 있음)은 다음 바퀴로
- 새 파일 없으면 30초마다 재확인, IDLE_MIN(기본 60)분 동안 없으면 종료
- 쓰는 중엔 <컷>.part.webm → 성공 시 mv로 이름 변경. 중간에 죽어도 반쪽 파일이 완성본으로 안 보이고, 재실행 시 .part부터 지우고 새로 만듦
- GPU 실패 시 그 컷만 CPU로 재시도
rm -f "$DST/$b.part.webm" "$DST/$b.part_qc.jpg"
if "$MATTE" "$f" "$DST/$b.part.webm" --gpu > "$DST/$b.log" 2>&1 \
|| { echo "[retry-cpu] $b"; "$MATTE" "$f" "$DST/$b.part.webm" --cpu >> "$DST/$b.log" 2>&1; }; then
mv -f "$DST/$b.part.webm" "$DST/$b.webm"; mv -f "$DST/$b.part_qc.jpg" "$DST/${b}_qc.jpg" 2>/dev/null
echo "[ok] $b $(date +%T) $(grep -o '"matte_s_per_frame": [0-9.]*' "$DST/$b.log" | tail -1)"
- 3편은 인월드 컷이 섞여 있어 블루 컷 1개만 심볼릭 링크 폴더로 분리:
06_flow/v3_blue/V3_F7_C18.mp4 → 06_flow/v3/V3_F7_C18.mp4 - 4편은 다운로드를 기다리는 감시 루프로 기동: 60초 간격 최대 120회, webm 10개 차면 종료
- 큐 로그:
05_matte/v1/queue.log,extra_ref_queue.log,ref_v3_queue.log,v4_queue.log
3-8. 캐시·임시 파일 위치 (C 드라이브 금지)
- venv·모델·pip 캐시 전부 D:\_cache(/mnt/d/_cache/venvs/matte, D:\_cache\venvs\dml, D:\_cache\u2net, D:\_cache\pip), Windows TEMP D:\_cache\tmp\wintmp
- 10-07 19:53 재기동부터 TMPDIR=/mnt/d/_cache/tmp 추가(matte.sh의 mktemp -d QC 임시 폴더 포함). 이유: WSL 가상 디스크 ext4.vhdx가 임시 파일로 75GB까지 부풀고 안 줄어듦
4. 동시성·속도
동시성
- 처음(18:34~): 큐 1줄, CPU. README 원칙 "무거운 처리는 한 번에 하나씩"(하이브리드 RAM 약 1.9GB)
- GPU 이전 후(19:54~): 큐 2줄 동시. 이유 = AI 몫이 GPU로 빠져 CPU 여유가 생김, 사용자 "gpu도 빡시게 갈궈"
- 라인 A: extra → (원래 ref까지 체인)
- 라인 B: ref → v3_blue
- 두 줄이 같은 GPU를 나눠 써도 추론 0.05~0.1초/장 유지(extra·ref 로그가 19:54~20:00에 겹침)
- 큐 명령은 _tools/heavy.sh(동시 2개 세마포어) 밑에서 돌리지 않았음(실행 명령 기준). 대신 큐 자체가 컷을 순서대로 하나씩 처리
속도 실측 (각 <컷>.log 첫 줄 matte_s_per_frame)
| 구간 | 백엔드 | 초/프레임 | 비고 |
|---|---|---|---|
| V1_S3 · S4 · S5_O1 | CPU | 1.281 · 1.498 · 1.413 | 기준값 1.3~1.5 |
| V1_S5_O2 · O3 · O4 · O5 | CPU | 1.881 · 1.81 · 5.071 · 8.505 | 다른 워커와 CPU 경합(추정). O5는 4초 컷에 827초 |
| V1_S5_O6 · O7 | GPU | 2.591 · 1.603 | 이전 직후. 워커 wall 0.581s/mask = 디코드·CPU 쪽 병목, 첫 실행 Defender 스캔 가능성(추정) |
| V1_S7 | GPU | 0.49 | |
| extra 9컷 | GPU | 0.119~0.283 | |
| ref 4컷 · v3 1컷 | GPU | 0.166~0.231 · 0.267 | 2줄 동시 실행 중 |
| v4 10컷 | GPU | 0.086~0.22 | V4_O1_snap 96프레임 벽시계 9.6초 |
- GPU 추론 자체(
gpu_s_per_mask): 0.051~0.096초/장 - 정리: CPU 1.3~1.5초/프레임 → GPU 0.09~0.5초/프레임(경합 없는 구간 기준). 4초 컷이 2~3분 → 10~30초
5. 함정과 해결
| 증상 | 원인 | 해결 | 근거 파일 |
|---|---|---|---|
| 머리카락에 보라 프린지, 초록 안감이 청록으로 | ffmpeg chromakey가 전역 단일 키색 + 디스필 | 로컬 플레이트 색차 키 + B ≤ max(R,G) 디스필 |
05_matte/README.md 비교표, review/cmp_S4_*.jpg |
| 머리 안쪽 파란 덩어리 | HF remove-background = u2net_human_seg(사람 사진용) | 애니 특화 isnet-anime를 가이드로만 사용 | review/cmp_S4_clean_hf.jpg, review/ref_u2net_human_seg_f36.jpg |
| 발밑 그림자가 반투명 얼룩 | 키만 쓰면 어두운 배경을 시드로 못 잡음 | AI 마스크 바깥에서 시드 기준 완화 + 가비지 매트 | review/tune_S4_deg.jpg, keyer.py 1단계 |
| 워터마크·바람 입자 잔여 | 흰색이라 키가 불투명으로 판정 | 가비지 매트(AI 마스크 40px 바깥 ×0)로 제거 | 06_flow/FLOW-UI.md 7절, 06_flow/v1/LOG.md V1_S5_O5 |
| 파란 소품·하이라이트가 뚫림 | 키 기반이라 파랑 = 배경 | 몸통 안쪽은 코어가 막음, 가장자리 14px는 못 막음 → Flow 프롬프트에 파란 조명·반사 금지 | README.md 한계 절 |
| WSL에서 GPU 못 씀 | WSL에 CUDA·ROCm 없음, 그래픽카드 AMD | Windows Python + onnxruntime-directml 워커로 분리 | bin/aimask_dml.py 머리 주석 |
| 프레임 전송에 장당 3.5초 | WSL→Windows 파이프 ~0.8MB/s | 워커가 영상 직접 디코드, 마스크만 반대 방향으로 회수 | keyer.py DmlMasker 주석 |
| Windows 워커 시작 40초+ | import rembg(pymatting·numba) 로딩 |
rembg 없이 같은 전·후처리 직접 구현 | aimask_dml.py 머리 주석 |
| GPU가 조용히 CPU로 떨어질 위험 | onnxruntime 프로바이더 폴백 | 프로바이더 목록 검사, DML 없으면 즉시 에러 | keyer.py DmlMasker |
| VP9 알파가 QC에서 사라짐 | ffmpeg 네이티브 vp9 디코더가 알파 무시 | QC 추출 때 -c:v libvpx-vp9 강제 |
matte.sh QC 부분 |
| 반쪽 파일이 완성본으로 남을 위험 | 큐를 중간에 죽임 | .part.webm로 쓰고 성공 시 rename |
_tools/matte_queue.sh |
[fail] V1_S5_O6 (19:16~19:24) |
원인 기록 없음, 로그가 GPU 재실행으로 덮어써짐. CPU 큐를 GPU로 갈아타던 시점(추정) | 19:25:35 GPU 큐로 재기동, 같은 컷 재처리 성공 | 05_matte/v1/queue.log |
| 같은 폴더(ref) 이중 처리 위험 | 라인 A 체인 extra; ref와 라인 B 체인 ref; v3_blue가 둘 다 ref를 맡음. 건너뛰기 검사는 완성 .webm만 봄 → 같은 컷 동시 처리·서로의 .part 삭제 가능 |
라인 A의 체인 부모 bash -c(PID 2117)만 kill. 자식 extra 큐(PID 2118)는 계속 돌고, 끝난 뒤 ref로 안 넘어감. pkill -f 금지 |
10-07 19:55 세션 기록, extra_ref_queue.log(extra만 처리) |
| C 드라이브 차오름 | WSL ext4.vhdx가 임시 파일로 부풀고 자동 축소 안 됨 |
캐시·venv·TMPDIR 전부 D:\_cache |
README.md 실행 환경, 10-07 19:53 재기동 명령 |
6. 다시 돌리는 법
cd /mnt/d/malfoy-meme
# 폴더 하나 큐로(블루 컷만, GPU 기본·실패 시 CPU)
nohup env TMPDIR=/mnt/d/_cache/tmp _tools/matte_queue.sh 06_flow/v4 05_matte/v4 >> 05_matte/v4_queue.log 2>&1 &
# 두 번째 라인은 다른 폴더만(같은 SRC를 두 줄이 맡지 않게)
nohup env TMPDIR=/mnt/d/_cache/tmp _tools/matte_queue.sh 06_flow/ref 05_matte/ref >> 05_matte/ref_queue.log 2>&1 &
# 단일 컷 확인 후 QC 시트 열기
05_matte/matte.sh 06_flow/v4/V4_O1_snap.mp4 05_matte/v4/V4_O1_snap.webm && explorer.exe "$(wslpath -w 05_matte/v4/V4_O1_snap_qc.jpg)"
# 멈출 땐 정확한 PID만: ps -eo pid,ppid,args | grep matte_queue → kill <PID>
오디오 파이프라인
1. 한 줄 정의 · 입력 → 출력
- 정의: ElevenLabs API로 곡·대사·효과음을 후보 여러 개로 뽑고, 파이썬(librosa·faster-whisper·demucs)으로 실측해서 고른 뒤, 스토리보드 박 격자에 맞게 잘라 붙여 편집용 오디오 묶음을 만드는 단계
- 입력: 스토리보드의 박 격자(BPM·섹션 시각), 대사 대본, 효과음 목록, 가사(3·5편)
- 출력(편별 폴더
04_audio/,v3/,v4/) - 메인 곡 wav(48k) + 같은 내용 mp3, 3·5편은 보컬 분리판(
_inst·_vocals) beats.json: 박·마디·섹션·드롭·브레이크·훅·대사 자리 실측값(편집·render가 이 시각을 그대로 씀)vo/final/: 앞뒤 무음 자르고 atempo(재생 속도만 바꾸고 음높이는 유지하는 ffmpeg 필터)로 길이 맞춘 대사 wavsfx/: 효과음 mp3(용도별 2테이크)- 편별 결과 요약
| 편 | 메인 곡 | 후보 수 | 템포 실측 | 크레딧(character_count 차) |
|---|---|---|---|---|
| 1편 S-RANK PULL | bgm/BGM_MAIN_aligned.wav (bgm_A_swing, 앞 0.920s 자름) |
7 (A~G) | 128.17 | 47,676 → 50,777 = 3,101 |
| 3편 로판 OP(한국어) | v3/song/SONG_MAIN.wav (K9 + K5 꼬리, 30.000s) |
21 (K1~K21 19 + J1 + E1) | 159.97 | 50,777 → 60,715 = 9,938 |
| 4편 HYPER EDIT | v4/BGM_V4_MAIN.wav (bgm4_A 편집, 20.385s) |
4 (A·B·C·Ce) | 170.03 | 60,715 → 62,039 = 1,324 |
| 5편 로판 OP 영어판 | v3/song/SONG_MAIN_EN.wav (E11 재편집, 30.000s) |
12 (E2~E13) | 159.97 격자 | 62,039 → 67,151 = 5,112 |
- 합계 19,475 크레딧(근거: 각 README 핵심 절,
v4/_work/credits.log)
2. 흐름도
flowchart TD
A[스토리보드 박 격자와 섹션 길이] --> B[작곡 계획 JSON 작성<br/>섹션 길이 = 마디 수 × 마디 길이]
B --> C[ElevenLabs 음악 생성<br/>후보 여러 개, 동시 요청 2개]
C --> D[구조 1차 거름<br/>섹션별 음량 실측]
D --> E{노래가 있는 곡인가}
E -- 예 --> F[보컬 분리<br/>demucs]
F --> G[가사 음성 인식 대조<br/>faster-whisper]
E -- 아니오 --> H[박자·드롭·브레이크 실측<br/>librosa]
G --> H
H --> I[후보 선택]
I --> J[앞 무음 자르기·구간 이어 붙이기·꼬리 붙이기<br/>박 격자에 정렬]
J --> K[박 시각 기록 beats.json]
L[대사 대본] --> M[목소리 오디션<br/>같은 대사·같은 시드]
M --> N[음높이 측정과 음성 인식 점수로 목소리 선택]
N --> O[대사 테이크 생성<br/>인식 실패 테이크 탈락]
O --> P[앞뒤 무음 자르기와 재생 속도 맞춤]
Q[효과음 목록] --> R[효과음 생성 2테이크씩]
K --> S[편집 묶음 전달]
P --> S
R --> S
S --> T{사용자 반려}
T -- 한국어 노래 반려 --> U[영어 가사로 같은 계획 재생성]
U --> C
3. 실제 실행 절차
공통: API 키는 ~/.secrets/elevenlabs.env의 ELEVENLABS_API_KEY에서 읽음(값은 [REDACTED]). 모든 스크립트가 같은 방식.
key=open(os.path.expanduser('~/.secrets/elevenlabs.env')).read().split('ELEVENLABS_API_KEY=')[1].split()[0].strip('"\'')
① 곡 생성: composition_plan(섹션별 지시문 + 길이 목록) = 박 격자
- 엔드포인트 POST https://api.elevenlabs.io/v1/music?output_format=mp3_44100_192, 모델 music_v2_5(1편 C만 music_v1), respect_sections_durations: true
- 섹션 duration_ms를 마디 수로 계산해서 넣음 → 섹션 경계가 스토리보드 컷 경계와 같아짐
- 1편 128 BPM: 2마디 = 3,750ms. 청크 6개(인트로 3750 · 임팩트 3750 · 드롭 7500 · 드롭2 3750 · 대사 구간 3750 · 아웃트로 5000) (_work/gen_music.py)
- 4편 170 BPM: ms=lambda bars:int(round(bars*BAR*1000))로 3·6·4·3마디 (v4/_work/gen_music_v4.py)
- 3편 160 BPM: 인트로 3000 · 버스 6000 · 프리코러스 6000 · 코러스 9000(또는 4500×2) · 엔딩 히트 3000 · 아웃트로 3000 (v3/_work/gen_song.py)
- 섹션마다 text([Drop] {…} 형식 지시) + positive_styles·negative_styles. 노래 없는 곡은 negative에 vocals·singing·lyrics
- 변형은 환경변수·인자로: 1편 V2=1(BPM 태그 반복·대사 구간 드럼 제거), 3편 DICTION(발음 또렷이)·BRK2(브레이크 표기 변경)·SPLITC(코러스 2청크 분할), 4편 인자 A/B/C/Ce(장르 묶음·브레이크 위치)
- 응답 헤더 song-id를 로그에 남김(*.log), 요청 본문은 *.request.json으로 저장
chunks=[ ("[Build-up] {...snare roll and white-noise riser building to the drop}",ms(3),[...]),
("[Drop] {explosive drop on the first downbeat: ...}",ms(6),G+["high energy",...]), ...]
body={"composition_plan":{"chunks":[...]},"model_id":"music_v2_5","respect_sections_durations":True}
- 명령 예:
python3 _work/gen_music.py bgm_A_swing music_v2_5 swing,python3 v4/_work/gen_music_v4.py bgm4_A A,DICTION=1 SPLITC=1 python3 v3/_work/gen_song.py song_K9 ko
② 후보 N개
- 1편 7개(A~G: swing·future bass × v1·v2·v3), 3편 21개(한국어 19 + 일본어 J1 + 영어 E1), 4편 4개, 5편 12개(E2~E5 구 구조, E6~E13 새 구조)
- 생성 1곡당 5~13초(로그 NNs 표기 실측)
③ 실측(librosa)
- 1편 _work/analyze.py: beat_track(start_bpm=128)로 박 시각, 0.25s 단위 RMS(음량)·150Hz 미만 저역 에너지 열 출력 → 드롭·무음 브레이크·히트 위치를 눈으로 읽음
- 3편 v3/_work/screen.py: 브레이크 창(13.75~14.9) RMS, 로고 히트 검출 → 믹스 RMS로 1차 거름(K1·K2·K4·K6~K8·K11~K21 여기서 탈락)
- 3·5편 v3/_work/analyze3.py: beat_track(start_bpm=160, tightness=400), 섹션별 RMS(intro·verse·pre·brk·chorus·endhit·logo), 상위 1% 온셋, 보컬 stem의 RMS로 보컬 구간
- 4편 v4/_work/analyze4.py(analyze.py + 선형 BPM 맞춤·드롭·빈 구간·끝 검출), kicks.py(저역 킥 위상)
t,b=librosa.beat.beat_track(y=y,sr=sr,start_bpm=128,units='time')
rms=librosa.feature.rms(y=y,hop_length=int(sr*0.25),frame_length=int(sr*0.25))[0]
lo=S[f<150].mean(0) # 저역(킥) 에너지
④ 보컬 분리 + 가사 인식 검증(노래 곡만)
- demucs htdemucs two-stems(보컬 / 나머지 2갈래로 나눔) → v3/_work/stems/htdemucs/song_*/vocals.wav · no_vocals.wav
- 원곡 믹스 그대로 whisper에 넣으면 인식 0 → 보컬만 뽑아서 판정(v3 README)
- 한국어 v3/_work/asr2.py: faster-whisper small, CPU int8, 섹션 창 3개(2.5~9.5 / 8.75~14.25 / 14.5~24.75)별로 가사 원문과 difflib 비율 + 훅 「딱히너좋아서그런거아니거든」 13자 최장 공통 부분
m=WhisperModel('small',device='cpu',compute_type='int8',cpu_threads=10)
ss,_=m.transcribe(seg,language=lang,beam_size=5,temperature=0,vad_filter=False,condition_on_previous_text=False,...)
hook=difflib.SequenceMatcher(None,HOOK,norm(rr[2]['asr'])[:len(HOOK)*2]).find_longest_match(...).size
- 결과(
asr2_K9.json): K9 프리 0.80 · 코러스 0.82 · 훅 11/13자(「아니거든」→「아니다든」, 「묘약」→「표약」). 버스 창은 「아 아 아…」 환각으로 0.05 - 영어
v3/_work/asr_en.py:small.en+word_timestamps=True로 단어 시각, "its not / not like" 패턴으로 훅 등장 시각 검출 → E11 선택, 단어 시각은 가사 카라오케 자막에 재사용(07_render/v5/overlays/lyrics_timing.json)
⑤ 선택 근거 - 1편 A: 128.16 BPM, 임팩트 → 무음 브레이크 → 드롭 구조가 있고 0.92s만 자르면 S1~S7 경계와 맞음. E는 124.3 BPM 템포 이탈로 탈락 - 3편 K9: 진짜 정지 브레이크(13.59~15.02) + 정시 훅(15.3)을 둘 다 가진 유일 후보. K3는 훅 1s 늦음 - 4편 A: 빌드업 대비 드롭 음량 차가 가장 큼(rms 3→27) - 5편 E11: 브레이크 무음 14.0~16.5, 훅 2회, 코러스 줄 인식 가장 정확
⑥ 박 정렬: 앞 무음 자르기 · 구간 이어 붙이기
- 1편: 원본 앞 0.920s 자름 → 큰 히트 3.773 / 7.484 / 11.233 / 14.990 / 18.734 / 22.487(격자 대비 -17~+23ms), 드롭 7.484(목표 7.5, -16ms). beats.json에 trim_from_raw_s: 0.92
- 4편 v4/_work/build_master4.py: 모델이 빌드업을 1마디 길게 만들어 드롭이 5.647에 옴 → 앞 8박(2.8235s) 자르고, 지시를 안 지킨 브레이크·끝 히트는 편집으로 만듦
TRIM=8*P; DROP=2*BAR; BRK0,BRK1=DROP+5.5*BAR,DROP+6*BAR # 10.588-11.294
HIT=DROP+12*BAR; STOP=HIT-0.5*P # 19.765 / 19.588
out[:,s(BRK0):s(BRK1)-s(0.003)]=0 # 2박 무음 + 12ms 디클릭
rc=ld('../sfx/sfx4_reverse_cymbal_02.mp3'); st=s(BRK1)-s(0.72) # 리버스 심벌 정점을 드롭2에
- 3편
v3/_work/build_master.py: 0~26.49 = K9, 첫 마디가 무음이라 K9 코러스 다운비트(15.00~15.30, 보컬 없는 판) 조각을 0 / 0.375 / 0.75 / 1.125에 4번 붙임, 26.49~30 = K5 꼬리(같은 템포·같은 조)를 당겨 마지막 코드 27.0. 믹스·inst·vocals 3판을 같은 함수로 동시에 조립 - 5편
v3/_work/en/build_master_en.py: E11을 한국어판 격자(+0.018)에 맞춰 구간별 오프셋으로 이어 붙임, 12ms 크로스페이드, 원곡 코드가 1s에 꺼져서 합성 리버브 꼬리 2.6s
SEGS=[(1.5,2.62,0.015),(2.62,14.95,0.39),(14.95,26.8,1.515),(26.8,30.0,1.637)] # (출력 시작, 끝, 원곡 오프셋)
- 정렬 후 다시 analyze 돌려
beats.json기록(박·마디 다운비트·섹션·훅·대사 빈 창vo_free_windows·대사 제안 자리vo_suggested)
⑦ TTS 오디션 → 대사 생성
- _work/tts.py add OWNER VOICE_ID NAME: 공유 라이브러리 목소리를 계정에 추가(/v1/voices/add/{owner}/{id}), 후보는 shared_ko_female.json 등 검색 결과에서 고름
- _work/tts.py tts VOICE_ID OUT TEXT STAB SEED: 모델 eleven_v4, stability 0.35~0.45, similarity_boost 0.8, seed 고정, 오디오 태그([smug]·[haughty]) 사용
- 오디션 = 같은 대사·같은 seed로 후보 3~4명 → vo_eval.py로 F0(기본 음높이, librosa.pyin) 중앙값·음역 폭(5~95% 반음 수) + whisper 인식 일치율
f0,vf,_=librosa.pyin(y,fmin=70,fmax=600,sr=sr)
st=12*np.log2(np.percentile(f0,95)/np.percentile(f0,5)) # 음역 폭(semitone)
r=difflib.SequenceMatcher(None,clean(ref),clean(txt)).ratio()
- 1편: Emmaline F0 253Hz + RP 억양 → 선택. Posh는 표현력(10.8st) 최고지만 성인 음역 232Hz, Michelle Queen 274Hz지만 일상 톤
- 3편: Chaemong 인식 0.91 · 333Hz 선택(Anna Kim 0.84·316 / Yura 0.82·327 / Annie 0.82·349), 소년 Taemin 101Hz · 음역 12.5st · 인식 1.0
- "gems" 발음 필터(1편): Emmaline이 "gems"를 "gens"로 읽는 테이크가 많음 → whisper
small.en으로 인식해서 "gems"가 나온 테이크만 남기고 나머지vo/_rejected/. t1 확률 0.99, t2 0.79 - 비언어음(「Hmph.」「흥!」)은 ASR로 검증 불가 → 귀 확인 표시.
[scoffs]·[sighs]는 웃음·한숨으로 바뀌어 탈락, 「흐, 흥!」은 「흥 흥 흥」 반복이라 「흥!」으로 바꾸고 첫 음절만 자름
⑧ atempo로 대사 길이 맞춤
- v4 TTS의 speed 파라미터는 길이에 반영 안 됨(실측) → ffmpeg atempo로 후처리
- v3/_work/fit_vo.py: librosa.effects.split(top_db=35)로 앞뒤 무음 자름(앞 10ms·뒤 40ms 여유, 8ms 페이드) → _trim.wav, 배속 필요하면 _fitNNN.wav
subprocess.run(['ffmpeg','-v','error','-y','-i',raw,'-af',f'atempo={tempo}',fit],check=True)
- 예: 1편 S6 5.44s → atempo 1.2 → 4.54s(3.75s 창보다 여전히 길어 18.75 시작이면 약 23.1 끝), 3편 V3 1.25배 1.84s · V6 1.15/1.30배 2.82/2.49s
⑨ SFX 생성
- POST /v1/sound-generation, 모델 eleven_text_to_sound_v2, 항목마다 duration_seconds·prompt_influence(0.5~0.7), 용도별 2테이크, 프롬프트 원문은 sfx_prompts.json·sfx3_prompts.json·sfx4_prompts.json
- 편 간 재사용: 도장·부채·글리치는 1편 sfx/, 화이트 플래시·줌 스냅은 3편 v3/sfx/
- 작은 테이크(피크 -13dB: gacha_click_02, camera_shutter_01)는 게인 올려 사용
⑩ 크레딧 측정
- GET /v1/user/subscription의 character_count를 작업 전후로 찍어 차이 = 사용량(v3/_work/credits.py, v4/_work/credits.log: before 60715 20:53:03 / after 62039 (+1324) 20:57:10)
- 곡 생성 과금은 늦게 반영됨 → 생성 직후 값이 작게 보임. 3편은 이 때문에 21곡에서 멈춤, 4편 README도 "반영이 늦을 수 있음" 표기
- 편별 상한: 1·3편 25,000, 4편 8,000, 5편 10,000 안에서 끝냄
⑪ 한국어 보컬 반려 → 영어판
- 10-07 21시 사용자가 3편 한국어 OP 곡(K9, 인식 0.8)을 "영어로" 반려(ElevenLabs 한국어 노래 보컬 품질 불만, 근거: 설명/03_파이프라인_그래프.md)
- 같은 계획에서 가사 text와 Korean lyrics→English lyrics만 교체, 구조도 고침(gen_song_en2.py): 프리코러스 4.5s + 전용 브레이크 청크 3s(1.5s는 편집에서 잘라냄) + 코러스 4.5s×2 → 브레이크 무음 성공률 올라감
- 이후 규칙: 노래는 영어 보컬 기본, 한국어는 대사·자막만. 한국어판(SONG_MAIN.wav)은 그대로 보존
4. 동시성·속도
- ElevenLabs 동시 요청 상한 2개: 넘기면 HTTP 429 받음 → 곡은 2개씩 생성(
v4/README.md기타 절). 3편 SFXsfx3.py는ThreadPoolExecutor(2) - 1편
sfx.py·4편sfx4.py는ThreadPoolExecutor(4)로 짬(짧은 효과음이라 통과한 것으로 추정, 로그상 429 기록 없음) - 곡 로그 완료 시각(파일 수정 시각): K6~K9 20:14:52~20:15:00, K14~K21 20:21:42~20:22:07, E10~E13 21:12:29~21:12:40 → 2~4개가 수 초 간격으로 끝남(배치 단위 기동, 정확한 동시 수는 추정)
- 속도 실측: 곡 1개 생성 5~13초(로그), demucs htdemucs CPU 분리 1곡 약 5분(
demucs2.log, 35초 오디오에 5:19), whisper small CPU int8 10스레드 - 무거운 로컬 처리(demucs·whisper)는 순차. 곡 생성만 2개 병렬
5. 함정과 해결
| 증상 | 원인 | 해결 | 근거 파일 |
|---|---|---|---|
| 드롭·히트가 격자보다 0.8~0.9s 늦음 | 모델이 앞에 여분 리드인을 붙임 | 앞 0.920s(1편)·8박(4편) 자르고 실측 재기록 | 04_audio/beats.json, v4/_work/build_master4.py |
| 지시한 브레이크·끝 히트가 안 나옴 | 4편 후보 4개 모두 2마디 지시 무시, 빌드업 1마디 길게 생성 | 편집으로 2박 무음 + 리버스 심벌 + 반박 정지 + 끝 히트 조각 합성 | v4/README.md |
| 대사 구간 밀도가 드롭과 같음(rms 0.135) | 모델이 "low density" 지시 무시 | 편집에서 대사 아래 BGM -8~-10dB 덕킹 | 04_audio/README.md |
| 1.5s 브레이크 청크 요청 거부 | composition_plan 청크 최소 길이 3,000ms | 프리코러스 안에 「마지막 1마디 정지」 지시를 합침, 영어판은 3s 청크 만들고 편집에서 1.5s 잘라냄 | v3/README.md |
| 가사 인식 0 | 원곡 믹스째로 whisper에 넣음 | demucs로 보컬만 분리 후 인식 | v3/README.md, asr2.py |
| 버스 창 인식 「아 아 아…」 | whisper 반복 환각 | 섹션 창별 비율로 보고 환각 창은 제외, 훅 최장 공통 부분으로 별도 판정 | asr2_K9.json |
| "gems"가 "gens"로 들림 | Emmaline 발음 습관 | small.en 인식으로 "gems" 나온 테이크만 채택, 나머지 _rejected/ |
04_audio/README.md |
| 오디오 태그가 다른 소리로 바뀜 | [scoffs]→웃음, [sighs]→한숨, 「흐, 흥!」→「흥 흥 흥」 |
태그 1개만, 「흥!」으로 바꾸고 첫 음절만 자름 | v3/README.md, fit_vo.py mode first |
| speed 파라미터로 대사가 안 짧아짐 | v4 TTS에서 speed가 길이에 반영 안 됨(실측) | ffmpeg atempo 1.15~1.30 | 04_audio/README.md, fit_vo.py |
| 엔딩 대사 3줄이 2.5s 창에 안 들어감 | 맞춤 후에도 3.75s | 대사 자리를 24.0부터 당기고 겹치는 구간은 보컬 뺀 판(inst)으로 교체 | v3/beats.json vo_suggested |
| 곡 끝 코드가 1s에 꺼짐 | 원곡 아웃트로가 짧음 | 합성 리버브 꼬리 2.6s(5편), K5 꼬리 이어 붙이기(3편) | build_master_en.py, build_master.py |
| 429 응답 | 동시 요청 상한 2개 | 곡 생성 2개씩 | v4/README.md |
| 크레딧이 덜 쓴 것처럼 보임 | 곡 과금 반영 지연 | 생성 직후 값 믿지 않고 여유를 남겨 멈춤(3편 21곡에서 중단) | v3/README.md, v4/_work/credits.log |
| 한국어 보컬 반려 | ElevenLabs 한국어 노래 보컬 품질(인식 0.8이어도 사용자 불만) | 영어 가사 12후보 재생성 → E11, 이후 노래는 영어 기본 | v3/README.md 영어 보컬판 절 |
| 계정 voice library에 후보 목소리 누적 | 오디션마다 voices/add |
1편 5개·3편 6개 추가됨, 필요 없으면 삭제 | 각 README 기타 절 |
6. 다시 돌리는 법
python3 /mnt/d/malfoy-meme/04_audio/v3/_work/credits.py # 시작 전 character_count 기록(스크립트 안 기준값 50777은 바꿔 쓸 것)
cd /mnt/d/malfoy-meme/04_audio/v4/_work && (python3 gen_music_v4.py bgm4_A A & python3 gen_music_v4.py bgm4_Ce Ce; wait) # 동시 2개까지만
python3 analyze4.py ../cand/bgm4_A.mp3 # 박·드롭·빈 구간 실측
python3 build_master4.py ../cand/bgm4_A.mp3 BGM_V4_MAIN # 박 격자 정렬 편집
cd ../../v3/_work && demucs -n htdemucs --two-stems vocals -o stems ../song/song_E11.mp3 # 추정 명령(결과 폴더 구조 기준)
WM=small.en python3 asr_en.py stems/htdemucs/song_E11/vocals.wav # 가사 인식·훅 시각
python3 fit_vo.py && python3 credits.py # 대사 자르기·atempo, 크레딧 차이 확인
합성·render 파이프라인
1. 한 줄 정의 · 입력 → 출력
- 정의: 코드 배경 블록(HyperFrames)·AI 캐릭터 알파 영상(matte)·오디오를 겹쳐 업로드용 mp4 한 개로 굽는 단계. render = HTML 컴포지션을 프레임 단위로 캡처해 영상으로 뽑는 것, composite(합성) = 그 결과 위에 캐릭터·효과를 겹치는 것
- 입력
- 배경 블록 7종:
03_motion/compositions/*.html+ 공용mm-lib.js(1080x1920, 128 BPM 박 단위 타이밍) - 캐릭터 알파:
05_matte/{v1,extra,ref,v4}/*.webm(VP9 알파) - 인월드 클립:
06_flow/**/*.mp4, 정지 일러:02_frames/** - 오디오: BGM·VO·SFX (
04_audio/**,beats.json박 실측) - 출력:
07_render/V1_SRANK_PULL.mp4~V5_ROFAN_OP_EN.mp4(1080x1920·30fps·H.264·AAC 320k),V4_HYPER_EDIT_upload.mp4(CRF 21),V{n}_contact.jpg(1초 컨택트시트)
배경 블록 7종과 layer 변수 (03_motion/README*.md)
| 블록 | id | 길이 | 쓰인 곳 |
|---|---|---|---|
| ① gacha_build | gacha-build |
3.75s | 1편 S1 |
| ② srank_flash | srank-flash |
1.875s | 1편 S2, 4편 판 |
| ③ kinetic_wall | kinetic-wall |
7.516s | 1편 S4, 2편 드롭, 4편 판 |
| ④ beat_cutin | beat-cutin |
3.75s | 1편 S5, 4편 판 |
| ⑤ nameplate_slam | nameplate-slam |
1.875s | 1편 S3, 4편 판 |
| ⑥ ending_stamp | ending-stamp |
3.75s | 1편 S7 |
| ⑦ clip_wall | clip-wall |
7.5s(최대 16s) | 2편 인트로, 4편 판 |
- 공통 변수 palette(a=빨강·검정·흰·금 / b=에메랄드·은·검정·금), layer(all / bg=배경만 / fg=UI만, 배경 투명) |
|||
| - 캐릭터를 끼우는 규칙: 같은 블록을 2번 올림 → bg 슬롯(아래) · 캐릭터 · fg 슬롯(위). 순서는 z-index로 지정(DOM 순서가 z 순서라 track-index만으론 안 됨) | |||
| - 안전지대: 상단 0~230/300px 어둡게(자막용), 큰 타이포는 위·가장자리 |
편별 합성 방식 비교
| 편 | 방식 | HyperFrames가 그린 것 | 캐릭터 합성 주체 | 오디오 | 최종 인코딩 | 근거 |
|---|---|---|---|---|---|---|
| 1편 S-RANK PULL (26.5s) | HyperFrames 2패스 + ffmpeg 알파 합성 | bg 패스 pass_bg.mp4(배경 블록 + S6 인월드 + 오디오 믹스) / fg 패스 pass_fg.mov(이름판·도장·자막, 투명) |
ffmpeg filter_complex overlay (구간별 webm 12조각) + 전체 zoom punch·RGB glitch·비네트·그레인 |
HyperFrames data-automation 덕킹 → 2패스 loudnorm -14 LUFS |
libx264 CRF 16 slow | 07_render/v1/composite.py, v1/hf/index.html |
| 2편 AGENT SHOWCASE (15.75s) | HyperFrames 배경 1장 + 파이썬 가상 카메라 | src/bg.mp4(clip_wall + kinetic_wall + kinetic_wall_t0), src/overlay.png(제목 정지 PNG) |
numpy/cv2 프레임 합성: 키프레임 25개로 캐릭터에만 zoom·pivot·pan | 미리 자른 audio_v2.wav 그대로 mux(라우드니스 처리 코드 없음) |
CRF 18(자막 없는 판) slow | v2/tools/comp.py |
| 3편 ROFAN OP (30s) · 5편 영어판 | HyperFrames 단일 render + 알파 컷만 미리 합성 | 컷 22개·오버레이 5종·오디오 전부 한 프로젝트 | charcomp.sh가 알파 캐릭터를 bgchar.mp4 위에 ffmpeg로 미리 구워 불투명 mp4(C07·C18)로 넣음 |
build.py가 <audio> 볼륨 automation 생성 → finalize.sh 2패스 loudnorm |
HyperFrames -q looks 렌더본 영상 copy |
v3/tools/build.py, charcomp.sh, finalize.sh (v5 = v3 복사본, CUTS·곡·펀치 몇 줄만 다름) |
| 4편 HYPER EDIT (20.385s) | 샷 라이브러리 자동 추출 → edl → comp4 | 배경 판 src/plates.mp4(블록 7개 이어 붙인 37.5s), 타이포 sprite src/typo_png/ 43장 |
comp4.py 프레임 합성: 78컷마다 판 조각 재색칠·알파 클립 속도 램프·키잉 정지 일러·타이포 sprite 이동·틴트 |
audio_v4.wav mux(믹스 스크립트 미확인) |
CRF 18 + 업로드본 CRF 21 | v4/tools/shots.py, edl.py, comp4.py |
- 실측 라우드니스(최종 파일 ebur128, 10-08): V1 -13.9 LUFS / -0.9 dBFS, V2 -13.0 / -0.4, V3 -13.9 / -2.9, V4 업로드본 -14.1 / -1.0, V5 -13.9 / -3.4 → loudnorm을 안 거친 2편만 1 LU 큼
2. 흐름도
flowchart TD
블록["배경 블록 7종<br/>03_motion/compositions"] --> 동기화["sync.sh로 블록 사본 복사<br/>(심볼릭 링크 금지)"]
동기화 --> 호스트["편별 HyperFrames 호스트 index.html<br/>(손으로 작성 또는 build.py 생성)"]
알파["캐릭터 알파 webm<br/>05_matte"] --> 분기{"알파 webm을<br/>HyperFrames에 직접 넣나?"}
분기 -- "아니오(멈춤 위험)" --> 하이브리드["밖에서 합성"]
하이브리드 --> 일편["1편: 배경 패스 + 전경 패스 사이에<br/>ffmpeg overlay"]
하이브리드 --> 이편["2편: 파이썬 가상 카메라<br/>numpy·cv2 프레임 합성"]
하이브리드 --> 삼편["3·5편: 알파 컷만 ffmpeg로<br/>불투명 mp4로 미리 구움"]
하이브리드 --> 사편["4편: 샷 라이브러리 추출 → 컷 목록 생성 → comp4 합성"]
호스트 --> 렌더["HyperFrames render<br/>heavy.sh 동시 2개 · workers 2~4"]
삼편 --> 렌더
렌더 --> 일편
렌더 --> 이편
렌더 --> 사편
렌더 --> 드래프트["드래프트 540p 또는 draft 화질"]
일편 --> 드래프트
이편 --> 드래프트
사편 --> 드래프트
드래프트 --> 시트["1초 간격 컨택트시트로 판정"]
시트 -- "수정 필요" --> 호스트
시트 -- "통과" --> 최종["최종 1080p<br/>CRF 16~18"]
최종 --> 음량["2패스 loudnorm<br/>-14 LUFS · 최대 -1 dBTP"]
음량 --> 업로드["업로드본 CRF 21<br/>+ 최종 컨택트시트"]
3. 실제 실행 절차
-
블록 사본 동기화 (정본
03_motion/compositions는 수정 금지, 편별 프로젝트로 cp) - 1편07_render/v1/sync.sh: 블록 6개 +mm-lib.js를v1/hf/compositions/로 복사(clip_wall 제외) - 2편v2/tools/sync.sh: clip_wall·kinetic_wall 복사 후patch_kw.py로 id를 바꾼 사본kinetic_wall_t0.html생성(같은 컴포지션 두 번 꽂으면 id 충돌), assets는 디렉터리 심볼릭 링크python # v2/tools/patch_kw.py 핵심: 새 id + 로컬 시작 오프셋 변수 t0 s = s.replace('data-composition-id="kinetic-wall"', 'data-composition-id="kinetic-wall-t0"') new = ('var T0 = +V.t0 || 0;\n' ' if (T0 > 0) { var outer = gsap.timeline({ paused: true }); outer.add(tl.tweenFromTo(T0, DUR, { ease: "none" }), 0); tl = outer; }\n' ' window.__timelines["kinetic-wall-t0"] = tl;')- 3·5편tools/sync_overlays.sh: 오버레이 워커 결과overlays/compositions/*.html+ 폰트를 복사 - 4편v4/bg/: kinetic_wall_b·beat_cutin_b 같은 id 바꾼 사본으로 판 7개 나열 -
1편: HyperFrames 2패스 (
v1/hf/index.html) - 루트에pass변수(enum bg/fg/all).pass=bg면.only-fg숨김(배경 블록 + 오디오),pass=fg면 배경 투명 +.only-bg숨김bash heavy.sh npx --yes hyperframes@0.8.140 render . --workers 2 --crf 10 --variables '{"pass":"bg"}' -o build/pass_bg.mp4 heavy.sh npx --yes hyperframes@0.8.140 render . --workers 2 --format mov --variables '{"pass":"fg"}' \ --frames-cache-dir /mnt/d/_cache/hf-frames -o build/pass_fg.mov- 실측: bg 1m56s(134.7MB, beginframe), fg 1m55s(223.8MB, screenshot capture: 투명 mov 출력은 BeginFrame 빠른 경로를 못 탐) -
1편: ffmpeg 알파 합성 (
v1/composite.py draft|final) - 구간표segs(시작, 길이, webm, 소스 시작, 배치) → webm마다-c:v libvpx-vp9로 디코드(알파 유지) →overlay ... enable='between(t,..)'연쇄 → fg mov 덮기 - 배치: 전신 928x1650 하단 정렬(머리가 상단 자막띠 밖), S3는 zoompan 1.00→1.09 클로즈업, S7은 23.9s부터s7_freeze.png정지 반복 - 후처리: 박마다 전체 zoom punch(exp감쇠 식), 전환점 3프레임rgbashift,vignette,noisepython zexpr = "1" + "".join(f"+{a}*gte(in_time,{t:.3f})*exp(-(in_time-{t:.3f})/{d})" for t, a, d in punch) venc = ["-c:v", "libx264", "-profile:v", "high", "-crf", "16" if MODE == "final" else "22", "-preset", "slow" if MODE == "final" else "veryfast", "-pix_fmt", "yuv420p", "-movflags", "+faststart"]- draft는 마지막에scale=540:960→build/draft_540p.mp4, final은 1080p →07_render/V1_SRANK_PULL.mp4 -
2편: 배경 render + 가상 카메라 (
v2/tools/comp.py) -bg/(clip_wall 11.3s + kinetic_wall + kinetic_wall_t0)를 render →src/bg.mp4(2m26s, workers 2, 0.8.139) -overlay/index.html(제목, 투명 정지)을 PNG 한 장으로 render →src/overlay.png- comp.py: 알파 webm을ffmpeg -c:v libvpx-vp9 ... -pix_fmt rgba로 메모리에 디코드(쓰는 구간만), 프레임마다cv2.warpAffine로 가상 카메라, contact shadow → 캐릭터 → 제목 띠 → overlay.png → flash → glitch → 비네트+그레인 뱅크 6장 → raw rgb24를 ffmpeg 파이프로 인코딩 - 키프레임 형식(t, zoom, pivot_x, pivot_y, pan_x, pan_y, ease), 드롭 펀치 1.16→1.00, 얼굴 cut-in 1.95배, hair flip push-in 1.55배bash heavy.sh /mnt/d/_cache/venvs/matte/bin/python tools/comp.py --scale 0.5 --out draft/v2_540_r3.mp4 # 드래프트 heavy.sh /mnt/d/_cache/venvs/matte/bin/python tools/comp.py --scale 1.0 --crf 18 \ --out /mnt/d/malfoy-meme/07_render/V2_AGENT_SHOWCASE.mp4 --audio src/audio_v2.wav # 최종 4분 17초 -
3·5편: 단일 render (
v3/tools/build.py→index.html) -CUTS표 하나가 컷 경계 정본(곡 박 실측이 바뀌면 여기만 고침),VIDEO표에 컷별 (소스, 시작, 배속) -index.tpl.html의%%CUTS_HTML%%·%%OVERLAYS_HTML%%·%%AUDIO_HTML%%자리를 채움 - 알파 캐릭터 컷(C07·C18)은charcomp.sh로 미리 구움:bgchar.html배경을 렌더한renders/bgchar.mp4위에 webm(또는 블루 키잉한 Flow mp4)을 overlay →assets/comp/C07.mp4·C18.mp4(CRF 12,-g 6) - 인월드 Flow 클립은prep_assets.py로 워터마크 delogo +-crf 14 -g 12재인코딩(키프레임 촘촘하게)bash heavy.sh npx --yes hyperframes@0.8.139 render -o renders/draft4.mp4 --workers 4 -q draft . # 1m12s heavy.sh npx --yes hyperframes@0.8.139 render -o renders/final_looks.mp4 --workers 4 -q looks . # 1m08s (v5 1m43s) bash tools/finalize.sh # 영상 copy + 2패스 loudnorm + 10x3 컨택트시트 + ebur128 확인- 오버레이 5종(로고·카라오케 가사·크레딧·이름카드·VO 자막)은 별도 워커 프로젝트v3/overlays/에서--format webm투명 렌더도 해 둠(renders/v3_overlays_all.webm), 본편엔 컴포지션 자체를 복사해 꽂음 -
4편: 샷 라이브러리 → edl → comp4 -
shots.py: 알파·인월드 클립 39개를 90x160·24fps로 디코드 → 프레임별 움직임 에너지(알파 변화량 + 알파 가중 rgb 변화량) → 0.30s 이상 떨어진 국소 최대 = 동작 정점 → 정점 앞뒤 0.18~0.70s 구간 157개 + 알파 bbox(샷 크기)·머리 위치 →src/shots.json-edl.py:04_audio/v4/beats.json(170 BPM, 드롭 2.8s·브레이크·드롭2·마지막 히트) 격자 → 박·반박·4분의1박 컷 슬롯 → 내용(알파 정점·인월드·정지 일러 키잉·타이포 카드) 배정 + 의상 7벌·머리 3종 순환(포니 42% 목표) + 클로즈업↔전신 교대 + 매치컷 →src/edl.json78컷(alpha 43·world 16·still 12·card 6·hold 1) -bg/판 7개를 이어 render →src/plates.mp4(37.5s, 5m24s, workers 2),typo_build.py로 만든typo/를--format png-sequencerender → sprite 43장(4.7s) -comp4.py: 컷마다 판 조각(palette로 3색 재색칠) 또는 절차 패턴 → 뒤 타이포 → 캐릭터(속도 램프 0.6~0.85·정지 프리즈·인월드 풀프레임) + 가상 카메라 펀치 1.25·흔들림 → 스티커 외곽선 → 앞 타이포 → 하프톤 프레임 → flash·반전·RGB 분리·그레인bash python3 tools/shots.py && python3 tools/edl.py --seed N heavy.sh python3 tools/comp4.py --scale 0.5 --crf 20 --audio src/audio_v4.wav --out draft/v4_540_r3.mp4 heavy.sh python3 tools/comp4.py --scale 1.0 --crf 18 --audio src/audio_v4.wav --out /mnt/d/malfoy-meme/07_render/V4_HYPER_EDIT.mp4 python3 tools/sheet.py draft/v4_540_r3.mp4 draft/sheet_r3.jpg 13 mid # 컷마다 1칸 + 시각·종류 라벨- 업로드본:V4_HYPER_EDIT_upload.mp4= 같은 영상 libx264 CRF 21(파일 메타crf=21.0확인, 45.0→16.7 Mbps). 만든 명령은 파일로 안 남음 -
드래프트 → 컨택트시트 판정 → 최종 루프 - 드래프트: 1편 540x960 CRF 22 veryfast, 2·4편
--scale 0.5(540p, veryfast), 3·5편 HyperFrames-q draft(1080p 그대로) - 판정: 1초 간격 타일 한 장으로 전체 흐름을 한눈에 봄bash # v2/tools/contact.sh: t=0.5,1.5,... 8x2 ffmpeg -v error -y -ss $off -i "$in" -vf "fps=1/$step,scale=270:480,tile=8x2:padding=4:color=white" -frames:v 1 -q:v 3 "$out" # v3/tools/finalize.sh: 최종본 10x3 ffmpeg -v error -y -i "$OUT" -vf "fps=1,scale=216:384,tile=10x3:padding=4:color=black" -frames:v 1 -q:v 3 "$SHEET"- 판정 결과물:v1/_qc/draft_sheet*.jpg·trans_sheet*.jpg,v2/draft/contact_r1~r2.jpg,v4/draft/sheet_r2~r3.jpg→ 라운드 r1→r3 반복 후 최종 1080p -
오디오: 덕킹 → 라우드니스 - 덕킹(ducking = 대사 나올 때 BGM만 내리기)은 HyperFrames
<audio data-automation>볼륨 lane으로 처리, 전환 0.07~0.2s- 1편: BGM 0.63 기본 → VO 구간 0.5(약 -2dB) → S6 대사 18.75~23.4 0.22(약 -9dB) → 23.6~ 0.38
- 3편: 1.2~1.8 VO1 구간 1→0.62, 24.0~25.8은 보컬 뺀
SONG_MAIN_inst.wav(0.6)로 30ms 교차해 대사 자리 확보, 이후 0.75→0.6→0.5 - 2·4편: VO 없음
- 라우드니스: 2패스 loudnorm(1패스 측정 → 측정값 넣고
linear=true재적용), 목표 I -14 LUFS / TP -1 dBTP / LRA 11, 48kHz AAC 320kbash M=$(ffmpeg -hide_banner -nostats -i "$IN" -vn -af loudnorm=I=-14:TP=-1.0:LRA=11:print_format=json -f null - 2>&1 | sed -n '/^{/,/^}/p') ffmpeg -v error -y -i "$IN" -c:v copy \ -af "loudnorm=I=-14:TP=-1.0:LRA=11:measured_I=$(g input_i):measured_TP=$(g input_tp):measured_LRA=$(g input_lra):measured_thresh=$(g input_thresh):offset=$(g target_offset):linear=true,aresample=48000" \ -c:a aac -b:a 320k -ar 48000 -movflags +faststart "$OUT"
4. 동시성·속도
- heavy.sh semaphore 2슬롯 (semaphore = 동시에 들어갈 수 있는 자리 수를 세는 잠금): render·대량 ffmpeg·
hyperframes check·comp 파이썬 전부 이걸로 감쌈bash for s in 1 2; do exec {fd}>/tmp/malfoy-heavy-$s.lock if flock -n "$fd"; then nice -n 5 "$@"; rc=$?; flock -u "$fd"; exit $rc; fi exec {fd}>&- done; sleep 5 # 빈 슬롯 날 때까지 5초마다 재시도 - 왜 2개: CPU 12코어를 WSL·Windows가 공유, 렌더 3기 동시면 CPU 경합이 진짜 병목(RENDER-FAST 실측 메모). 락은 WSL 로컬 /tmp(9p 경유 안 함)
- workers(render 안에서 병렬로 캡처하는 Chrome 수)
- 실제 사용: 1편 2, 2편 2, 3·5편 4, 4편 판 2, 오버레이·클립월 미리보기 2, 블록 미리보기 3~4
- 규칙: 슬롯 2개가 다 돌 땐
--workers 4, 혼자면--workers 6, 4편 브리프는≤2 - 120프레임(4초) 미만은 auto가 worker 1개로 고정 → 짧은 컷은
--workers직접 지정해야 빨라짐 - GPU render 실측 (
_tools/RENDER-FAST.md, 3초·90프레임 draft, 다른 렌더 3기 진행 중이라 ±30%)
| 경로 | GPU | capture | 처리 속도 | 배수 |
|---|---|---|---|---|
| WSL 기본(worker 1) | software, beginframe | 18.0s | 5.0 fps | 1.0x |
WSL --workers 4 |
software, beginframe | 10.3~14.5s | 6~9 fps | 1.2~1.7x |
WSL --workers 6 |
software, beginframe | 8.2s | 11 fps | 2.2x |
| WSL headed WSLg + D3D12 | hardware, screenshot | 32.0s(+setup 28.6s) | 2.8 fps | 0.56x |
Windows --workers 4 |
hardware D3D11, screenshot | 7.8~10.7s | 8~11 fps | 1.7~2.3x |
- 결론: GPU 이득 0에 가까움, 효과 있는 레버는 workers 수(약 2배)와 동시 렌더 수 줄이기. WSL GPU는 headless Chrome ANGLE이
/dev/dri를 못 열어 불가, headed로 GPU 잡으면 screenshot 경로로 떨어져 1.8배 느림 - 요약 둘째 줄이
beginframe capture · software gpu면 빠른 경로,screenshot capture면 느린 경로 - 편별 실측 시간
- 1편 bg 패스 1m56s / fg 패스 1m55s (각 26.5s, workers 2)
- 2편 bg 2m26s, comp.py 최종 1080p 4m17s
- 3편 draft 1m31s·1m12s, looks 1m08s / 5편 looks 1m43s (30s, workers 4)
- 3편 draft2만 9m04s:
parallel capture stalled60초 → screenshot 세션으로 재시도. 같은 시각에 1·2편 최종 합성이 돌던 중이라 CPU 경합으로 추정 - 4편 plates 5m24s(37.5s), typo sprite 4.7s
- ffmpeg 동시성: clip_wall은
<video>마다 추출 ffmpeg 1개가 동시에 뜸 → 31대 버전에서 ffmpeg 30개·load 160 사고 → 12대로 축소, 생성기는--jobs 1순차
5. 함정과 해결
| 증상 | 원인 | 해결 | 근거 파일 |
|---|---|---|---|
| 알파 webm 캐릭터를 HyperFrames 컴포지션에 넣고 render하면 캡처가 멈춤(stall) | HyperFrames의 알파 webm 프레임 캡처 문제(원인 상세 미확인) | 하이브리드: 알파는 HyperFrames 밖에서 ffmpeg(-c:v libvpx-vp9 디코드) 또는 파이썬으로 합성. 3편은 알파 컷만 불투명 mp4로 미리 구움 |
03_motion/README-B.md(미리보기 절), v3/tools/charcomp.sh 2행 |
TMPDIR=/mnt/d/...로 render하면 Chrome이 SIGILL로 죽음(capture 실패, 2회 재현) |
chrome-headless-shell이 drvfs 임시 폴더에서 기동 실패 | env -u TMPDIR + 프레임 캐시만 D로 HYPERFRAMES_EXTRACT_CACHE_DIR=/mnt/d/_cache/tmp/hf-frames(또는 --frames-cache-dir) |
_tools/RENDER-FAST.md, 03_motion/README-B.md·README-C.md |
| 투명 fg 패스(mov)가 느린 경로로 렌더됨 | 알파 출력은 BeginFrame이 안 돌고 screenshot capture로 떨어짐 | 투명 패스는 화면 요소만 최소로, 배경 패스는 불투명 mp4로 분리 | v1/build/render_fg.log |
sparse keyframes (max interval: 6s) 경고, seek 실패·프레임 정지 위험 |
Flow 원본 mp4 키프레임 간격 6s | -g 30 -keyint_min 30으로 재인코딩. 3편은 prep_assets.py -g 12, 미리 합성 컷 -g 6 |
v1/build/render_bg.log, v3/tools/prep_assets.py |
| 같은 블록을 두 번 꽂으면 깨짐 | composition-id 충돌, timeline 키 = id | id 바꾼 사본 생성(kinetic_wall_t0.html, kinetic_wall_b.html) |
v2/tools/patch_kw.py, v4/bg/index.html |
| 컴포지션 파일을 심볼릭 링크하면 빈 파일로 읽힘 | 번들러가 파일 링크를 못 따라감 | sync.sh로 cp(정본은 03_motion, 사본만 수정) |
03_motion/README-C.md, v1/sync.sh |
| 미리보기 호스트를 03_motion 루트에 두면 lint 에러 | multiple_root_compositions |
호스트 프로젝트를 밖(_work_c/, preview/_bproj)으로 뺌 |
03_motion/README-C.md |
| 서브컴포지션 변수 기본값 고친 게 반영 안 됨 | 로더가 서브컴포지션 <html> 선언을 버림 |
스크립트에 기본값 사본 두고 tools/sync_defaults.py 실행 |
v3/overlays/README.md |
렌더에서 JS로 만든 <video>가 빠짐 |
비디오 재생은 HyperFrames가 소유(seek·추출) | 클립 교체를 모니터별 릴 1개에 미리 구워 <video> 고정 개수만 둠 |
03_motion/README-C.md |
/mnt/d에서 snapshot 1회 6분+ |
drvfs(9p) 파일 읽기 느림 | ext4 사본에서 작업 후 복사 → 18초 | 03_motion/README.md |
| 3편 draft2가 9분 걸림 | parallel BeginFrame capture 60초 stall → screenshot 재시도, 동시 합성 2개와 CPU 경합(추정) | heavy.sh 2슬롯 지키기, 무거운 comp와 render 겹치지 않기 | v3/renders/draft2.log |
2편 최종 로그 끝에 Broken pipe |
배경 디코드 ffmpeg 파이프를 다 읽기 전에 닫음(추정). 출력 mp4는 정상 생성 | 무시 가능, 길이·프레임 수는 ffprobe로 확인 | v2/tools/comp_final.log |
| 2편만 라우드니스 -13.0 LUFS / 최대 -0.4 dBFS | audio_v2.wav를 loudnorm 없이 mux |
다시 낼 땐 finalize.sh식 2패스 loudnorm을 거칠 것 | 10-08 ebur128 실측, v2/tools/comp.py |
Windows render에서 글자 모양이 다름, --variables '{json}'이 깨짐 |
Windows 기본 sans-serif(Arial), cmd 따옴표 처리 | 웹폰트 명시, --variables-file 사용 |
_tools/RENDER-FAST.md |
| WSL 메모리 부족(11.7G)으로 render 위험 | 무거운 렌더가 WSL RAM 안에서 돎 | Windows 렌더 래퍼 render-win.sh → Windows Node + Chrome(D3D11)로 WSL RAM 밖에서 render, D: 파일을 9p 없이 읽음. 속도는 WSL workers와 비슷 |
_tools/render-win.sh, D:\_cache\win-tools\hf-win\render.cmd |
- Windows 래퍼 구조:
render-win.sh가wslpath -w로 경로 변환 →cmd.exe /c render.cmd <프로젝트> <out> [옵션].render.cmd안에HYPERFRAMES_BROWSER_PATH(chrome-headless-shell win64 154)·HYPERFRAMES_FFMPEG_PATH·FFPROBE_PATH(ffmpeg 9.0.2)·HYPERFRAMES_EXTRACT_CACHE_DIR·TEMP·npm 캐시를 전부D:\_cache\win-tools로 고정, hyperframes 0.8.140
6. 다시 돌리는 법
# 공통: 렌더는 TMPDIR 지우고 heavy.sh로 (동시 2개 상한)
cd /mnt/d/malfoy-meme/07_render/v1 && bash sync.sh && cd hf && env -u TMPDIR HYPERFRAMES_EXTRACT_CACHE_DIR=/mnt/d/_cache/tmp/hf-frames /mnt/d/malfoy-meme/_tools/heavy.sh npx --yes hyperframes@0.8.140 render . --workers 4 --crf 10 --variables '{"pass":"bg"}' -o ../build/pass_bg.mp4 && env -u TMPDIR /mnt/d/malfoy-meme/_tools/heavy.sh npx --yes hyperframes@0.8.140 render . --workers 4 --format mov --variables '{"pass":"fg"}' -o ../build/pass_fg.mov && cd .. && python3 composite.py draft # 확인 후 final
cd /mnt/d/malfoy-meme/07_render/v2 && bash tools/sync.sh && /mnt/d/malfoy-meme/_tools/heavy.sh /mnt/d/_cache/venvs/matte/bin/python tools/comp.py --scale 0.5 --out draft/v2_540.mp4 && bash tools/contact.sh draft/v2_540.mp4 draft/contact.jpg
cd /mnt/d/malfoy-meme/07_render/v3 && bash tools/sync_overlays.sh && python3 tools/build.py && bash tools/charcomp.sh && env -u TMPDIR /mnt/d/malfoy-meme/_tools/heavy.sh npx --yes hyperframes@0.8.139 render -o renders/final_looks.mp4 --workers 4 -q looks . && bash tools/finalize.sh # 5편은 v5에서 동일
cd /mnt/d/malfoy-meme/07_render/v4 && python3 tools/shots.py && python3 tools/edl.py && /mnt/d/malfoy-meme/_tools/heavy.sh python3 tools/comp4.py --scale 1.0 --crf 18 --audio src/audio_v4.wav --out /mnt/d/malfoy-meme/07_render/V4_HYPER_EDIT.mp4
ffmpeg -i /mnt/d/malfoy-meme/07_render/V4_HYPER_EDIT.mp4 -c:v libx264 -crf 21 -preset slow -pix_fmt yuv420p -c:a copy -movflags +faststart /mnt/d/malfoy-meme/07_render/V4_HYPER_EDIT_upload.mp4 # 업로드본 (원 명령 기록 없음, crf만 메타로 확인한 재구성)
/mnt/d/malfoy-meme/_tools/heavy.sh /mnt/d/malfoy-meme/_tools/render-win.sh /mnt/d/malfoy-meme/07_render/v1/hf /mnt/d/malfoy-meme/07_render/v1/out.mp4 --workers 4 --quality looks # WSL 메모리 부족할 때 Windows 쪽 render