ML.
/

← 글 목록SeongHwa Lee57분 분량학습 노트 · AI와 함께 분석

Lemo-Opuscar 분석: 영상 스타일 66개를 코드와 프롬프트로 어떻게 묶었을까요?

Lemo-Opuscar는 코딩 에이전트가 수채화, 우키요에, 위성 브리핑 같은 스타일로 짧은 영상을 직접 그리고 작곡하고 믹싱하게 만드는 스타일 라이브러리입니다. 렌더러는 400줄도 안 되고, 본체는 30만 단어 분량의 스타일 프롬프트와 감독 지침입니다. 코드를 따라가고, 데모를 직접 렌더링해서 worker 수별 속도와 결과가 정말 같은지 측정했습니다.

목차

1. 왜 Lemo-Opuscar인가요?

이번 주 GitHub 트렌딩에 올라온 저장소입니다. 2026년 9월 26일에 공개되었고 5주 만에 별 12,063개와 포크 1,394개를 받았습니다. main 브랜치의 커밋은 80개뿐이고 77개를 한 사람(lemomo-ai)이 올렸습니다. 외부 기여자의 커밋은 3개입니다. 첫 커밋에는 스타일이 39개였고 지금은 66개입니다.

README의 첫 줄은 이렇습니다.

A toolbox of film styles made in code: 66 ready to use, and your own.

그 아래에 이런 경고도 붙어 있습니다.

Built for Claude. The styles are tuned for Claude Opus 5.5; other models may not reproduce them well, so use them with care.

Lemo-Opuscar의 66개 스타일 데모에서 한 장씩 뽑은 프레임을 이어 붙인 띠. 뉴스 자막, 위성 지도, 수묵화, 픽셀 RPG, 아이소메트릭 섬, 3D 제품 렌더 등이 네 줄로 늘어서 있고 오른쪽 끝에 LEMO OPUSCAR 로고가 있습니다.

저장소의 docs/cover.jpg입니다. 66개 데모 영상에서 한 장씩 뽑았습니다. (CC BY 4.0, LemoLab)

이 그림에는 영상 생성 모델로 만든 프레임이 하나도 없습니다. 대부분의 데모는 에이전트가 Canvas 2D, WebGL, three.js 코드로 한 프레임씩 그렸고, 음악도 무료 샘플 라이브러리와 numpy 합성으로 직접 작곡했으며, 내레이션은 TTS로 만들었습니다. 다만 README의 "No stock footage"라는 문장을 모든 데모에 그대로 적용할 수는 없습니다. 10월 22일에 추가된 Edit & Documentary 계열은 퍼블릭 도메인이나 CC 라이선스의 실사·아카이브 영상과 문서를 잘라 씁니다. 1920년대 뉴스릴 데모는 1925년 음반 녹음을 음악으로 쓰고, Broadcast News Package 데모는 AI로 생성한 배경 이미지 두 장을 씁니다. 출처는 데모마다 CREDITS나 SOURCES.md에 적혀 있습니다.

그렇다면 이 저장소에는 무엇이 들어 있을까요? 렌더러 코드가 중심일 것 같지만 그렇지 않습니다. core/render의 렌더링 도구(.mjs 8개)는 다 합쳐도 400줄이 안 됩니다. 나머지는 거의 다 에이전트가 읽을 글입니다. 이 글에서는 에이전트용 문서가 어떻게 짜여 있는지, 그리고 그 아래에 깔린 작은 렌더러가 어떻게 동작하는지 살펴봅니다.

2. 한 문장으로 이해하기

Lemo-Opuscar는 코딩 에이전트용 영상 스타일 라이브러리입니다. 스타일마다 "무엇을 지켜야 하는가"를 적은 STYLE.md와 코드로 만든 데모 영상 한 편이 있고, 공통 감독 지침(DIRECTOR.md), 제작 지침(TECHNIQUE.md), 그리고 render(t)가 그린 화면을 프레임 단위로 찍는 작은 도구 모음(core/)이 함께 들어 있습니다.

질문Lemo-Opuscar의 답
누가 영상을 만드나요?코딩 에이전트입니다. 사용자는 스타일과 주제만 말합니다. 에이전트가 기획, 그림, 작곡, 믹싱, 렌더링까지 합니다.
무엇으로 그리나요?HTML 페이지 하나입니다. 페이지가 window.render(t)를 노출하면 headless Chrome이 프레임마다 그 함수를 부르고 화면을 찍습니다.
음악과 소리는요?Python입니다. 샘플러(sampler.py), 현악기 물리 모델링(pluck.py), 절차적 효과음(sfx.py)이 있습니다.
목소리는요?영어는 Kokoro(오프라인), 중국어는 edge-tts(온라인)입니다. 생성한 음성은 Whisper로 다시 받아 적어서 대본과 맞는지 확인합니다.
스타일은 무엇으로 정의하나요?STYLE.md입니다. 재질, 색 논리, 글꼴, 움직임, 카메라 문법, 사운드 팔레트, 함정을 적습니다. 이야기는 적지 않습니다.
데모 영상은 왜 있나요?기법을 배우는 참고 구현입니다. 에이전트는 원칙적으로 자기 기획서를 먼저 쓴 다음에야 데모를 열 수 있습니다.
어떻게 설치하나요?Claude Code 플러그인(claude plugin install lemo-opuscar@lemolab)이나 저장소 클론입니다.
라이선스는요?코드와 문서는 MIT, 데모 영상과 스틸은 CC BY 4.0입니다. 사용자가 만든 영상은 사용자 것입니다.

3. 기존 글들과 어떤 관계인가요?

이 블로그에서 다룬 저장소 중 세 가지와 가깝습니다.

대상공통점Lemo-Opuscar와의 차이
HyperFramesHTML을 headless Chrome으로 찍어 MP4를 만듭니다HyperFrames는 렌더러에 12만 줄을 들여 웹 애니메이션의 시간을 대신 정합니다. Opuscar는 페이지에 render(t)를 요구하고 렌더러는 400줄입니다.
Superpowers에이전트의 작업 방식을 마크다운으로 정합니다Superpowers는 소프트웨어 개발 절차를 다룹니다. Opuscar는 영화 연출 절차(기획서, 레퍼런스, 큐 맵, 자체 검수)를 다룹니다.
Claude Code Game Studios한 분야의 제작 스튜디오를 에이전트로 흉내 냅니다Game Studios는 역할별 에이전트가 일을 나눠 맡습니다. Opuscar는 에이전트 하나가 감독, 미술, 작곡, 믹싱을 모두 맡습니다.

