오늘 아침 Hacker News에서 흥미로운 글을 발견했다. 10B급 성능을 갖춘 경량 이미지 인페인팅(image inpainting) 프레임워크, Moebius 0.2B를 소개하는 글이었다. 인페인팅이란 이미지에서 지우고 싶은 영역을 지정하면 모델이 해당 부분을 자연스럽게 채워주는 기술이다. 공개된 모델은 PyTorch와 NVIDIA CUDA 환경을 요구했지만, 파라미터 수가 0.2B에 불과하다는 점에 착안해 WebGPU를 활용, 브라우저에서 직접 구동하는 데 도전해봤다.
오늘 아침 Hacker News에서 Moebius: 10B급 성능을 갖춘 0.2B 경량 이미지 인페인팅 프레임워크를 소개하는 글을 발견했다. 작지만 강력한 인페인팅 모델로, 이미지에서 지우고 싶은 영역을 지정하면 그 자리를 자연스럽게 채워주는 기술이다. 공개된 모델은 PyTorch와 NVIDIA CUDA를 요구했지만, 파라미터 수가 0.2B에 불과하다는 점에 착안해 WebGPU를 활용, 브라우저에서 직접 구동하는 데 도전해봤다. 결론부터 말하자면 성공했다. simonw.github.io/moebius-web/에서 직접 체험해볼 수 있다. 자세한 과정은 아래에서 이어간다.
완성된 도구의 데모 영상이다:
원하는 이미지를 불러오고(정사각형이 아닌 이미지는 레터박스 처리됨), 지울 영역을 드래그로 선택한 뒤 "Run inpaint" 버튼을 클릭하면 모델이 알아서 처리해준다.
오늘 내 메인 작업은 Datasette에 테이블 생성 및 수정을 위한 UI를 추가하는 것이었다. 지난주에 배포한 행 삽입 및 편집 기능의 후속 작업이다.
Codex Desktop으로 해당 작업을 진행하고 있었는데(관련 PR), 중간 규모의 리팩터나 UI 마무리 작업이 끝날 때까지 기다리며 5~10분씩 손가락을 빙빙 돌리는 상황이 자주 반복됐다.
(코딩 에이전트의 재미있는 점 중 하나는, 문제가 어려울수록 더 오래 기다려야 하니 그만큼 딴짓할 시간도 늘어난다는 것이다!)
그래서 터미널 창에 Claude Code를 띄우고, Moebius를 웹으로 포팅하는 작업을 얼마나 진행할 수 있을지 시도해보기로 했다.
첫 번째 단계로, 일반 Claude에게 이 프로젝트의 실현 가능성을 물어봤다. GitHub 저장소를 클론할 수 있는 Claude.ai를 활용했다:
Clone https://github.com/hustvl/Moebius/ and tell me if they published the code and weights to run this model anywhere
(당시에는 모델 가중치 링크를 아직 발견하지 못한 상태였다. "News" 섹션 안에 숨어 있었다.)
이어서:
For Moebius what are the options for running it right now - Python and NVIDIA CUDA only or other options too?
그리고:
Muse on the feasibility of porting it to Transformers.js or similar and running it in a browser
나는 모델에게 "X에 대해 muse해봐"라고 자주 말하는데, 구체적인 목표 없이 어떤 문제를 깊이 생각해보도록 유도할 때 내가 찾은 가장 간결한 표현이다.
해당 대화 기록은 여기서 확인할 수 있다. 마지막 답변을 복사해 research.md로 저장해뒀으며, 이후 Claude Code가 참고할 수 있도록 했다.
Claude는 내가 제안했던 Transformers.js 라이브러리보다 한 단계 아래에 위치하는 ONNX Runtime Web의 WebGPU 백엔드 사용을 권장했다.
이 정도면 Claude Code를 풀어놓고 얼마나 멀리 갈 수 있는지 시도해볼 만하다는 확신이 생겼다.
이런 프로젝트를 시작할 때 나는 보통 코딩 에이전트가 필요로 할 만한 정보를 최대한 미리 모아두는 편이다. 어차피 성공을 기대하지 않았기에 /tmp 폴더에서 모든 작업을 진행했다:
cd /tmp
mkdir Moebius
cd Moebius
# Grab the Moebius python code
git clone https://github.com/hustvl/Moebius
# And the model weights (Claude figured this out):
GIT_LFS_SKIP_SMUDGE=0 git clone \
https://huggingface.co/hustvl/Moebius Moebius-weights
# Finally a couple of libraries we might use:
git clone https://github.com/huggingface/transformers.js
git clone https://github.com/microsoft/onnxruntime프로젝트용 디렉터리를 만들고 그 안에서 git init를 실행해 Claude가 코드와 메모를 커밋할 수 있는 환경을 구성했다:
mkdir /tmp/Moebius/moebius-web
cd /tmp/Moebius/moebius-web
git init
# Copy in that research.md from earlier
git add research.md
git commit -m "Initial research by Claude Opus 4.8"미리 준비한 자료들이 들어있는 /tmp/Moebius 폴더의 상위 디렉터리에서 claude 인스턴스를 실행했다. 프롬프트는 다음과 같았다:
Read ./moebius-web/research.md - your goal is to port this model to ONNX and WebGPU so we can run it directly in a browser, with a simple UI
작업이 시작되자 다음 프롬프트를 추가로 입력했다(오타 포함):
Bulid this in /tmp/Moebius/moebius-web and commit early and often, also maintain a notes.md file in there with notes about what you figure out along the way - also start by writing out a plan.md in there and update that plan as oy work too
에이전트에게 이런 식으로 메모를 남기게 하는 것은 내가 자주 쓰는 방식이다. 나 자신을 위해서도, 같은 프로젝트를 이어받을 다음 에이전트 세션을 위해서도 유용하다. 프로젝트가 끝날 무렵 notes.md 파일이 어떤 모습이었는지 링크에서 확인해볼 수 있다.
작업을 시작시켜놓고 메인 프로젝트로 돌아가 중간중간 Claude의 진행 상황을 확인했다. 어느 정도 동작하는 것 같아 보이자 이렇게 프롬프트를 입력했다:
Tell me what URL I can visit in my own browser to try this
이후 Chrome에서 직접 테스트해보고, 발생한 오류와 오류 스크린샷을 Claude Code에 붙여넣었다.
이런 과정을 몇 차례 반복하니 실제로 동작하는 결과물이 나왔다! 이제 인터넷에 올려 다른 사람들도 쓸 수 있게 할 차례였다.
How would we publish this to Hugging Face such that the model weights were on there and the HTML demo would show up in Hugging Face spaces?
Claude Code는 hf CLI 도구 사용법을 알고 있어서, Hugging Face에 모델 저장소를 만들고 해당 저장소에 쓰기 권한을 가진 토큰을 발급한 뒤 /tmp/Moebius/token.txt 파일에 넣어두면 Claude가 바로 활용할 수 있었다.
변환된 ONNX 가중치 1.24GB가 huggingface.co/simonw/Moebius-ONNX에 업로드됐다.
이전에 다른 데모들이 Hugging Face에서 브라우저로 가중치를 로드하는 것을 본 적이 있어서 충분히 가능하다는 걸 알고 있었다. 프론트엔드 코드는 GitHub Pages에 호스팅하기로 하고 이렇게 입력했다:
I want to publish the moebius-web folder to GitHub, minus the large files (so maybe minus the models/ folder), such that when I turn on GitHub Pages for that repo navigating to https://simonw.github.io/moebius-web/ serves the UI
최종 URL을 미리 알려준 것은, 프로덕션 배포 시 실제로 동작하도록 데모 내 URL을 수정해야 할 경우를 대비한 것이었다.
메인 프로젝트 작업 사이사이에 몇 차례 더 반복하고 나서, 마침내 실제로 동작하는 배포 버전이 완성됐다!
그런데... 페이지를 새로고침할 때마다 약 1.3GB의 모델 가중치를 다시 다운로드하는 것 같았다. 브라우저 캐싱이 절실히 필요한 상황이었다!
anything clever we can do with serviceworkers or similar to help cache this stuff? It seems to reload every time, I am concerned that there might be something weird about the way HF redirects work that mean we don't benefit from browser caching
Transformers.js 프로젝트들은 이 문제를 잘 처리하고 있다는 걸 알고 있어서, Whisper Web 데모를 복사해 /tmp/Moebius/whisper-web에 넣어두고 이렇게 입력했다:
look in /tmp/Moebius/whisper-web (with a subagent) and see how they do this
해당 프로젝트의 JavaScript 파일이 난독화된 빌드 결과물이었기 때문에, 서브에이전트를 활용하면 최상위 토큰 컨텍스트를 소모하지 않고 파일을 분석할 수 있을 것이라 판단했다.
Claude는 해당 프로젝트가 caches.open("transformers-cache"), 즉 CacheStorage API를 사용하고 있음을 파악하고 우리 프로젝트에도 이를 적용했다.
이 프로젝트의 전체 Claude Code 대화 기록도 공개해뒀다(내가 만든 claude-code-transcripts 도구로 게시했다).
이번 작업은 전형적인 바이브 코딩(vibe coding)이었다. 코드는 한 줄도 직접 들여다보지 않았고, 테스트와 소소한 기능 개선 제안(예: 대용량 파일 다운로드 시 프로그레스 바 추가), 그리고 원하는 동작 방식의 예시를 가리켜주는 것이 내 역할의 전부였다.
직접 코드를 작성하지 않았기 때문에 WebGPU, ONNX, Moebius 모델 자체에 대해 깊이 이해하게 된 것은 없었다.
이런 프로젝트에서 늘 그렇듯, 가장 중요한 수확은 무엇이 가능한가를 확인한 것이었다:
내가 만든 프로젝트에 대해 좀 더 직접 파악해볼 필요가 있겠다 싶었다. Claude.ai를 열고 이렇게 입력했다:
Clone https://github.com/simonw/moebius-web/ and use it to teach me all about the model and ONNX and the process of converting a model to ONNX and WebGPU and basically everything I'd need to know in order to fully understand this repo
대화 기록과 Claude가 생성한 understanding.md 파일은 여기서 확인할 수 있으며, 현재 GitHub 저장소에도 추가해두었다. 특히 ONNX에 대한 설명이 매우 유익했다:
ONNX(Open Neural Network Exchange)는 신경망을 위한 이식 가능하고 프레임워크에 독립적인 파일 포맷이다.
.onnx파일은 본질적으로 두 가지 요소로 구성된다:
- 연산 그래프(computation graph) — 노드들로 이루어진 유향 그래프로, 각 노드는 하나의 연산자(operator)(
Conv,MatMul,Add,Einsum,Softmax,Gather,Resize, …)이며, 노드 사이를 흐르는 명명된 텐서로 연결된다. 순전파(forward pass)의 "레시피"라 할 수 있다.- 가중치(weights) — 학습된 파라미터 텐서(컨볼루션 커널, 임베딩 테이블 등)로, 동일한 그래프 내에 초기값(initializer)으로 저장된다.
핵심은 ONNX가 무엇을 계산할지를 추상적으로 기술할 뿐, 어떻게 또는 어떤 하드웨어에서 실행할지는 명시하지 않는다는 점이다. 연산자 집합은 opset 번호로 버전 관리되며(이 저장소는 opset 18을 사용), 어떤 연산자가 존재하고 그 의미가 무엇인지를 정확히 고정한다.
PyTorch에는 ONNX로 내보내기 위한 기능이 내장되어 있는데, export_onnx.py에서 그 예시를 확인할 수 있다:
torch.onnx.export( dec, (lat,), dec_path, opset_version=args.opset, input_names=["latent"], output_names=["image"], dynamic_axes={"latent": {0: "B"}, "image": {0: "B"}}, )
또한 유용한 용어 사전과 모델 파이프라인 구조를 보여주는 ASCII 아트 다이어그램(약간의 오류는 있지만)도 포함되어 있었다.
이 글은 내 블로그의 장문 아티클만 모아 보여드리고 있습니다. 모든 포스트를 받아보려면 /atom/everything/을 구독하거나, 다른 구독 옵션을 살펴보세요.