악역영애 말포이
뉴스레터코드 vs AI프롬프트 원문집파이프라인 그래프도구별 파이프라인효과 사전키트 README상태 공간 STATE프롬프트 README에이전트 배선도
목차

도구별 파이프라인 — 악역영애 말포이 영상 5편

순서 섹션
1 Claude 오케스트레이션 (지휘·분할·병합·장애 복구)
2 Aside 브라우저 (레퍼런스·핀터레스트·Flow 자동화)
3 codex 이미지 생성
4 매팅 (사람만 오려내기, GPU)
5 오디오 (ElevenLabs)
6 합성·render

Claude 오케스트레이션 파이프라인

1. 정의 · 입력 → 출력

항목 수 내역
서브에이전트 기동(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. 실제 실행 절차

  1. 지시서부터 고정(공유 재료는 워커가 아니라 문서가 들고 다님) - 공통 규칙은 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만 읽고 제출
  2. fan-out: 의존 없는 갈래는 즉시 동시 기동 - 첫 15분(17:22~17:37)에 7기 동시: 리서치 2 + 핀터레스트 + 배경 모션 + V1 오디오 + 매팅 시험 + Flow 정찰 - 이유: 곡(박 시각)·배경 블록·매팅 방식은 서로 기다릴 필요가 없음. 합류는 합성 단계 하나 - 백그라운드 기동 → 메인은 완료 알림(task-notification) 받을 때마다 판정·다음 기동

  3. 분할 계약: "섹션 워커 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
  1. 병합 + 누락 대조(스크립트로) - 효과 사전: 섹션 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)

  2. 통신: 진행 중 워커에 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기씩 "세션 재시작으로 중단됐었어. 이어서 진행" + 남은 일 목록
  1. 부하 사고와 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. 장애 복구: 재부팅 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 역할을 "산출 파일 존재 확인"이 대신함
  3. 사용자 피드백 = 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기 동시 문서
  1. 크레딧·비용 관리 - 계획: 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])
  1. 교훈은 메모리 파일로
    • 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. 함정과 해결 표

증상 원인 해결 근거 파일
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

다음에 이 파이프라인을 1명령으로 돌리려면

  1. 지시서 묶음을 실행 단위로: STORYBOARD·EDL·*_jobs.json·SCHEMA를 하나의 run.yaml(편·노드·담당·입력·출력·상한)로 합치고, 메인은 이 파일만 읽어 워커를 띄우게
  2. 부하 규칙을 지시서가 아니라 도구에 강제: 렌더·ffmpeg·check 래퍼가 heavy.sh를 자동 경유하고, /mnt/d 전체 find는 훅으로 차단(이번 원인 3개는 모두 도구 수준에서 막을 수 있던 것)
  3. checkpoint를 파일로: 노드마다 state.json(완료·진행·실패, 산출 경로)을 남겨 재부팅 후 resume 한 번으로 멈춘 워커만 다시 띄우게(지금은 메인이 폴더를 눈으로 확인)
  4. 사용자 반려 → 규칙 자동 누적: 반려 원문을 rules 표에 행으로 추가하면 STORYBOARD 공통 절과 이후 워커 지시서에 자동 삽입(이번엔 메모리·문서에 손으로 옮김)
  5. 예산·ETA를 계측으로: Flow·ElevenLabs 잔액과 산출 파일 수를 주기적으로 읽어 상한 도달 시 정지·진행률 기반 ETA를 자동 보고

Aside 브라우저 파이프라인

1. 한 줄 정의

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. 공통 준비

  1. 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 정상)
  2. repl 규칙: 한 스코프라 같은 const 재선언 불가, 실패한 호출의 const도 남음 → globalThis.Z = {} 식 namespace 객체에 담고 배치마다 새 이름(Z, Z4, Q, W)
  3. page.waitForTimeout 없음("not a function") → Z.sleep = ms => new Promise(r=>setTimeout(r,ms))
  4. 결과는 console.log로만 돌아옴. 스크린숏은 display(await p.screenshot()), 단 CDP timeout 잦음 → bringToFront() 후 3회 재시도 헬퍼