특히 HyperFrames와 비교하면 같은 문제를 반대쪽에서 풉니다. 이 차이는 13장에서 다시 정리합니다.

4. 저장소 구성과 규모

lemo-opuscar/
├── AGENTS.md          에이전트의 첫 진입점 (요청 → 읽을 문서 라우팅)
├── DIRECTOR.md        감독 지침: 기획, 소리, 리듬, 카메라, 연기, 검수, 지도 규칙
├── TECHNIQUE.md       제작 지침: 페이지 계약, 음성, 음악, 믹싱, 3D
├── MY-STYLES.md       사용자가 자기 스타일을 만드는 절차
├── MAINTAINING.md     라이브러리 소유자용 (스타일 추가, 배포)
├── core/              공통 도구 (렌더, TTS, 음성 검사, 샘플러, 효과음, 마스터링)
├── styles/<slug>/     STYLE.md · DEMO.md · style.json · demo/ · poster.jpg
├── plugin/            Claude Code 플러그인 (SKILL.md + setup.sh)
├── styleboard/        GitHub Pages 갤러리 빌더
└── tools/             에셋 다운로드, 릴리스, 플러그인 디렉터리 에디션 빌드

줄 수를 세어 보면 무게중심이 어디 있는지 보입니다.

영역규모내용
core/render/*.mjs (렌더 도구 8개)393줄페이지 열기, 정지 화면, 영상, 이벤트, 읽기 시간 검사 등
core/lib.js44줄시드 난수, 이징, 단조 3차 보간
core/audio/*.py, core/tts/*.py1,403줄샘플러 670줄, 음성 검사 193줄 등
core/render/mux.sh100줄2패스 라우드니스 정규화와 손상 오디오 검출
공통 지침 7개 (AGENTS, DIRECTOR, TECHNIQUE 등)711줄에이전트가 요청에 따라 골라 읽는 문서
styles/*/STYLE.md 66개8,099줄 · 약 16만 단어스타일의 불변 규칙
styles/*/DEMO.md 66개7,720줄 · 약 15만 단어데모를 어떻게 만들었는지
styles/*/demo/ 소스 (CSS 제외)13만 3천 줄JS 8만 7천, Python 3만 5천, HTML 7천, 셸

공통 렌더러는 400줄 남짓인데 데모 코드는 13만 줄이 넘습니다. 붓 엔진, 캐릭터 리그, 셰이더, 작곡 스크립트가 공통 라이브러리에 있지 않고 데모마다 따로 있습니다. 데모 66편을 모두 이으면 약 60분(3,590초)입니다.

카테고리는 11개입니다. 편집·다큐(8), 증거 영상(8), 손그림·회화(8), 정보·발표(8), 재질·3D(7), 그래픽·타이포(7), 인쇄·판화(6), 동아시아 전통(4), 카툰(4), 게임(4), 영화와 시대(2)입니다.

5. 직접 렌더링해 보았습니다

데모 소스는 "읽기용"이라고 적혀 있지만 렌더러 계약을 확인하려고 두 편을 직접 찍어 보았습니다. 정지 화면을 찍는 데 필요한 것은 npm 패키지 두 개(playwright-core, three)와 Chrome Headless Shell뿐이고, MP4로 렌더링하려면 ffmpeg가 더 필요합니다.

npm install
node node_modules/playwright-core/cli.js install chromium-headless-shell
node core/render/still.mjs styles/urban-sketch/demo 6 20 --out stills/us

처음에는 바로 되지 않았습니다. 데모 글꼴이 git에 없기 때문입니다(.gitignore에 styles/*/demo/**/*.ttf). 글꼴 요청이 404로 끝나면 페이지에서 [pageerror] A network error occurred. 오류가 나고, 도구는 바로 종료됩니다. 데모의 CREDITS를 보고 Google Fonts 저장소에서 Caveat와 Reenie Beanie를 받아 넣은 뒤에야 렌더링되었습니다. Halftone Dossier 데모에는 글꼴을 받아 주는 fetch_fonts.mjs가 들어 있었습니다.

Urban Sketch 데모의 20초 프레임. 수채 물감으로 칠한 푸른 하늘 아래 펜 선으로 그린 빌딩들이 있고, 가운데 계단식 탑 꼭대기에 사람이 매달려 날아가는 밀짚모자를 잡으려 손을 뻗고 있습니다.

Urban Sketch · Pen & Wash 데모 "Where the Wind Went"의 20초 프레임을 직접 렌더링했습니다. (데모: CC BY 4.0, LemoLab)

Halftone Dossier 데모의 9초 프레임. 종이 질감 배경에 '测试重力'라는 큰 한자 제목이 있고, 주황색 망점으로 찍힌 큰 숫자 01 앞에서 주황 고양이가 앞발로 탁자 위 머그컵을 밀고 있습니다. 오른쪽 위에 今日次数 16이 보입니다.

Halftone Dossier 데모 "胖橘案卷 Case File: Chubby"의 9초 프레임입니다. (데모: CC BY 4.0, LemoLab)

두 장 모두 이미지 파일을 쓰지 않았습니다. 수채 번짐, 망점, 종이 질감, 펜 선이 전부 코드로 그려집니다.

6. 영상 한 편은 어떤 단계로 만들어지나요?

TECHNIQUE.md 첫머리의 그림이 전체 구조입니다.

timeline (single source of truth)
   ├─► picture:  web page with render(t) ──► headless Chrome, frame by frame ──► video.mp4
   ├─► voice:    TTS per line ──► speech-to-text check ──► word timings
   ├─► music:    score from the same timeline (samples + synthesis) ──► stems
   ├─► sound:    procedural foley at event times
   └─► subtitles: .srt + burned-in captions
mix (duck, compress, balance) ──► mux with ffmpeg, two-pass loudnorm −14 LUFS ──► film.mp4

