스킬·렌더링 도구
3961ddb 기준.
lemo-opuscar는 코딩 에이전트가 그림·소리·편집을 코드로 제작하도록 돕는 영상 스타일 라이브러리입니다. 사용자는 주제와 스타일을 고르고, 에이전트는 그 스타일의 규칙을 읽어 별도의 작품을 설계합니다. 화면은 주로 Canvas 2D나 Three.js로 그리며, 완성된 웹페이지를 프레임마다 촬영해 영상 파일로 바꿉니다.
이 저장소에서 가장 중요한 발상은 동영상을 녹화하는 대신, 원하는 시각의 화면을 함수로 계산한다는 것입니다.
window.render(3.5)는 3.5초 시점의 한 장면을 그립니다.
이 함수를 1/24초 간격으로 호출하면, 연속된 정지 화면이 영상이 됩니다.
STYLE.md는 화풍과 움직임의 규칙을 설명하는 교재,
demo/는 그 규칙으로 만든 졸업 작품,
core/는 여러 작품이 함께 사용하는 촬영·녹음 장비입니다.
견본 작품의 대사만 바꾸는 것으로 끝내지 않습니다.
AGENTS.md는 새 이야기의 구성을 먼저 정하고, 데모에서는 구현 기법을 참고하도록 안내합니다.
| 층 | 대표 파일 | 독자가 배울 것 |
|---|---|---|
| 연출과 규칙 | DIRECTOR.md, TECHNIQUE.md, styles/*/STYLE.md | 추상적인 요구를 구체적인 제작 조건으로 바꾸기 |
| 개별 작품 | styles/swiss-motion/demo/main.js, styles/swiss-motion/demo/film.js, styles/swiss-motion/demo/mix.py | 장면·타이밍·사운드를 실제 코드로 표현하기 |
| 공용 도구 | core/render/, core/audio/, core/tts/ | 여러 작품을 처리하는 재사용 가능한 제작 파이프라인 |
styleboard/catalog.json에는 59개 항목이 있고, 실제 스타일 디렉토리도 59개입니다.
STYLE.md 파일 자체는 60개인데, 하나는 새 스타일용 styles/_template/입니다.
styleboard/build.py의 load_styles()가 밑줄로 시작하는 디렉토리를 제외하므로 숫자가 일치합니다.
스타일은 편집·다큐멘터리, 실증 영상, 회화, 게임, 재질·3D 등을 포함한 11개 분류에 속합니다.
먼저 core/render/page.mjs의 페이지 약속을 이해하고,
styles/swiss-motion/demo/main.js가 그 약속을 구현하는 모습을 보세요.
마지막으로 core/render/video.mjs에서 한 장면이 MP4까지 가는 경로를 따라가면 됩니다.
2026-10-23 GitHub 기준 스타 5,814개, 포크 653개입니다. 저장소 생성일은 2026-09-26으로, 생성 후 27일 시점의 수치입니다. TrendShift Weekly #21의 분석 대상으로서 눈에 띄는 규모이지만, 이 숫자만으로 사용자 만족도나 영상 품질을 입증할 수는 없습니다.
관심을 끄는 지점은 결과물에 그치지 않습니다. 스타일 설명, 구현 예제, 제작 도구가 함께 있어 “어떻게 만들었는가”를 코드까지 거슬러 올라갈 수 있습니다. 에이전트가 그림뿐 아니라 음악과 믹싱까지 작성한다는 소개는 README의 주장입니다. 실제 저장소에서도 장면 코드, 악보 데이터, 효과음 합성 함수, 믹서가 각각 확인됩니다.
README의 변경 안내는 2026-10-16에 실증 영상 8종,
2026-10-22에 편집·다큐멘터리 8종과 개인 스타일 제작 기능을 소개합니다.
styles/caption-news/STYLE.md는 실제 영상·보고서·통계를 배치하는 규칙을,
MY-STYLES.md는 기존 스타일을 변형해 자신의 스타일로 보관하는 절차를 설명합니다.
학습 관점에서는 확장 방향이 흥미롭습니다.
같은 렌더러 위에서 직접 그린 애니메이션도 만들고, 출처가 있는 문서·사진·영상을 조합할 수도 있습니다.
외부 자료를 쓰는 스타일은 FACTS.md, CREDITS, 자료 목록을 통해 근거와 권리를 관리하도록 설계되어 있습니다.
이것이 실제 인기의 원인이라는 통계는 없으며, 소스와 공개 소개에서 읽을 수 있는 매력 요인입니다.
| 방식 | 잘 맞는 일 | lemo-opuscar의 상대적 장점과 대가 |
|---|---|---|
| 일반 영상 편집기 | 타임라인을 보며 손으로 컷과 효과 조정 | 코드와 시간표를 수정해 반복 제작하기 좋음. 대신 편집 UI 대신 코드·도구를 익혀야 함 |
| 영상 생성 모델 | 장면의 모습을 자연어로 요청 | 좌표·숫자·문자를 코드에서 직접 지정. 대신 복잡한 실사 장면을 수식과 그림만으로 만들기는 어려움 |
| FFmpeg 단독 | 변환·자르기·인코딩·필터 적용 | 스타일 규칙과 그림 엔진, 음성·음악 도구까지 제공. 최종 인코딩에서는 FFmpeg를 함께 사용 |
| 개별 그래픽 예제 | 특정 효과나 장면을 배우기 | 렌더링·검사·배포까지 연결된 사례가 많음. 데모마다 구조와 완성도는 다를 수 있음 |
영상 편집기가 손으로 조작하는 주방이라면, 이 저장소는 조리법과 자동 조리 장비를 함께 공개한 주방입니다. 재료와 시간을 정확히 정할 수 있지만, 어떤 맛을 만들지 결정하는 일까지 사라지지는 않습니다.
README와 TECHNIQUE.md에는 외부 영상 없이 만든다는 포괄적인 소개가 남아 있습니다.
그러나 styles/caption-news/STYLE.md는 실제 영상과 문서를 사용하며,
styles/news-package/demo/CREDITS는 AI로 생성한 배경 이미지 두 장과 외부 음성 서비스를 명시합니다.
따라서 공통점은 코드로 화면을 구성하고 편집·출력한다는 데 있습니다. 모든 데모가 외부 영상·생성 이미지·온라인 서비스 없이 재현된다고 해석하면 맞지 않습니다. 또한 Claude Opus 5.5에 맞춘 스타일이라는 설명은 README 기준이며, 다른 모델과의 성능 비교표는 제시되지 않습니다.
| 기술 | 확인한 버전·선언 | 실제 역할과 위치 |
|---|---|---|
| JavaScript ESM | 루트 package.json: type: module | 장면과 도구를 모듈로 분리. core/lib.js에 보간·난수 함수 |
| Canvas 2D | 브라우저 내장 API, npm 버전 없음 | styles/swiss-motion/demo/main.js의 getContext('2d') |
| Three.js | 선언 ^0.170.0, 잠금 0.170.0 | 3D 장면과 core/three/post.js 후처리 |
| WebGL / 셰이더 | 브라우저 그래픽 API | core/post/crt.js의 CRT 효과, core/three/post.js의 심도·색보정 |
| HTML·CSS·폰트 | 정적 페이지와 로컬 폰트 | 각 데모의 index.html, 갤러리의 styleboard/template.html |
루트 의존성에는 React·Vue·웹 프레임워크가 없습니다. 영상의 화면은 일반적인 앱 UI보다 그림을 그리는 캔버스에 가깝습니다. 다만 스타일마다 구현이 독립적이므로, 특정 데모의 구성까지 루트 의존성만으로 판단해서는 안 됩니다.
| 기술 | 버전 근거 | 하는 일 |
|---|---|---|
| Node.js | package.json: >=20 | 브라우저 제어, 프레임 분배, FFmpeg 하위 프로세스 실행 |
| Playwright Core | 선언 ^1.63.0, 잠금 1.63.0 | core/render/page.mjs에서 Chromium을 열고 화면 캡처 |
| Python | TECHNIQUE.md: 3.11 이상 | 음성·사운드·자막·갤러리 생성. 설치 도구는 uv 사용 시 3.12 선택 |
| NumPy·SciPy | requirements.txt, 버전 미고정 | 음원 배열 생성, 필터, 합성, 믹싱 |
| soundfile·soxr·pillow | requirements.txt, 버전 미고정 | 음원 입출력·재표본화·검토 이미지 제작 |
| 음성 선택 구성 | requirements-voice.txt: kokoro-onnx, edge-tts, faster-whisper, av<19 | 로컬 TTS, 온라인 TTS, 발화 내용 검사. 세 패키지는 버전 미고정 |
| 음악·개발 선택 구성 | requirements-music.txt: numba / requirements-dev.txt: librosa | 현악기 물리 모델의 가속 / 샘플 인덱스 재생성. 모두 버전 미고정 |
core/render/serve.mjs는 http.createServer()를 이용한 작은 정적 파일 서버입니다.
127.0.0.1의 빈 포트에 바인딩하고, 브라우저가 모듈·이미지·폰트를 읽을 수 있게 합니다.
계정·결제·데이터베이스를 제공하는 서비스 백엔드와 역할이 다릅니다.
package.json의 ^1.63.0은 허용하는 버전 범위,
package-lock.json의 1.63.0은 기록된 구체적인 버전입니다.
Python 목록에는 대체로 이런 고정 정보가 없습니다.
av<19는 정확한 버전 고정이 아니라 상한 제한입니다.재현 가능한 결과를 배우려면 그림 코드뿐 아니라 브라우저·패키지·폰트·원본 소재의 버전도 함께 보아야 합니다.
루트 package.json의 자체 버전은 0.1.0이며 private: true입니다.
이는 GitHub의 최신 릴리스 태그 thanks와 별개의 정보입니다.
plugin/skills/lemo-opuscar/scripts/setup.sh, 소재 다운로드는 tools/fetch.sh, 배포 보조는 tools/publish.sh에 있습니다.styleboard/build.py가 스타일 메타데이터에서 갤러리를 생성합니다. 이것을 실행하면 문서나 카탈로그가 갱신될 수 있습니다..github/workflows/pages.yml은 ubuntu-latest, Python 3.12, checkout @v7, setup-python @v7을 사용합니다.@v5, deploy-pages @v5. 웹용 영상은 web 릴리스, 고화질 영상은 films 릴리스로 분리합니다.Pages 워크플로는 갤러리를 만들고 영상을 다운로드한 뒤, 링크된 영상 파일이 모두 존재하는지 검사합니다.
스타일별 영화를 새로 렌더링하는 워크플로는 아닙니다.
커밋 3961ddb에는 Dockerfile, Cargo.toml, pyproject.toml, go.mod가 없으며,
공통 빌드 체계를 Docker·Rust·Go 프로젝트처럼 설명할 근거도 없습니다.
core/render/page.mjs의 openDemo()는 작품 내부의 그림 엔진을 알지 못합니다.
index.html을 열고 window.READY === true를 기다릴 뿐입니다.
requireDemo()는 진입 파일의 존재를 확인하고,
takeSize()는 화면 크기를 검사합니다.
| 페이지가 공개하는 값 | 소비하는 도구 | 약속 |
|---|---|---|
READY | openDemo() | 폰트·이미지 등 준비가 끝나면 true. 대기 상한 180초 |
render(t) | 정지 화면·동영상 도구 | 초 단위 시각을 받아 그 시점의 화면을 그림 |
DUR | 동영상·이벤트·가독성 도구 | 작품 전체 길이. core/render/video.mjs는 양수인지 확인 |
EV | core/render/events.mjs | 효과음·대사 등의 시각과 종류. 없으면 빈 배열 |
TEXTS(t) | core/render/readcheck.mjs | 화면 글자의 식별자·전체 문구·사각형 좌표 |
openDemo()는 페이지의 JavaScript 오류와 스크립트·문서의 HTTP 오류 응답을 만나면 중단합니다.
반면 이미지·폰트·선택 데이터의 HTTP 오류는 경고하고 계속합니다.
대체 처리가 없는 필수 이미지를 빠뜨리면 경고만으로는 완성도를 보장할 수 없으므로, 작품 자체의 준비 조건도 중요합니다.
styles/swiss-motion/demo/main.js는 이 계약의 실제 구현입니다.
폰트를 불러오고 악보·대사·발화 시간을 읽은 뒤,
renderFilm()과 buildEvents()를 전역 인터페이스에 연결합니다.
아래는 해당 파일에서 화면 계약에 해당하는 부분입니다.
window.DUR = DUR;
window.EV = buildEvents();
window.SUBS = subs();
const Q = new URLSearchParams(location.search);
window.render = t => renderFilm(g, t, {
nosub: Q.has('nosub'), poster: Q.has('poster')
});
window.READY = true;
core/render/video.mjs는 전체 프레임 수를 Math.round(DUR * FPS)로,
작업자당 범위를 Math.ceil(TOTAL / WK)로 정합니다.
작업자마다 별도의 브라우저를 열고 f / FPS를 render()에 전달합니다.
// video.mjs의 핵심 흐름을 줄인 학습용 의사 코드
const TOTAL = Math.round(DUR * FPS);
const per = Math.ceil(TOTAL / WK);
await Promise.all(workers.map(async w => {
const a = w * per;
const b = Math.min(TOTAL, a + per);
for (let f = a; f < b; f++) {
await page.evaluate(t => window.render(t), f / FPS);
const jpeg = await page.screenshot({ type: 'jpeg', quality: 95 });
// 실제 코드는 쓰기 버퍼가 차면 drain 또는 종료를 기다린다.
await sendToEncoder(jpeg);
}
}));
Swiss Motion 데모의 styles/swiss-motion/demo/film.js는 DUR = 44입니다.
기본 24 fps라면 1,056프레임이고, 기본 작업자 3개는 각각 352프레임을 맡습니다.
첫 작업자는 0~351번, 다음은 352~703번, 마지막은 704~1,055번을 계산합니다.
마지막 프레임의 시각은 정확히 44초가 아니라 1055 / 24초입니다.
프레임을 병렬로 만들 수 있는 이유는 각 장면이 직전 프레임의 실행 결과에 의존하지 않아야 하기 때문입니다. 위치를 매번 조금씩 더하는 대신 시각에서 바로 계산해야 합니다. 다만 결정성은 제작 규칙이며, 렌더러가 모든 장면 코드의 결정성을 자동 증명해 주지는 않습니다.
“앞사람이 어디까지 그렸는지 보고 이어 그리기”는 작업을 나누기 어렵습니다. “정답 좌표가 적힌 352장의 설계도를 각각 받기”는 서로 기다리지 않고 작업할 수 있습니다. 시간으로 계산하는 화면이 바로 그 설계도입니다.
styles/swiss-motion/demo/film.js의 buildEvents()는
{t, type, ...} 형태의 이벤트를 생성하고 시간순으로 정렬합니다.
add(T.land, 'thump', {g: 1.1})처럼 착지 시점과 충격음을 연결하며,
대사도 type: 'voice'와 대사 식별자로 기록합니다.
core/render/events.mjs는 이를 {dur, ev} 형태의 JSON으로 내보냅니다.
styles/swiss-motion/demo/mix.py 같은 작품별 믹서가 읽으면 Python에서 JavaScript의 그림 코드를 직접 이해할 필요가 없습니다.
core/audio/sfx.py의 add(buf, x, at, gain, pan)는 초 단위 시각을 샘플 위치로 바꿔 소리를 섞습니다.
// events.json의 형태를 설명하는 학습용 예시
{
"dur": 6,
"ev": [
{"t": 1.5, "type": "thump", "g": 0.8},
{"t": 2.0, "type": "voice", "id": "line1"}
]
}
파일은 느슨한 결합을 만듭니다. JSON·WAV·MP4가 정상적으로 생성되면 다른 언어의 도구로 바꾸기도 쉽습니다.
반대로 시간표를 여러 곳에 중복해서 적으면 화면과 소리가 어긋납니다.
TECHNIQUE.md가 하나의 시간표를 공유하도록 권하는 이유입니다.
모든 데모가 똑같은 이름의 timeline.js를 쓰는 것은 아닙니다.
core/render/video.mjs는 JPEG를 FFmpeg의 표준 입력으로 보냅니다.
인코딩 옵션은 libx264, -preset medium, -crf 14, yuv420p입니다.
쓰기 버퍼가 차면 drain이나 인코더 종료를 기다리며,
작업자별 영상은 마지막에 concat과 -c copy로 이어 붙입니다.
| 문제 | 대응 코드 | 범위와 한계 |
|---|---|---|
| 인코더보다 캡처가 빠름 | ff.stdin.write()와 drain | 한 파이프의 흐름을 조절. 전체 시스템의 메모리를 제한하는 기능은 아님 |
| 동일 출력에 동시 쓰기 | core/render/video.mjs의 .lock, flag: 'wx' | 출력 파일 단위 잠금. 프로세스 생존 여부와 잠금 나이를 확인 |
| 여러 작품이 동시에 렌더링 | core/render/slot.mjs의 acquire(), tryTake() | RENDER_SLOTS 설정 시 선택적으로 작동 |
| 죽은 작업이 자리 차지 | core/render/slot.mjs의 stale() | 15초마다 생존 표시 갱신, 90초 정체 시 회수 가능 |
core/render/slot.mjs의 withMutex()는 디렉토리 생성의 원자성을 이용해 자리 배정을 보호합니다.
추가 작업을 시작할 때 기본 여유 메모리 기준은 30%이지만, 실행 중인 작업이 없으면 하나는 허용합니다.
따라서 이것은 강제 메모리 상한이 아닌 시작 시점의 혼잡 조절입니다.
영상 작업자 수 --workers와 전체 렌더링 수 RENDER_SLOTS도 서로 다른 값입니다.
STYLE.md는 유지할 스타일 규칙, DEMO.md는 특정 예제 작품의 설명입니다.
style.json은 검색·전시용 메타데이터이며, 영상의 장면 전체를 선언하는 공통 설정 파일은 아닙니다.
예를 들어 제목과 분류를 바꾸는 일과, 이야기·그림·소리를 새로 만드는 일은 서로 다릅니다.
plugin/에는 실제 영상 엔진보다 라이브러리를 찾아 연결하는 진입점이 들어 있습니다.
설치 스크립트는 기본 안내·공용 도구를 가져오고, 필요할 때 특정 데모를 추가하는 희소 체크아웃을 사용합니다.
전체 소스를 받는 방식과 스킬의 첫 설치가 차지하는 용량이 다른 이유입니다.
| 언어·형식 | 파일 수 | 물리적 줄 수 |
|---|---|---|
| JavaScript | 533 | 82,976줄 — .js, .mjs, .cjs 합계 |
| Python | 313 | 37,171줄 |
| HTML | 74 | 11,287줄 |
| CSS | 37 | 8,866줄 |
| Shell | 97 | 2,524줄 |
| 위 다섯 종류 합계 | 1,054 | 142,824줄 — 약 14.3만 줄 |
| Markdown | 164 | 18,136줄 — 위 코드 합계와 별도 |
| JSON | 396 | 55,014줄 — 위 코드 합계와 별도 |
커밋 3961ddb의 파일에서 빈 줄·주석을 포함한 물리적 줄을 센 값입니다.
.git, node_modules, .venv, 이름이 vendor인 디렉토리와
package-lock.json은 제외했습니다. HTML 안의 JavaScript는 HTML에 포함되고,
생성된 갤러리·중복 예제·폰트 데이터가 들어간 CSS도 포함됩니다.
따라서 사람이 작성한 고유 로직의 양이나 유지보수 난도를 그대로 나타내지는 않습니다.
같은 다섯 언어·형식 기준으로 core/는 21개 파일·2,278줄,
styles/는 1,017개 파일·135,497줄입니다.
데모가 전체 코드 줄 수의 약 95%를 차지합니다.
큰 공용 프레임워크 하나를 공부하기보다, 작은 공용 도구와 다양한 개별 구현을 비교하는 학습에 적합합니다.
core/README.md → 도구의 입력과 출력을 정리합니다.core/render/page.mjs → 웹페이지가 준비되는 조건을 찾습니다.styles/swiss-motion/demo/main.js → 전역 함수가 실제 장면 함수에 연결되는 지점을 봅니다.styles/swiss-motion/demo/film.js → renderFilm()과 buildEvents()를 비교합니다.core/render/video.mjs → 프레임 분배·캡처·인코딩·정리 순서로 돌아옵니다.core/lib.js의 seg(t, a, b)는 시각을 0~1 진행률로 바꾸고,
lerp(a, b, t)는 두 값 사이를 계산합니다.
ss() 같은 이징 함수는 진행률의 속도감을 바꿉니다.
다음은 이 도우미로 작성한 학습용 장면 함수입니다.
import { seg, lerp, ss } from '/core/lib.js';
// g는 2D 문맥, w와 h는 출력 화면 크기다.
function draw(g, t, w, h) {
const p = ss(seg(t, 1, 3));
const x = lerp(w * 0.1, w * 0.8, p);
g.setTransform(1, 0, 0, 1, 0, 0);
g.clearRect(0, 0, w, h);
g.fillStyle = '#a8452f';
g.fillRect(x, h * 0.45, 80, 80);
}
// 1초 이전에는 시작점, 3초 이후에는 도착점에 머문다.
// draw(2초)를 여러 번 불러도 위치가 누적되지 않는다.
실습 아이디어: 시각을 2초 → 0초 → 2초 순서로 바꿔 그려 보세요.
두 번의 2초 화면이 같아야 합니다.
x += speed로 위치를 누적하는 방식이 왜 이 조건을 깨는지 설명해 보세요.
mulberry(seed)는 같은 시드에서 같은 난수열을 만듭니다.
하지만 난수 생성기를 한 번만 만들고 매 프레임 소비하면 호출 순서에 따라 결과가 달라집니다.
입자 속성을 준비 단계에 고정하거나, 프레임·개체 식별자로 난수를 재구성해야 원하는 시각을 독립적으로 그릴 수 있습니다.
styles/swiss-motion/demo/film.js의 renderFilm()은
setTransform(1, 0, 0, 1, 0, 0)으로 좌표 변환을 초기화한 뒤 배경을 채웁니다.
장면 안에서는 save(), translate(), restore()로 변환 범위를 묶습니다.
앞선 장면의 확대·회전이 다음 장면에 남지 않게 하는 패턴입니다.
measureNumeral()은 measureText('4').actualBoundingBoxAscent로 숫자의 실제 높이를 측정합니다.
글꼴의 크기 값과 사람이 보는 글자 높이가 같지 않기 때문입니다.
styles/swiss-motion/demo/main.js가 document.fonts.load()를 먼저 기다리는 것도
대체 글꼴로 측정해 배치가 달라지는 문제를 줄이기 위한 순서입니다.
실습 아이디어: 같은 100px 크기로 숫자·한글·영문을 나란히 그리고 실제 경계를 표시하세요. 한글에 필요한 글리프를 가진 폰트를 선택하고, 폰트 준비 전후의 측정값이 어떻게 다른지 비교해 보세요.
styles/swiss-motion/demo/ease.js는 BPM = 120,
BEAT = 60 / BPM, E8 = BEAT / 2, E16 = BEAT / 4를 정의합니다.
따라서 한 박은 0.5초, 8분음표는 0.25초입니다.
snap(t, t1, d)는 도착 시각 t1을 기준으로 움직임이 미리 시작되게 만듭니다.
cubicBezier(.7, 0, .2, 1)은 입력 진행률에 해당하는 곡선 위치를 이분 탐색으로 구합니다.
전체 작품이 같은 곡선을 사용하면 빠르기와 도착감이 통일됩니다.
이는 웹 UI의 애니메이션 시간을 외우는 공부보다, 영상에서 리듬 규칙을 설계하는 공부에 가깝습니다.
실습 아이디어: 2초에 도착하는 도형 세 개를 각각 한 박, 반 박, 4분의 1박 전에 출발시키세요. 도착 시각을 고정한 채 출발 시각만 바꿔도 움직임의 인상이 달라지는지 관찰합니다.
core/three/post.js의 makePost()는
DofScenePass → UnrealBloomPass → 색보정·비네팅 → OutputPass를 연결합니다.
DofScenePass는 깊이 텍스처로 초점에서 얼마나 벗어났는지 계산하고,
선택적으로 GTAOPass의 주변 가림을 적용합니다.
여기서 ssaa: 2는 가로·세로를 각각 두 배로 처리하므로 픽셀 수는 네 배입니다.
또한 심도 셰이더는 perspectiveDepthToViewZ()를 사용합니다.
TECHNIQUE.md는 직교 카메라에 적용하려면 깊이 변환을 바꾸어야 한다고 명시합니다.
실습 아이디어: 먼저 정지한 단순 3D 장면에서 심도·블룸·주변 가림을 하나씩 켠 결과를 비교하세요. 효과를 한꺼번에 켜면 어떤 단계가 화면과 처리량을 바꿨는지 알기 어렵습니다.
core/audio/sfx.py의 SR = 48000은 1초를 48,000개 표본으로 표현한다는 뜻입니다.
thump()는 감쇠하는 저음과 필터링한 잡음을 섞고,
whoosh()는 주파수 대역을 움직입니다.
add()는 음량과 좌우 위치를 적용해 스테레오 배열에 더합니다.
# 저장소 루트의 Python 대화형 환경에서 사용할 학습용 예시
import numpy as np
from core.audio.sfx import SR, thump, add
buf = np.zeros((int(4 * SR), 2), dtype=np.float32)
add(buf, thump(v=0.8), at=1.5, gain=0.4, pan=0.0)
# 충격음은 72,000번째 표본 위치에서 시작한다.
# 저장·재생 전에는 최대 진폭과 클리핑을 확인한다.
core/audio/sampler.py의 render(events)는
시각·악기·음높이·길이·세기·좌우 위치가 담긴 악보를 스테레오 음원으로 만듭니다.
credits(names)는 사용한 샘플의 출처 표기에 필요한 문구를 반환합니다.
공용 도구가 음원뿐 아니라 출처 관리도 돕는다는 점을 볼 수 있습니다.
실습 아이디어: 같은 충격음을 0.5초 간격으로 세 번 배치한 뒤, 좌우 위치와 음량만 다르게 해 보세요. 샘플 라이브러리를 쓰는 실습은 실제 악기 이름을 확인하고 필요한 라이브러리만 준비하는 편이 좋습니다.
core/tts/tts.py는 대사별 WAV와 dur.json을 생성합니다.
기본 음성은 af_bella, 속도는 0.92, 언어는 en-us입니다.
core/tts/asr_check.py는 이 소리를 다시 글로 인식해 원문과 비교하고,
단어 시각을 words.json에 기록합니다.
compare()는 영어를 정규화한 단어열로 비교하고,
zh·ja·ko는 문자 유사도를 비교합니다.
기본 기준 0.92는 모델의 확신도가 아니라 difflib.SequenceMatcher의 유사도입니다.
고유명사나 숫자는 대사의 asr 필드로 비교용 표기를 따로 둘 수 있습니다.
detect_lang()은 한글을 CJK 문자에 포함하지만 반환값은 zh와 en뿐입니다.
한국어 대사는 --lang ko를 명시해야 합니다.
load_model()은 CPU와 int8 계산을 사용하므로 음성 검사 자체에 CUDA가 필수인 구조는 아닙니다.
TECHNIQUE.md의 기본 음성 합성 안내는 영어·중국어 중심입니다.
core/tts/tts.py의 기본 언어는 영어이고, 한국어 글을 영어로 읽으려 하면 입력 검사에서 거부합니다.
한국어 실습은 먼저 사용할 음성 엔진·음성을 정하고, 짧은 문장을 직접 들어 확인해야 합니다.
core/render/readcheck.mjs의 need()는 화면 글자에 필요한 시간을 계산합니다.
필요 시간 = max(1.5,
한중일 문자 수 / 4.5
+ 나머지 비공백 문자 수 / 15
+ 1.5)초
예: 공백을 뺀 한글 9글자만 있으면 9 / 4.5 + 1.5 = 3.5초
검사 간격 기본값: 0.04초, 글자 사각형이 화면 안에 있어야 함
같은 식별자라도 문구가 바뀌면 새 텍스트로 처리하며, 처음 완전히 보이는 연속 구간을 검사합니다.
TEXTS()가 없거나 검사할 텍스트를 한 번도 반환하지 않으면 종료 코드 2입니다.
이것은 통과가 아니라 검사 불가를 의미합니다.
또한 화면 글자를 OCR로 발견하는 도구가 아니므로 페이지가 모든 대상 문구를 정확히 보고해야 합니다.
core/render/mux.sh는 별도로 오디오 손상과 길이를 검사하고,
두 차례의 loudnorm으로 통합 음량 −14 LUFS, 실제 피크 −1.2 dB를 목표로 최종 합성합니다.
짧은 오디오는 무음으로 채우고, 음량 목표를 벗어나면 경고합니다.
자동 검사가 통과해도 발음의 자연스러움·사실 정확성·미적 완성도는 사람이 확인해야 합니다.
좋은 자동 검사는 “아무 오류도 안 났다”와 “검사할 조건을 실제로 만족했다”를 구분합니다. 실습 아이디어: 일부러 짧게 노출한 문구, 화면 밖 문구, 검사 정보가 없는 장면을 나누어 각각 어떤 결과가 나와야 하는지 먼저 예상해 보세요.
| 항목 | 근거가 있는 요구사항 | 초보자가 확인할 점 |
|---|---|---|
| 운영체제 | TECHNIQUE.md: Apple silicon macOS에서 개발, Linux는 동작 예상, Windows는 WSL 안내 | 세 운영체제에서 같은 수준으로 검증되었다는 뜻은 아님 |
| Node.js | 20 이상 | 루트 engines.node와 설치 도구의 검사 조건 |
| Python | 3.11 이상, 음성 구성의 kokoro-onnx는 3.11~3.13 안내 | 설치 도구의 기본 선택은 uv 사용 시 3.12 |
| 미디어 도구 | FFmpeg와 ffprobe | 소스가 사용하는 H.264·AAC 인코더와 필터를 갖춘 빌드 필요 |
| 브라우저 | Playwright용 chromium-headless-shell | 다른 실행 파일은 PLAYWRIGHT_CHROME으로 지정 가능 |
| GPU | 3D·셰이더가 많은 스타일에서 중요 | macOS는 기본 ANGLE Metal. 소프트웨어 WebGL이면 느릴 수 있다는 경고 |
| CPU·RAM | 공식 최소 코어 수·RAM 용량·VRAM 용량 미기재 | 작업자마다 브라우저와 FFmpeg가 생김. 작은 장면과 적은 작업자부터 확인 |
기본 출력은 1920×1080, 24 fps, 작업자 3개입니다.
takeSize()는 가로·세로 각각 16~8192 범위의 짝수만 허용합니다.
허용되는 최대 크기가 실용적으로 처리 가능한 크기를 보장하지는 않습니다.
1080p RGBA 화면 한 장은 단순 계산으로 약 7.9 MiB입니다. 가로·세로를 두 배로 그리면 한 장만 약 31.6 MiB가 됩니다. 실제 3D 처리에는 깊이·색상·중간 결과용 여러 버퍼와 브라우저 메모리가 더 필요합니다. 48 kHz 스테레오 float32 오디오 1분은 한 배열만 약 22 MiB이고, 믹싱은 여러 배열을 함께 유지합니다.
처리량은 영상 길이뿐 아니라 픽셀 수, 후처리 단계 수, 동시에 열린 브라우저 수에 함께 좌우됩니다.
--workers 1은 브라우저 수를 줄이는 방법이고,
--size 960x540은 출력 픽셀 수를 줄이는 방법입니다.
단, 작품 코드가 고정 좌표를 사용하면 크기 옵션만 바꿔도 자동으로 화면 구성이 맞춰지는 것은 아닙니다.
| 구성 | 문서에 적힌 대략적 용량 | 필요한 경우 |
|---|---|---|
| 기본 도구 | 약 350 MB | Node 패키지·헤드리스 브라우저·Python 기본 구성 |
| 음성 구성 | 약 0.55 GB + 인식 모델 | TTS와 음성 검사. 인식 모델은 영어 약 145 MB, 중국어 약 480 MB 안내 |
| 음악 추가 구성 | 약 140 MB | core/audio/pluck.py의 numba 기반 현악기 모델 |
| 악기 샘플 | 라이브러리별 약 70~400 MB | 샘플 악기 사용 시. 압축 해제 때는 팩 크기의 약 두 배 여유 안내 |
| HDRI | 약 12 MB | 일부 3D 스타일의 환경 조명 |
용량 수치는 TECHNIQUE.md의 안내값이며, 패키지 변화나 작품별 원본 소재는 별도입니다.
중간 영상·음원·자료 캐시도 디스크를 사용합니다.
README의 에이전트 제작 시간 30~60분 역시 안내치이며, 하드웨어별 렌더링 벤치마크는 아닙니다.
Kokoro는 모델 준비 후 로컬 음성 합성이 가능하지만,
core/tts/tts_zh.py의 edge-tts는 네트워크를 사용합니다.
패키지·모델·샘플·자료의 첫 다운로드와 코딩 에이전트 이용에도 별도 환경이 필요합니다.
core/tts/tts.py는 espeak-ng 데이터의 실제 경로를 확인할 수 있을 때, UTF-8 기준 160바이트 이상이면 거부합니다.
긴 한글 경로도 영향을 줄 수 있습니다.
저장소에서 확인되는 이런 제약을 무시하고 “설치만 하면 모든 데모가 즉시 재생성된다”고 기대해서는 안 됩니다.
아래 명령은 학습자가 도구와 의존성을 준비한 뒤 수행할 예시입니다.
데모는 완제품 재생성보다 구현 기법을 참고하는 용도로 제공됩니다.
styles/와 core/를 직접 바꾸기보다
별도의 films/learn-01/ 같은 연습 프로젝트에 필요한 코드를 작성하세요.
목표: 6-1절의 draw()를 사용해 사각형이 움직이는 장면을 만드세요.
index.html에 캔버스와 모듈을 연결하고,
DUR = 6, render(t), READY를 공개합니다.
배경도 매 프레임 칠해 JPEG에 투명 영역이 남지 않게 합니다.
완료 기준: 0·1·2·3초 정지 화면이 예상한 위치에 있고, 6초·24 fps 영상의 계산상 프레임 수가 144개임을 설명할 수 있습니다. 정지 화면만 만드는 단계에서는 음성·음악 모델을 준비할 필요가 없습니다.
# 저장소 루트에서, 본인이 만든 연습 프로젝트를 대상으로 실행
node core/render/still.mjs films/learn-01 0 1 2 3
node core/render/video.mjs films/learn-01 --fps 24 --workers 1
목표: 9글자의 한글 제목을 그리고,
TEXTS(t)에 전체 문구와 화면 좌표를 보고하세요.
한글 폰트는 실제 사용할 글자를 포함한 파일을 사용하고, 로드가 끝난 뒤 준비 완료로 표시합니다.
비교: 제목을 2초만 노출하는 경우와 4초 노출하는 경우를 나눕니다. 기본 규칙의 필요 시간은 3.5초이므로 결과가 달라져야 합니다. 제목 일부가 화면 밖으로 나가는 경우도 별도로 만드세요.
완료 기준: 가독성 실패 1과 검사 불가 2를 구분하고,
문구가 처음 보일 때부터 TEXTS()에 전체 문장을 반환하는 이유를 설명합니다.
node core/render/readcheck.mjs films/learn-01
# 이 명령은 화면 글자 검사다. 자막 파일 검사를 대신하지 않는다.
목표: 도형이 1.5초에 착지하도록 하고,
EV에도 같은 시각의 thump 이벤트를 넣습니다.
events.json을 읽는 작은 믹서에서 6-5절의 add()를 사용해 6초 WAV를 만드세요.
완료 기준: 화면과 소리의 시각을 한 상수에서 정하고,
착지 시각을 바꾸면 효과음도 함께 이동합니다.
샘플 위치 1.5 * 48000 = 72000과 프레임 위치 1.5 * 24 = 36의 관계를 설명합니다.
node core/render/events.mjs films/learn-01
# events.json을 읽어 본인이 만든 믹서로 out/mix.wav를 생성한 다음
sh core/render/mux.sh films/learn-01/out/video.mp4 \
films/learn-01/out/mix.wav films/learn-01/film.mp4 24 0
목표: 시드로 고정한 입자 20개를 추가하고 작업자 1개와 3개로 각각 출력합니다. 각 출력은 다른 파일 이름을 사용합니다.
완료 기준: 같은 시각에서 추출한 화면의 입자 위치가 일치합니다. 압축된 MP4 파일 전체의 해시가 같은지보다 장면 내용이 같은지를 확인하세요. 인코딩 구간이 달라지면 압축 결과는 달라질 수 있습니다.
관찰: 시간을 누적하거나 전역 난수를 매번 소비하는 코드를 일부러 넣었을 때 작업자 경계에서 어떤 문제가 생기는지 원인까지 기록합니다.
node core/render/video.mjs films/learn-01 --workers 1 \
--out films/learn-01/out/one.mp4
node core/render/video.mjs films/learn-01 --workers 3 \
--out films/learn-01/out/three.mp4
목표: 직접 측정한 값 세 개나 재사용 조건이 명확한 자료 하나로 짧은 설명 영상을 만드세요.
styles/caption-news/STYLE.md의 정보 배치 규칙을 참고하되,
새로운 도입·설명·결론을 구성합니다.
조건: 주장별 출처 또는 측정 방법을 FACTS.md에,
이미지·폰트·음원의 권리 정보를 CREDITS에 기록합니다.
음성은 사용할 권리가 있는 녹음 또는 확인한 엔진으로 준비하고, 한국어 ASR 검사는 --lang ko를 지정합니다.
완료 기준: 가독성·음성 내용·화면 경계·음량을 각각 검사한 뒤 최종 영상을 소리와 함께 확인합니다. 자동 검사로 확인하지 못한 사실과 미적 판단도 따로 적습니다.
첫 실습의 성공 기준은 화려한 영화 한 편보다, 한 시각의 그림과 소리를 의도대로 재현하고 설명할 수 있는 것입니다.
| 주차 | 학습 내용·읽을 소스 | 만들 결과와 확인 질문 |
|---|---|---|
| 1주차 | JavaScript 모듈, 함수, 배열, JSON. core/lib.js와 styles/swiss-motion/demo/main.js | 한 장면을 그리는 함수. 입력 시각과 출력 화면을 설명할 수 있는가? |
| 2주차 | Canvas 좌표·변환·폰트 측정. styles/swiss-motion/demo/film.js의 renderFilm() | 6초 제목 영상. 화면을 건너뛰어 그려도 같은가? |
| 3주차 | Playwright, 비동기 처리, 프로세스·스트림. core/render/page.mjs, core/render/video.mjs | 정지 화면과 MP4 출력. 버퍼가 차면 누가 기다리는가? |
| 4주차 | 표본화, 파형, 필터, 믹싱. core/audio/sfx.py, styles/swiss-motion/demo/mix.py | 이벤트에 맞는 효과음. 초·프레임·표본을 서로 바꿀 수 있는가? |
| 5주차 | TTS·ASR, 검사 경계, 문자 정규화. core/tts/asr_check.py, core/render/readcheck.mjs, core/render/mux.sh | 실패 사례를 포함한 검사 기록. 검사 불가를 통과로 오해하지 않는가? |
| 6주차 | 스타일 설계와 정적 사이트 생성. STYLE.md, styleboard/build.py, .github/workflows/pages.yml | 자기 스타일의 짧은 작품과 소개 카드. 메타데이터와 실제 파일이 일치하는가? |
각 주차는 일정의 예시이며, JavaScript·Python을 처음 배우면 1~2주차를 더 길게 잡아도 됩니다. Three.js와 셰이더는 2D 파이프라인을 이해한 뒤 별도 갈래로 공부하세요. 깊이 버퍼·색 공간·렌더 타깃을 동시에 배우면 문제의 원인을 분리하기 어렵습니다.
학습 순서는 결과를 더 화려하게 만드는 순서보다, 한 단계씩 원인을 설명할 수 있게 되는 순서가 좋습니다. “그림이 틀렸는가, 캡처가 틀렸는가, 인코딩이 틀렸는가”를 나누어 질문할 수 있으면 이 저장소 밖의 자동화·데이터 처리 작업에도 같은 사고방식을 적용할 수 있습니다.
시각 표현: 보간 → 좌표 변환 → 카메라 → 셰이더·후처리. 제작 자동화: 비동기 함수 → 스트림 → 하위 프로세스 → 잠금·작업 제한. 콘텐츠 품질: 시간표 → 글자 노출 → 음성 대조 → 출처와 최종 검토.
| 용어 | 쉬운 설명 |
|---|---|
| 코딩 에이전트 | 요청을 읽고 코드를 작성하거나 도구를 사용하는 AI 작업자. 여기서는 연출·그림·소리 제작을 돕습니다. |
| 스타일 지시서 | 색·서체·재질·움직임·소리에서 유지할 규칙. 특정 작품의 이야기와 구분합니다. |
| Canvas 2D | 브라우저의 평면 그림판 API. 선·도형·글자·이미지를 코드로 그립니다. |
| WebGL / 셰이더 | 브라우저에서 GPU 계산을 사용하는 그래픽 API / GPU에서 실행하는 작은 계산 프로그램. |
| Three.js | 카메라·조명·재질·모형으로 3D 장면을 만들기 쉽게 해 주는 JavaScript 라이브러리. |
| 헤드리스 브라우저 | 일반 창을 띄우지 않고 자동화 도구가 제어하는 브라우저. 화면 렌더링과 캡처는 수행합니다. |
| 결정성 / 시드 | 같은 입력에서 같은 결과를 내는 성질 / 재현 가능한 난수열의 시작값. 시드만으로 모든 상태 의존성이 없어지지는 않습니다. |
| 보간 / 이징 | 두 값 사이의 값을 계산하기 / 진행률의 변화 속도를 조정하기. |
| 타임라인 / 큐 | 작품 전체 시간표 / 특정 동작·소리가 일어나야 하는 시점. |
| fps / 프레임 | 1초당 화면 수 / 그중 한 장의 화면. 24 fps의 화면 간격은 약 0.0417초입니다. |
| 워커 | 일감을 나누어 처리하는 작업 단위. 이 렌더러에서는 각 작업 단위가 브라우저와 인코더를 사용합니다. |
| 역압 | 받는 쪽의 처리 속도에 맞춰 보내는 쪽이 잠시 기다리는 흐름 제어입니다. |
| 뮤텍스 / 생존 표시 | 공유 자원을 한 번에 하나만 바꾸게 하는 잠금 / 작업이 살아 있음을 주기적으로 갱신하는 표시. |
| TTS / ASR | 글을 음성으로 바꾸기 / 음성을 글로 인식하기. 두 경로를 연결해 발화 내용을 대조합니다. |
| 표본화율 | 소리를 1초에 몇 개의 숫자로 기록하는지 나타내는 값. 48 kHz는 초당 48,000개입니다. |
| 덕킹 / 클리핑 | 대사가 나올 때 배경음을 낮추기 / 신호가 허용 범위를 넘어서 잘리는 왜곡. |
| LUFS / 실제 피크 | 청감에 가깝게 평가하는 음량 단위 / 재생 과정에서 표본 사이까지 고려한 최고 신호 수준. |
| 먹싱 | 영상과 음성을 최종 컨테이너에 합치는 과정. 여기의 core/render/mux.sh는 필터와 재인코딩도 수행합니다. |
| SSAA / HDRI | 더 크게 그렸다가 줄여 매끈하게 만들기 / 넓은 밝기 범위를 담아 환경 조명 등에 사용하는 이미지. |
| 희소 체크아웃 | Git 저장소에서 필요한 경로만 선택해 파일로 펼치는 방식. |
| 정적 사이트 생성 | 메타데이터와 템플릿으로 HTML을 미리 만드는 방식. 갤러리는 Python으로 생성합니다. |
| MIT / CC BY 4.0 | 이 저장소가 코드·문서와 데모 영상·정지 화면에 각각 안내하는 라이선스. 소재별 예외와 출처 표기도 확인합니다. |
소스 링크는 커밋 3961ddb를 가리킵니다.
외부 기술 문서는 시간이 지나면 최신 버전으로 바뀔 수 있으므로,
예제의 동작은 이 글에 적힌 의존성 버전과 함께 비교하세요.
| 항목 | 상태 |
|---|---|
| 선정 | TrendShift Weekly #21 · 2026-10-23 분석 |
| 스타 / 포크 | 5,814 / 653 — 2026-10-23 GitHub 기준 |
| 주 언어 | JavaScript — 2026-10-23 GitHub 기준 |
| 생성일 | 2026-09-26 — 2026-10-23 GitHub 기준 |
| 최신 릴리스 | thanks — 2026-10-12 (2026-10-23 GitHub 기준). package.json의 0.1.0과 별개 |
| 최근 push | 2026-10-22 — 2026-10-23 GitHub 기준 |
| 최근 커밋 | 3961ddb · 2026-10-22 · README의 새 기능 소개를 데스크톱에서 한 줄에 들어가도록 줄인 변경 |
| 스타일 수 | 59종·11개 분류. 별도로 _template 한 개 |
| 코드 규모 | JS 82,976 / Python 37,171 / HTML 11,287 / CSS 8,866 / Shell 2,524줄. 집계 범위는 5-1절 |
| 공용 제작 도구 | core/의 위 다섯 언어·형식 합계 21개 파일·2,278줄 |
| 주요 잠금 버전 | playwright-core 1.63.0 / three 0.170.0 |
| 라이선스 | MIT — 2026-10-23 GitHub 기준. README는 코드·문서에 MIT, 데모 영상·정지 화면에 CC BY 4.0을 안내하며 음성·외부 소재에 예외 명시 |
| CI 범위 | 정적 갤러리 생성·영상 링크 검사·Pages 배포. 루트 통합 테스트 명령은 없고 일부 데모에 개별 검사 코드 존재 |
재현 범위: 그림·음악·렌더링 코드를 읽을 수 있지만,
일부 원본 영상·폰트·모델·음원은 별도 다운로드가 필요합니다.
대표작 OPUSCAR 98은 갤러리에 소개되지만, styleboard/build.py는 해당 영화 소스가 라이브러리에 포함되지 않는다고 밝힙니다.
59종 스타일 데모와 대표작 전체 소스의 공개 범위를 혼동하지 마세요.
권리 범위: README는 Game Show Flat의 macOS 음성, Paper-cut Lightbox의 Edge TTS 내레이션, Broadcast News Package의 Doubao 내레이션을 데모의 CC BY 4.0 적용 대상에서 제외하고 재사용하지 말라고 안내합니다. 코드의 MIT와 영상·음성·폰트·외부 자료의 이용 조건을 각각 확인해야 합니다.
실행 환경: TECHNIQUE.md의 운영체제 설명과 README의 모델 적합성·제작 시간은 안내입니다.
실제 재현 여부와 처리 속도는 하드웨어, 브라우저, 원본 소재와 모델의 준비 상태에 따라 달라집니다.
README의 안내를 독립적인 비교 실험 결과로 받아들이지 마세요.
검사 범위: 글자 노출 시간·음성 대조·음량 검사는 각각 제한된 조건을 확인합니다. 사실의 정확성, 자연스러운 발음, 출처의 적절성, 작품의 완성도까지 자동으로 보장하지 않습니다.