3-1. ① 레퍼런스 릴스 확보 (10-07 17:2x)

  1. 원본 주소 https://www.instagram.com/reels/DeKsEh0sBPy/ 를 openTab → 로그인 페이지로 리다이렉트, video 0개
  2. 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)
  3. 임베드 페이지를 Aside로 열고 video src 추출 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로 함
  4. 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. 수집 헬퍼 정의(탭 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개)
  2. 검색어 → 카테고리 폴더: 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
  3. 카테고리별 round-robin(검색어 번갈아)으로 최대 28장 골라 카테고리 약칭 경로 목록을 console.log → scratchpad list.txt
  4. 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
  5. PIL로 깨진 파일(최소 변 80px 미만 포함) 삭제 + 7열 컨택트시트 _sheet_<카테고리>.jpg → Read로 눈 확인
  6. 무관 이미지 삭제(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)

  1. 첫 프레임 업로드 준비 — D: 경로는 거부되므로 Aside 세션 폴더로 복사 bash D=/mnt/c/Users//.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/ - 세션 ID는 repl 출력의 "Displayed image saved to" 경로 또는 ls -t .../sessions/로 확인 - WSL→/mnt/c 복사 느림(2MB 2장에 1~2분) → run_in_background
  2. 프로젝트 열기·배너 — openTab(프로젝트 URL) → 쿠키 배너 getByRole('button',{name:'나중에'})
  3. 프로젝트 설정(톱니, 우상단 (1223,37), 이름 "타일 그리드 설정") — 무음 동영상 반환 전환 버튼 ON. 토글은 locator.click()이 "Checkbox click did not change checked state" 에러 → 좌표 mouse.click 후 aria-checked 재확인. 제출 시 프롬프트 지우기 기본 ON 유지(제출하면 상자·시작 프레임 비워져 재료 섞임 방지)
  4. 생성 설정 팝업 — 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 크레딧")
  5. 첫 프레임 지정(프레임 모드) - 처음 업로드: 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)
  6. 프롬프트 입력·제출 — [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'; };
  7. 참조(소재) 모드 + 캐릭터 @Malfoy (ref 배치) - 좌측 캐릭터 → 새 캐릭터 → 하단 업로드(단일 파일, 02_frames/turnaround.png 4면 턴어라운드) → 제목 연필(382,183) → Ctrl+A → Malfoy → Enter → 우상단 완료. 등록 0크레딧 - 설정 팝업 소재 라디오 → 상자에 @ 입력 → Malfoy 타이핑 → option Malfoy 캐릭터 → 칩(span.mention-chip[data-reference-type=entity]) → 추가 이미지·동영상은 button "프롬프트 상자에 소재 추가"(+) → Ctrl+End 후 나머지 문장 insertText - 동영상 참조(06_flow/ref_motion_spin3s.mp4, 레퍼런스 릴스 4.3초부터 3초 잘라 만든 것) 업로드 시 "이 동영상을 사용할 권리" → 동의
  8. 진행 대기 — main innerText의 NN% 사라지면 완료. 그리드는 .cdk-virtual-scrollable 가상 스크롤이라 window.scrollBy 안 먹음 → 이 요소 scrollTop 조작, 화면 밖 타일 진행률은 안 잡힘
  9. 다운로드 — 동시 생성이라 "맨 위 타일" 가정 금지. 프롬프트 앞 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 업스케일링됨. 우측 "일괄 다운로드" 아이콘은 해상도 선택 없어 안 씀
  10. 회수·정리·판정 bash S=/mnt/c/Users//.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 # 컷 수 - v1~v3은 scratchpad qc.sh NAME(세션 폴더→D 복사 + ffprobe + 5장 타일) 한 줄로 처리 - 판정 기준: 컷 분할·블루 붕괴·얼굴 붕괴·기괴한 손/몸만 1회 재생성(<id>_r2.mp4)
  11. 세션 끊김 복구 — Aside 데몬 자동 업데이트 → repl "REPL session not found" + 탭 닫힘 + 세션 폴더 ID 변경(UEt3yV… → 50vvOH…) - listBrowserTabs()로 상태 확인 → 새 namespace(globalThis.W)로 탭 재오픈·헬퍼 재정의 → WSL에서 새 세션 artifacts/mm mkdir → 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

4. 동시성·속도

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\\.aside\u\0\sessions\<세션>\artifacts\mm\로 복사 후 그 경로 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 장수 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. 실제 실행 절차

  1. 프롬프트 jsonl 작성 — 메인 Claude가 python3 - <<'EOF' heredoc으로 공통 스타일 문장(style/face 변수) + 컷별 문장을 이어 붙여 jsonl을 씀 - 크기: 시안 1024x1536, 세로 컷 1024x1792, 턴어라운드만 1792x1024 - 규칙: IP 단어 금지, 이미지에 글자 넣지 않음(글자는 코드 층에서 얹음) — V3-STORYBOARD.md 177행 - 참조 이미지는 images 배열에 절대경로. 뒤 배치일수록 앞 배치 결과(canon_*, O*)를 참조로 씀 → 앞 배치 파일명이 틀리면 뒤 배치 전체가 엉뚱한 얼굴을 참조하게 됨
  2. 러너 실행 — _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 + effort low(지시문 전달만 하면 되므로 저노력), 실제 그림 = codex 빌트인 $imagegen(gpt-image-2) - 참조가 있으면 지시문 앞에 "참조 이미지는 스타일·디자인 안내용, 새 이미지를 만들 것" 문장 자동 삽입 - 재실행 안전: output_path가 이미 있으면 건너뜀(main()에서 todo 필터)
  3. 회수(move) — codex는 PNG를 ~/.codex/generated_images/{세션 uuid}/*.png에 떨굼. 러너는 잡 종료 후 30초 동안 1초 간격으로 아래 함수를 불러 1장을 집어 shutil.move python 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를 두 잡이 동시에 가져가는 것만 막음. 어느 잡의 그림인지는 모름
  4. 컨택트시트 작성 — 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(교정 전 순서 그대로 보존)
  5. 내용↔파일명 대조 노드(스왑 교정) — 메인 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장 순환) - 교정 후 시트 다시 만들어 재대조
  6. 타임아웃 재시도 — 실패분만 TIMEOUT=600으로 다시 실행. 이미 있는 파일은 건너뛰므로 같은 jsonl 그대로 써도 됨

4. 동시성 · 속도

배치 장당 배치 전체 실효 처리량
캐릭터 시안 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초/장

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. 한 줄 정의 · 입력 → 출력

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) 채택

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)

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-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     # 키만, 깨끗한 화면용

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-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초

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. 한 줄 정의 · 입력 → 출력

편 메인 곡 후보 수 템포 실측 크레딧(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

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}

② 후보 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

⑤ 선택 근거 - 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에
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)]  # (출력 시작, 끝, 원곡 오프셋)

⑦ 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()

⑧ 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)

⑨ 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. 동시성·속도

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. 한 줄 정의 · 입력 → 출력

배경 블록 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

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. 실제 실행 절차

  1. 블록 사본 동기화 (정본 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개 나열

  2. 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 빠른 경로를 못 탐)

  3. 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, noise python 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

  4. 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초

  5. 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), 본편엔 컴포지션 자체를 복사해 꽂음

  6. 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.json 78컷(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-sequence render → 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). 만든 명령은 파일로 안 남음

  7. 드래프트 → 컨택트시트 판정 → 최종 루프 - 드래프트: 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

  8. 오디오: 덕킹 → 라우드니스 - 덕킹(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 320k bash 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. 동시성·속도

경로 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

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

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