핵심은 타임라인 하나입니다. 섹션, 템포, 박자, 타격 지점을 timeline.js 한 곳에 적고 그림, 악보, 믹서, 자막, 검사 스크립트가 모두 같은 숫자를 읽습니다. 그래서 고양이가 착지하는 프레임과 드럼이 울리는 오디오 샘플이 같은 시각 값에서 계산됩니다. 여러 데모에 있는 cuecheck.py는 그림의 타격 지점과 음악 큐의 차이를 재고, 목표는 0ms입니다.

에이전트가 따르는 작업 순서는 AGENTS.md에 있습니다.

  1. 요청 정리. 정할 수 없는 것만 한 번에 묻습니다(주제의 사실, 가진 자료, 언어, 스토리보드를 볼지). 그다음에는 더 묻지 않습니다.
  2. 기획서. TREATMENT.md를 씁니다. 데모를 열기 전에 씁니다.
  3. 스타일 프레임. 실제 그리기 코드로 스타일 프레임을 찍어 STYLE.md와 비교합니다.
  4. 스토리보드. 사용자가 원했을 때만 멈춰서 승인을 기다립니다.
  5. 제작. 음성 → 음성 검사 → 작곡 → 애니메이션 → 믹싱 → 렌더 순서입니다.
  6. 자체 검수와 납품. 영상, 포스터, 자막, 기획서, CREDITS, 명령 하나로 다시 빌드하는 build.sh를 넘깁니다.

README에 따르면 영상 한 편에 30~60분과 "적지 않은 토큰"이 듭니다.

7. 페이지 계약: 그림은 시간의 함수입니다

7.1 전역 변수 다섯 개

렌더 도구와 페이지 사이의 약속은 core/README.md의 표 하나가 전부입니다.

window.DUR = 54.2 // 영상 길이(초)
window.render = (t) => { ... } // t초의 프레임을 그린다. 결정적이어야 한다
window.READY = true // 글꼴과 이미지를 다 불러온 뒤 true
window.EV = [{ t, type, ... }] // 선택: 효과음·큐 이벤트
window.TEXTS = (t) => [{ id, text, x0, y0, x1, y1 }] // 선택: t에 보이는 모든 글자와 상자

render(t)는 이전 프레임의 상태를 쓰면 안 됩니다. TECHNIQUE.md는 이 규칙을 이렇게 적습니다.

No Date.now(), no Math.random() without a seed (core/lib.js has mulberry and hash), no state carried from the previous frame. Any frame can be rendered alone, in any order, by any worker.

Urban Sketch 데모의 render(t)를 보면 이 계약이 실제로 어떻게 쓰이는지 보입니다. 카메라, 인물 포즈, 모자 궤적이 모두 t의 함수이고, 손그림 선의 떨림(line boil)까지 boilOf(t)로 시드를 바꿔 만듭니다.

window.render = (t) => {
  const cam = camera(t)
  comp(cam, t) // WebGL 수채 합성
  // ...
  const hs = him(t),
    herP = herPose(t)
  const herPrims = figure(LOOK.her, herP, { ...HER }, { seed: 50, boil: boilOf(t) })
  const hst = hatState(t, herPrims.head, t >= 24.0 ? himPrims.hand.R : null)
  // ...
}
window.EV = EV // 걸음, 바람, 종소리, 착지 같은 효과음 이벤트
window.READY = true

데모 66편 중 65편이 window.DUR를 노출합니다. 나머지 한 편인 Halftone Dossier는 초기 데모라 자체 render.mjs로 렌더링합니다. 그래서 core/render/video.mjs로 찍으면 window.DUR must be a positive number of seconds (got undefined)로 멈춥니다. TEXTS까지 노출하는 데모는 25편입니다.

7.2 렌더러는 무엇을 하나요?

core/render/page.mjs(62줄)가 페이지를 엽니다. 저장소 루트를 127.0.0.1에서만 서빙하는 작은 정적 서버(serve.mjs)를 띄우고, 저장소 밖에 있는 사용자 프로젝트는 /@film/ 아래에 마운트합니다. Host 헤더가 127.0.0.1이나 localhost가 아니면 403을 돌려줘서 DNS 리바인딩도 막습니다.

실패는 숨기지 않습니다. 페이지에서 JS 오류가 나거나 스크립트·문서 요청이 404로 끝나면 즉시 process.exit(1)로 종료합니다. 반면 fetch, 이미지, 글꼴 404는 경고 한 줄만 남깁니다. 많은 데모가 voices/dur.json 같은 선택 파일을 읽고 없으면 기본값을 쓰기 때문입니다. 다만 5장에서 본 것처럼 글꼴 404가 페이지 안에서 예외로 이어지면 결국 멈춥니다.

video.mjs(72줄)의 흐름은 이렇습니다.

  1. 페이지를 한 번 열어 window.DUR만 읽고 닫습니다.
  2. 전체 프레임 수 round(DUR × FPS)를 worker 수로 나눠 연속 구간을 하나씩 맡깁니다.
  3. worker마다 브라우저를 따로 띄우고, 프레임마다 render(f / FPS)를 부른 뒤 JPEG(품질 95)로 찍어 ffmpeg 표준 입력에 넣습니다(-f image2pipe -c:v mjpeg). ffmpeg는 구간마다 libx264 CRF 14로 인코딩합니다.
  4. 구간 파일을 concat으로 무손실 연결합니다.

거의 HyperFrames의 가장 단순한 경로와 같습니다. 가상 시계도, 애니메이션 어댑터도, beginFrame도 없습니다. 페이지가 시각을 t로 직접 받으므로 이런 장치가 필요 없습니다.

7.3 JPEG로 찍고 그레인은 ffmpeg에서 넣습니다

TECHNIQUE.md는 렌더링이 빠른 이유를 세 가지로 적습니다. JPEG 스크린샷, worker마다 브라우저 하나, 그리고 그레인을 페이지가 아니라 ffmpeg에서 넣는 것입니다. 페이지에서 그레인을 그리면 모든 프레임이 압축되지 않아서 느려진다고 합니다.

JPEG 쪽은 직접 재 보았습니다. Urban Sketch 페이지에서 같은 프레임을 찍을 때 JPEG는 평균 32.1ms, PNG는 261.6ms였습니다. 8배 차이입니다. render(t) 자체는 8.1ms라서 한 프레임 비용의 80%가 캡처입니다.

7.4 동시 렌더를 기계 단위로 제한합니다

이 저장소에서 가장 공들인 렌더 코드는 의외로 동시성 제어입니다. 에이전트가 여러 영상을 병렬로 만들면 같은 기계에서 Chrome이 수십 개 뜰 수 있기 때문입니다.

  • 출력 잠금. video.mjs는 <out>.lock에 PID를 적고, 같은 --out에 두 렌더가 쓰려 하면 뒤의 것이 종료 코드 2로 멈춥니다. 잠금을 가진 프로세스가 종료되었거나 잠금이 12시간 넘게 갱신되지 않았으면 새 렌더가 잠금을 넘겨받습니다. 12시간 기준은 PID가 다른 프로세스에 재사용된 경우를 대비한 것입니다.
  • 렌더 슬롯. RENDER_SLOTS를 정하면 slot.mjs(108줄)가 그 기계에서 동시에 도는 전체 렌더 수를 제한합니다. 기본 잠금 디렉터리가 사용자(UID)마다 하나이고 클론 사이에 공유되므로, 정확히는 같은 사용자의 렌더끼리 슬롯을 나눕니다. 슬롯은 이 디렉터리 안의 폴더 하나이고, 슬롯을 가진 프로세스는 15초마다 heartbeat 파일을 갱신합니다. 90초 넘게 heartbeat가 없으면 다른 프로세스가 가져갑니다. 상태 확인과 슬롯 획득은 mkdir의 원자성을 이용한 짧은 뮤텍스 안에서 합니다. 이미 렌더가 돌고 있는데 여유 메모리가 30% 미만이면 하나 더 시작하지 않습니다.

7.5 마스터링 스크립트는 오디오 손상을 의심합니다

mux.sh는 100줄짜리 셸 스크립트이고, 코드의 절반 이상이 "이 오디오 파일이 정말 온전한가"를 확인하는 데 쓰입니다. 디코더 오류 문구를 찾고, 실제로 디코딩된 길이와 컨테이너가 주장하는 길이를 비교하고, WAV 헤더가 선언한 데이터 길이가 파일보다 긴지 dd와 od로 직접 읽어 봅니다. ffmpeg … -f wav -로 스트리밍한 WAV는 헤더 길이가 자리표시자라서 같은 경고를 내는데, 이 세 가지 문구는 정상으로 봅니다.

라우드니스는 2패스 loudnorm으로 −14 LUFS, true peak −1.2dB에 맞춥니다. 주석에 1패스는 약 0.5 LU 어긋난다고 적혀 있습니다. 결과를 ebur128로 다시 재서 ±1 LU를 벗어나면 경고합니다. ffmpeg 7 이상을 요구하는 이유도 여기에 있습니다. 6.1은 loudnorm을 거친 오디오의 길이를 짧게 보고해서 멀쩡한 파일을 잘린 파일로 거부했다고 합니다(이슈 #7).

8. worker를 늘리면 얼마나 빨라지나요?

Urban Sketch 데모(32.4초, 24fps, 778프레임, 1920×1080)를 worker 1·3·6개로 렌더링했습니다. 기본값은 3이고 단독 렌더는 6까지 권합니다.

worker렌더러 보고 시간 (done 로그)명령 전체 경과 시간1개 대비
146초59.7초1.0배
327초41.7초1.4배
629초43.3초1.4배

done 로그의 시간은 worker 페이지를 열 때부터 구간 연결이 끝날 때까지입니다. 그 앞의 DUR 확인 단계는 들어가지 않습니다.

worker 1개와 3개는 각각 두세 번 돌렸고 done 시간은 매번 같았습니다(46초, 27초).

3개에서 6개로 늘려도 빨라지지 않습니다. 원인은 페이지 초기화였습니다. 이 데모는 window.READY가 true가 되기까지 14.5초가 걸립니다. 글꼴을 기다리고, 수채 종이와 도시 전체를 미리 만들기 때문입니다. 계산해 보면 이렇습니다.

  • worker 1개: 초기화 14.5초 + 778프레임 × 40.2ms(그리기 8.1 + 캡처 32.1) = 45.8초. 측정값 46초와 맞습니다.
  • worker 3개: 초기화 14.5초 + 260프레임 × 40.2ms = 24.9초. 측정값은 27초입니다.
  • worker 6개: 프레임 쪽은 5초 남짓으로 줄지만 브라우저 6개가 동시에 초기화하면서 서로 느려집니다.

그리고 done 시간과 명령 전체 경과 시간의 차이인 13~14초는 7.2절의 1단계입니다. DUR 하나를 읽으려고 페이지를 열고 READY까지 기다리기 때문에, 렌더할 때마다 페이지 초기화를 두 번 하게 됩니다. 이 데모에서는 전체 시간의 3분의 1입니다. DUR를 events.json에서 읽거나 첫 worker의 페이지에서 읽으면 줄일 수 있어 보입니다.

다만 영상 한 편에 30~60분이 걸린다는 README의 숫자를 생각하면 렌더링은 작은 부분입니다. 시간 대부분은 에이전트가 코드를 쓰고 검수하는 데 듭니다.

9. 같은 입력이면 정말 같은 영상이 나오나요?

계약대로라면 어떤 프레임이든 순서와 상관없이 같은 그림이 나와야 합니다. 세 단계로 확인했습니다.

1. 렌더 순서. 같은 네 시각(3.5초, 12.25초, 21초, 28.75초)의 프레임을 세 가지 방식으로 찍었습니다. 시간순으로, 역순으로, 그리고 새 브라우저에서 매번 영상 끝부분을 먼저 그린 뒤 해당 시각으로 건너뛰어 찍었습니다. PNG 해시가 4개 시각 모두 같았습니다.

2. 순차 렌더 반복. worker 0이 맡는 0~259프레임을 같은 방식(JPEG 품질 95)으로 두 번 찍었습니다. 260장 모두 바이트까지 같았습니다.

3. 실제 video.mjs 결과. 디코딩한 프레임의 해시(ffmpeg -f framemd5)를 비교했습니다.

비교다른 프레임
worker 1개, 1회차 vs 2회차없음
worker 3개, 1회차 vs 3회차197~249 (53장)
worker 3개, 2회차 vs 3회차446447, 449509 (63장)
worker 1개 vs worker 3개534장 (구간마다 따로 인코딩해서)

worker 1개는 다시 렌더링해도 같았습니다. worker 3개는 실행할 때마다 한 프레임이 조금 다르게 찍혔습니다. 1회차에서는 197번, 2회차에서는 446번이 달랐습니다. 3회차의 197번은 2회차와, 446번은 1회차와 같았습니다. 그 뒤 수십 장이 함께 다른 이유는 x264의 P프레임이 그 프레임을 참조하기 때문입니다. 차이는 다음 키프레임(250번, 510번) 직전에서 정확히 끝납니다.

차이는 매우 작습니다. 197번 프레임의 PSNR은 59.3dB였고, 차이를 20배로 키운 이미지도 거의 검은색이었습니다. 순차 렌더에서는 재현되지 않고 병렬일 때만 나타났으니 부하가 걸렸을 때의 래스터화 차이로 보입니다. 이 데모는 수채 합성을 WebGL로 하므로 GPU 쪽일 가능성이 있지만 원인까지 확인하지는 못했습니다.

HyperFrames 분석에서도 worker 수에 따라 글자 래스터화가 달라지는 것을 보았습니다. 페이지 쪽 계약을 지켜도 Chrome과 GPU 아래에서는 비트 단위 결정성이 보장되지 않습니다. 영상을 해시로 비교해 회귀 테스트를 한다면 worker 1개로 고정해야 합니다.

10. 본체는 프롬프트입니다

10.1 STYLE.md: 지켜야 할 것만 적습니다

모든 STYLE.md는 거의 같은 11개 절로 되어 있습니다. Watercolor Brush를 예로 들면 이렇습니다.

절내용
1. Essence, and what it is not"따뜻한 코튼지 위의 반투명 붓 자국. 벡터 선은 없음." 그리고 수묵화, 임파스토, 크레용 그림책이 아닌 이유
2. Materials & rendering종이 생성 방법, 붓 자국(폭 프로파일, ±22% 노이즈, 3~9개 갈필 트랙), 3번 겹쳐 칠하는 워시
3. Colour logic흰색은 칠하지 않은 종이, 어둡게 할 때는 덧칠 횟수를 늘림, 장면마다 3~5색과 잉크, 강조색 하나
4. Type & subtitles세리프 제목, 손글씨 메모, 자막 위치와 크기와 최소 노출 시간
5. Motion quality"칠해지며 나타나기"가 주된 움직임. 다 그린 뒤에만 흔들림
6. Camera grammar카메라 움직임 8개와 각각이 표현하는 것. "경로가 아니라 어휘"
7. Sound palette악기, 재질에서 나온 폴리, 침묵의 쓰임, 믹스 성격
8. Native moves이 매체만 할 수 있는 연출 7개와 어울리는 주제 예시
9. Pitfalls of the medium실제로 겪은 버그 7개
10. Engine데모의 어느 파일에 무엇이 있는지
11. Variation space데모와 다른 구조, 오프닝, 엔딩의 예

9절이 특히 실용적입니다. 예를 들면 "붓 자국 생성기가 전역 난수를 소비하므로 render(t) 안에서 만드는 획은 반드시 withSeed로 감싸야 한다. 그렇지 않으면 병렬 worker끼리 결과가 달라진다", "classic script가 전역 스코프를 공유하므로 비동기 초기화에서 선언 전의 const를 건드리면 READY가 영영 false가 되고 모든 스크린샷이 시간 초과로 실패한다" 같은 문장입니다. 사람이 쓰는 스타일 가이드라기보다 에이전트가 같은 실수를 반복하지 않게 하는 사고 기록에 가깝습니다.

STYLE.md에는 이야기가 없습니다. 이야기, 장면 수, 길이, 엔드 카드는 DEMO.md로 분리되어 있고, DEMO.md는 항상 이 문장으로 시작합니다.

One example among many. Don't reuse its story, arc, shots, props or timings.

10.2 데모를 따라 하지 못하게 막는 장치들

이 저장소의 지침은 상당 부분이 데모를 베끼지 말라는 내용입니다. 좋은 데모가 있으면 에이전트는 주제만 바꿔서 그대로 따라 하기 쉽기 때문입니다.

  • 기획서 먼저, 데모는 나중에. TREATMENT.md에 후보 구조 세 개와 선택 이유를 쓰기 전에는 DEMO.md도 데모 코드도 열지 않습니다. 플러그인 모드에서는 아예 데모 소스를 받지 않고, 기획서를 쓴 뒤 setup.sh demo <slug>로 따로 받습니다. 예외는 두 가지입니다. 실사 자료로 만드는 Edit & Documentary 스타일은 기획 전에 자료가 있어야 하므로 데모의 자료 탐색 도구(footage.py 등)를 먼저 써도 됩니다. 12장의 내 스타일 만들기에서는 스타일 프레임을 찍으려고 기반 스타일의 DEMO.md와 소스를 먼저 볼 수 있습니다. 두 경우 모두 이야기는 데모와 달라야 합니다.
  • 첫 아이디어는 클리셰. "the first idea that comes to mind for a style is usually its cliché." 도입부, 결말, 음악 구조도 주제에 맞춰 정하라고 합니다.
  • "Not the demo again." 납품 전에 구조, 오프닝, 시그니처 숏, 카메라 경로, 음악 구조, 엔딩 여섯 가지를 데모와 비교해서 넷 이상이 달라야 합니다. 마지막으로 자기 콘택트 시트를 데모 포스터 옆에 놓고 봅니다.
  • 레퍼런스는 문법만. 기획 단계에서 벤치마크 작품을 한두 개 고르고 "배울 것"과 "가져오지 않을 것"(캐릭터, 디자인, 멜로디, 특정 숏)을 나눠 적습니다.

10.3 DIRECTOR.md: 숫자로 적은 연출 규칙

감독 지침은 "좋게 만들어라" 대신 확인할 수 있는 숫자를 줍니다.

  • 영상은 소리, 리듬, 카메라, 연출 순서로 평가합니다. 예쁜 프레임은 출발점일 뿐입니다.
  • 처음 3초 안에 훅을 둡니다. 느린 로고 공개로 시작하지 않습니다.
  • 보이는 동작에는 모두 소리가 있어야 합니다. 클라이맥스 앞에 진짜 침묵을 두 번 이상 두고, 소리로 장면을 넘기는 J컷·L컷을 두 번 이상 씁니다.
  • 카메라 움직임은 네 종류 이상 씁니다. 기본 페이드와 하드컷 대신 매체 안에서 전환합니다(종이가 접히며 다음 장면이 되는 식).
  • 자막은 최소 1.8초, 그리고 말하는 시간보다 0.6초 이상 길게 보여 줍니다. 타이틀 카드는 4초 이상 보여 줍니다.
  • 캐릭터 포즈는 if로 바꾸지 말고 키프레임과 이징으로 섞습니다. 손에 든 물건은 "두 손바닥 사이, 두 팔 사이의 깊이"에 둡니다. "걱정"과 "분노"는 눈썹 하나 차이이니 실제 크기로 확인합니다.
  • 수정은 한 번에 몰아서 합니다. 검수에서 나온 것을 한 번에 고치고 한 번 더 확인합니다.

10장 "가장 자주 본 실패"에는 피사체가 너무 작은 화면, 같은 색 위에 같은 색을 올린 화면, 자막이 피사체를 가리는 화면, 전환 중의 빈 프레임 같은 목록이 있습니다. 실제 제작에서 나온 목록으로 보입니다.

10.4 지도 규칙

DIRECTOR.md 13절은 모든 지도, 지구본, 위성 화면에 적용되는 규칙입니다. 영상이 국경을 다루지 않으면 국경을 그리지 않고, 분쟁 지역은 프레임 밖에 두라고 합니다. 여기까지는 일반적인 원칙이지만, 중국이 화면에 들어오면 중국 표준 지도를 따르라는 구체적인 규칙이 이어집니다(대만, 남중국해 도서, 장난과 악사이친의 표기, 대만·홍콩·마카오를 "국가" 목록에 넣지 않기). Natural Earth나 OpenStreetMap의 기본 경계 데이터로는 중국을 그리지 말라고 명시합니다. 최근 커밋(05f2481, 88bfcc2)에서 이 절을 추가하고 기존 데모의 지구본과 지도를 고쳤습니다. 만든 사람의 주 사용자층이 중국어권이라는 점이 드러나는 부분이고, 다른 지역에서 이 라이브러리로 지도 장면을 만든다면 이 규칙이 에이전트의 판단에 그대로 들어간다는 점을 알고 써야 합니다.

11. 에이전트가 스스로 확인하는 장치들

사람이 영상을 매번 볼 수 없으니 에이전트가 기계적으로 확인할 수 있는 검사가 여럿 있습니다.

도구확인하는 것
readcheck.mjs화면 글자가 읽을 수 있을 만큼 오래 남는지. 0.04초 간격으로 TEXTS(t)를 받아서 "CJK 글자 수 ÷ 4.5 + 그 밖의 글자 수 ÷ 15 + 1.5초" 이상 완전히 보였는지 봅니다
asr_check.pyTTS가 대본대로 읽었는지. faster-whisper로 받아 적어 영어는 단어 단위, 중·일·한은 글자 유사도 0.92 이상으로 비교합니다
mux.sh오디오 손상(발견하면 실패), 최종 라우드니스 −14 LUFS ±1과 true peak −1dB 이하(기준을 벗어나면 경고)
cuecheck.py (일부 데모)그림의 타격 지점과 음악 큐의 오차
ffmpeg blackdetect빈 프레임
tools/release.py check(라이브러리용) 아무 데서도 참조하지 않는 이미지, 도달하지 않는 데모 파일, PRODUCTION_LOG 같은 작업 흔적 문구

readcheck의 규칙에는 에이전트를 겨냥한 예외 처리가 있습니다. 타자기 효과로 글자가 하나씩 나타날 때 "지금까지 친 부분"을 TEXTS에 보고하면 글자가 늘 때마다 새 텍스트로 판정되어 검사 전체가 실패합니다. 그래서 첫 프레임부터 전체 문장을 보고하라고 계약에 적어 두었습니다. 검사를 통과하려고 계약을 우회하는 경우를 미리 막은 것입니다.

asr_check.py의 중국어 숫자 비교도 꼼꼼합니다. 十九와 19, 两千零二十六과 2026, 一万九千과 1.9万, 百分之三十과 30%를 같은 값으로 봅니다. 三点五十分은 3点50分과 맞지만 3点5分과는 다릅니다. 파일 머리 주석에는 3:50과도 맞는다고 적혀 있지만, 직접 호출해 보면 숫자는 같아도 分 한 글자 때문에 글자 유사도가 0.89로 기준(0.92)에 못 미쳐 실패합니다. 이 부분은 외부 기여자가 #10으로 보고하고 수정안을 보냈습니다. 관리자는 "잘못 들은 숫자는 여전히 실패해야 한다"며 값으로 비교하는 방식으로 다시 구현했고, 기여를 남기려고 원래 PR도 병합했습니다. TECHNIQUE.md는 "TTS 텍스트에는 숫자를 말로 풀어 쓰고 자막에는 숫자로 쓰라"고 하는데, 이 검사가 그 둘을 잇습니다.

readcheck의 CJK 판별에는 한글(\p{Script=Hangul})도 들어 있어서 화면의 한국어 글자도 초당 4.5자로 계산됩니다. 자막 바는 .srt가 노출 시간을 정하므로 TEXTS에 넣지 말라고 되어 있습니다. 다만 TTS 쪽에는 한국어 전용 경로가 없습니다. tts_zh.py가 edge-tts의 음성 이름을 받으므로 한국어 음성을 지정할 수는 있지만, 문서는 영어와 중국어만 다룹니다.

12. 내 스타일 만들기

10월 19일에 처음 들어왔고, 10월 22일에 두 번째 버전과 함께 README에 공지된 기능입니다. 라이브러리 스타일을 바꾸거나 빈 페이지에서 시작해서 자기 스타일을 만들고 ~/lemo-my-styles/<slug>/에 보관합니다. 라이브러리 업데이트나 플러그인 삭제와 상관없이 남습니다.

절차(MY-STYLES.md)는 이 저장소의 성격을 잘 보여 줍니다.

  1. 한 번에 한 질문. 선택지 2~3개와 추천 이유, 그리고 마지막에 항상 "자유 대화"를 둡니다. 질문은 열 개 정도까지입니다.
  2. 바꿀 수 있는 정도를 세 묶음으로 나눕니다.
    • 팔레트, 글꼴, 템포, 사운드, 시그니처 무브는 바꿔도 안전합니다.
    • 재질과 렌더링, 색 논리, 카메라 문법을 바꾸면 스타일의 성격이 달라집니다.
    • 1절 Essence와 10절 Engine은 바꿀 수 없습니다. 이걸 바꾸면 다른 스타일이니 원하는 결과에 더 가까운 기반 스타일을 추천합니다.
  3. 요약 승인 → 스타일 프레임 한 장 → 15~30초 데모 → 보관. 라이브러리 스타일을 바꾼 경우 같은 프레임을 원본과 나란히 찍어 보여 줍니다. 두 번 고쳐도 안 되면 더 가까운 기반 스타일을 제안합니다.

보관한 스타일은 CHANGES.md(절마다 원래 값, 바꾼 값, 이유)와 library_version(라이브러리 커밋)을 함께 남깁니다. 기반 스타일이 나중에 바뀌면 에이전트가 한 번 알리고 유용한 수정을 가져올지 묻습니다. 소프트웨어의 fork와 upstream 관계를 스타일 프롬프트에 적용한 셈입니다.

13. HyperFrames와 무엇이 다른가요?

두 프로젝트 모두 HTML을 headless Chrome으로 찍어 MP4를 만들고 둘 다 에이전트용이라고 말합니다. 하지만 어디에 투자했는지가 정반대입니다.

HyperFramesLemo-Opuscar
시간을 누가 정하나요?렌더러가 페이지의 시계와 애니메이션 13종을 원하는 시각으로 옮깁니다페이지가 render(t)로 직접 그립니다
렌더러 규모엔진·프로듀서·코어 12만 줄 이상core/render/*.mjs 393줄
그림을 그리는 방법GSAP, CSS, Lottie 등 기존 웹 애니메이션Canvas 2D, WebGL, three.js로 매번 직접 작성
캡처beginFrame, drawElement, 스크린샷 중 선택JPEG 스크린샷 하나
소리영상·오디오 트랙을 배치합니다작곡, 폴리, TTS, 믹싱, 마스터링까지 직접 합니다
에이전트에게 주는 것스킬 21개, JSON을 출력하는 lint·check CLI스타일 66개의 프롬프트(약 30만 단어)와 감독·제작 지침
품질을 지키는 곳lint 규칙 97개와 렌더러의 결정성숫자로 적은 연출 규칙과 자체 검사 도구

HyperFrames는 "에이전트가 평범한 웹 애니메이션을 쓰면 렌더러가 알아서 영상으로 만든다"는 방향입니다. Lemo-Opuscar는 "렌더러는 최소한으로 두고, 에이전트가 붓 엔진부터 직접 짜게 하되 무엇을 지켜야 하는지를 아주 자세히 알려 준다"는 방향입니다. 시간 모델만 보면 Opuscar는 Remotion의 useCurrentFrame() 쪽에 가깝습니다. React 없이 순수 함수 하나로 한 것입니다.

어느 쪽이 맞는지는 무엇을 만들고 싶은지에 달려 있습니다. 슬라이드형 모션 그래픽이라면 HyperFrames 쪽이 빠르고, 우키요에나 수채화처럼 웹 애니메이션 라이브러리로는 표현하기 어려운 질감이라면 Opuscar 쪽이 맞습니다. 다만 Opuscar 방식은 에이전트가 매번 수천 줄의 그리기 코드를 새로 써야 하므로 모델 성능에 크게 기댑니다. README가 Opus 5.5 전용이라고 경고하는 이유도 여기 있을 것입니다.

14. 코드와 문서를 읽는 추천 순서

  1. AGENTS.md: 88줄입니다. 요청 유형별로 어느 문서를 읽을지 정하는 라우터입니다.
  2. styles/watercolor/STYLE.md: 스타일 하나가 어떤 절로 이루어지는지 가장 잘 보입니다. 9절 Pitfalls는 꼭 읽어 볼 만합니다.
  3. DIRECTOR.md 4절과 11절: 기획서 형식과 납품 전 검수 목록입니다.
  4. TECHNIQUE.md 2절: 페이지 계약과 build.sh 예시입니다.
  5. core/render/page.mjs → video.mjs: 합쳐서 134줄입니다. 렌더러의 핵심입니다. 정적 서버와 브라우저 실행은 page.mjs가 불러오는 serve.mjs, browser.mjs에 있습니다.
  6. core/render/readcheck.mjs: 51줄짜리 읽기 시간 검사입니다. 계약의 주석이 잘 쓰여 있습니다.
  7. core/render/mux.sh: 손상 오디오를 어떻게 의심하는지 보입니다.
  8. styles/urban-sketch/demo/film.js: 실제 render(t)와 EV가 어떻게 생겼는지 봅니다.
  9. MY-STYLES.md 2절: 질문 설계(한 번에 한 질문, 추천 먼저, 자유 대화 탈출구)가 다른 에이전트 스킬에도 쓸 만합니다.

15. 인상적인 설계 포인트

1. 렌더러를 작게 두고 계약을 단단하게 했습니다. 페이지와의 약속은 전역 변수 다섯 개뿐입니다. 그래서 Canvas 2D, WebGL, three.js, SVG 데모가 같은 도구로 렌더링되고, 사용자가 자기 스택을 써도 됩니다.

2. 스타일과 이야기를 파일 단위로 나눴습니다. STYLE.md에는 불변 규칙만, DEMO.md에는 데모 이야기만 둡니다. 에이전트가 무엇을 지키고 무엇을 새로 만들어야 하는지가 파일 경계로 드러납니다.

3. "따라 하지 마라"를 절차로 강제합니다. 기획서를 먼저 쓰게 하고, 데모 소스를 나중에 받게 하고, 납품 전에 여섯 항목 중 넷이 달라야 한다는 숫자 기준을 둡니다. 좋은 예시가 오히려 결과물을 비슷하게 만드는 문제를 정면으로 다룹니다.

4. 품질 판단을 검사로 옮겼습니다. "화면 글자를 충분히 오래 보여 줘라"는 readcheck가, "대본대로 읽었나"는 asr_check가, "음량이 맞나"는 mux.sh가 확인합니다. 사람이 보지 않아도 에이전트가 숫자로 문제를 알 수 있습니다.

5. 실패 기록이 문서가 됩니다. STYLE.md의 Pitfalls, DIRECTOR.md의 "가장 자주 본 실패", TECHNIQUE.md와 커밋 메시지에 남은 ffmpeg 6.1 이야기까지 대부분 실제 제작에서 겪은 문제입니다. 커밋 메시지도 "gable roofs wound outward, so they stop flickering against the wall tops"처럼 무엇이 왜 깨졌는지를 적습니다.

16. 주의해서 볼 지점

1. Opus 5.5에 맞춰져 있습니다. README가 직접 경고합니다. 렌더러가 작은 만큼 그림 품질은 모델이 쓰는 코드에 달려 있습니다.

2. 데모가 바로 렌더링되지 않습니다. 데모 글꼴, 오디오, 텍스처는 git에 없습니다(CREDITS에는 출처만 적혀 있습니다). 데모는 "읽기용"이라고 문서에 적혀 있지만, 기법을 확인하려고 돌려 보면 글꼴부터 직접 받아야 합니다. Halftone Dossier처럼 공통 계약(window.DUR)을 따르지 않는 초기 데모도 남아 있습니다.

3. 공통 도구의 테스트가 저장소에 없습니다. slot.mjs에 "exported for tests only"라는 주석이 있지만 해당 테스트 파일은 저장소에서 찾지 못했습니다. 데모 폴더의 test.js들은 대부분 장면 확인용 페이지입니다. 품질 관리는 사람의 승인과 release.py check 같은 게이트에 기대고 있습니다.

4. 병렬 렌더는 비트 단위로 같지 않을 수 있습니다. 9장에서 보았듯 worker 3개로 찍으면 실행마다 한 프레임 정도가 눈에 보이지 않을 만큼 달라집니다. 페이지 초기화를 두 번 하는 구조(8장)도 짧은 영상에서는 비용이 큽니다.

5. 일부 경로는 외부 서비스로 텍스트를 보냅니다. 중국어 음성은 edge-tts로 Microsoft에 내레이션 텍스트를 보냅니다. 기본판의 DIRECTOR.md는 "온라인 서비스이고 연결이 필요하다"는 점을 질문할 때 알리라고만 합니다. 플러그인 디렉터리 에디션을 만드는 tools/directory.py는 이 부분을 "텍스트가 Microsoft로 가므로 사용자가 요청하고 동의할 때만 쓰라"는 문장으로 바꿔 넣습니다.

6. 라이선스 예외가 있습니다. 데모 영상은 CC BY 4.0이지만 음성 트랙 세 개(Game Show Flat의 macOS say, Paper-cut Lightbox의 Edge TTS, Broadcast News Package의 Doubao TTS)는 제외됩니다.

7. 한 사람이 빠르게 만들고 있습니다. 5주 동안 커밋이 80개 쌓였고, 스타일은 39개에서 66개로 늘었습니다. 외부 기여 커밋은 스타일 하나(Memphis Diorama), 중국어 숫자 비교, readcheck 수정 하나로 세 개입니다. 지침 문서가 자주 바뀌므로 플러그인을 업데이트하면 에이전트의 행동도 바뀝니다.

17. 결론

Lemo-Opuscar의 렌더러(core/render의 .mjs 파일)는 400줄도 되지 않습니다. 페이지가 render(t)로 프레임을 그리면 headless Chrome이 JPEG로 찍고 ffmpeg가 이어 붙이는 것이 전부입니다. 대신 이 저장소는 30만 단어 가까운 글을 들여 에이전트에게 무엇을 지켜야 하는지와 무엇을 따라 하면 안 되는지를 가르칩니다. 스타일은 11개 절의 불변 규칙으로 적고, 이야기는 데모 문서로 떼어 내고, 연출은 숫자로 적고, 품질 판단은 검사 도구로 옮겼습니다.

직접 측정해 보니 렌더러는 단순한 만큼 예측하기 쉬웠습니다. 프레임 비용의 80%는 JPEG 캡처였고, 짧은 데모에서는 페이지 초기화가 전체 시간을 좌우해서 worker를 3개에서 더 늘려도 빨라지지 않았습니다. worker 1개로 찍은 영상은 매번 같았고, 병렬로 찍으면 가끔 한 프레임이 사람 눈에 보이지 않을 만큼 달라졌습니다.

에이전트 스킬을 설계하는 입장에서 이 저장소가 주는 교훈은 렌더링보다 문서 쪽에 있습니다. 좋은 예시를 주면 에이전트는 그것을 베낍니다. Lemo-Opuscar는 예시를 주면서도 베끼지 못하게 하는 절차를 문서로 만들어 두었습니다.


분석 일자: 2026-11-04 대상 커밋: bebbbe1 (main 브랜치, 2026-11-01) 저장소: https://github.com/lemomo-ai/lemo-opuscar (코드 MIT, 데모 CC BY 4.0) 측정 환경: Apple M5 Pro(15코어), macOS, Node 24.15, FFmpeg 8.1, playwright-core 1.63, Chrome Headless Shell 153. Urban Sketch 데모(32.4초, 24fps, 1920×1080, 778프레임). 이 글의 데모 프레임 이미지는 LemoLab의 데모를 직접 렌더링한 것입니다(CC BY 4.0).

Claude Code와 함께 작성됨

GitHub에서 편집 ↗