
VLM에 보낼 프레임을 결정론적으로 추출하기 — framesieve 1.0 공개
요약
VLM(Vision Language Model)에 전달할 의미 있는 프레임을 결정론적으로 선별하는 TypeScript 라이브러리 framesieve 1.0이 공개되었습니다. 불필요한 토큰과 비용을 줄이기 위해 축소된 그레이스케일 이미지의 화소 차분을 활용하여 빠르고 가볍게 변화를 감지합니다.
핵심 포인트
- VLM의 토큰 비용과 레이턴시를 줄이기 위한 프레임 선별 라이브러리
- 학습 모델 대신 결정론적인 화소 차분 방식을 사용하여 가볍고 빠름
- 동일 입력에 대해 항상 동일한 결과를 보장하는 결정론적 설계
- Zero-dependency를 지향하며 호출 측에서 시간(elapsedMs)을 관리
서론
화면을 캡처하여 Vision AI(VLM)에 "지금 무엇이 일어나고 있는지"를 읽게 하는 구성은 컴퓨터 조작 에이전트나 화면 공유 어시스턴트, 상주 모니터링 등에서 자주 등장합니다. 다만, 매 프레임을 그대로 보내면 토큰(Token)도 레이턴시(Latency)도 비용도 금방 불어납니다. 정말로 보내고 싶은 것은 "의미 있는 변화가 있었던 프레임뿐"입니다.
하지만 이 "변화"를 판정하는 것은 의외로 어려운 일입니다. 단순히 화소 차분(Pixel difference)을 구하면 마우스 커서의 이동, 재생 중인 동영상, 깜빡이는 커서와 같은 사소한 움직임 때문에 전송이 넘쳐납니다. 그렇다고 임계값(Threshold)을 높이면, 이번에는 보고 싶었던 변화를 놓치게 됩니다. 적절한 수준의 폭이 의외로 좁습니다.
framesieve는 이 "보낼 프레임의 선별"을 **결정론적(Deterministic)**으로 수행하는 TypeScript 라이브러리입니다. 이번에 1.0 버전을 공개했습니다. 본 기사에서는 사용법의 입구부터 내부에서 어떤 처리를 하고 있는지에 대한 설계까지 소개합니다.
npm install framesieve
우선 전체상부터
framesieve가 수행하는 것은 한마디로 말해 "이 프레임을 보낼 것인가, 보내지 않을 것인가"의 판정뿐입니다. 실제로 VLM에 보내는 것은 호출 측의 역할이며, 라이브러리는 프로바이더의 SDK를 전혀 포함하지 않습니다. 판정과 전송을 확실히 분리하고 있다는 점이 특징입니다.
처리 흐름은 다음과 같습니다.
판정은 64x36 정도로 작게 축소한 프레임에 대해 수행하고, 실제로 보내는 것은 풀 해상도(Full resolution)로 사용하는 방식이 정석입니다.
코드로 작성하면 다음과 같습니다.
import { createFrameGate, frameFromImageData } from "framesieve";
const gate = createFrameGate({
diff: { algorithm: "edge", mode: "reference" },
...
여기서 중요한 것이 elapsedMs입니다. framesieve의 코어는 Date.now()도 난수(Random number)도 사용하지 않습니다. 시간은 모두 호출 측이 전달하는 이 "경과 밀리초(Elapsed milliseconds)"로 처리합니다. 즉, 동일한 프레임 열을 입력하면 반드시 동일한 판정 열이 반환됩니다. 이 결정론이 후술할 "교정(Calibration)"을 가능하게 합니다.
왜 "학습 모델"을 사용하지 않는가
"변화 감지라면 CLIP의 임베딩 거리(Embedding distance)나 SSIM을 사용하면 되지 않을까"라고 생각하실지도 모릅니다. framesieve는 의도적으로 축소된 그레이스케일(Grayscale) 이미지의 단순 차분을 주 신호로 삼고 있습니다.
이 판단에는 근거가 있습니다. 에이전트의 스크린 관측에 관한 연구에서는, **화소 차분 기반의 샘플링(Pixel difference-based sampling)**을 CLIP 기반이나 균등 간격 샘플링과 비교해도 다운스트림 태스크(Downstream task)의 정확도는 거의 동일한 수준으로 수렴한다고 보고되었습니다 [1]. 정교한 유사도 지표나 학습 모델을 도입해도 정확도는 거의 변하지 않는다는 뜻입니다. 그렇다면,
- 제로 의존(Zero-dependency)·완전한 결정론이라는 제품의 불변 조건 (SSIM 라이브러리도 학습 모델도 이 두 가지를 깨뜨립니다) -
- 비용(회의 앱 옆에서 2fps 정도로 동작하기 때문에 예산은 "1코어의 몇 %" 수준입니다. 축소 차분이라면 충분한 여유가 있습니다)
을 우선하는 것이 라이브러리로서는 합리적입니다. "똑똑함"보다는 "빠르고, 가볍고, 재현 가능함"을 선택했다고 말할 수도 있습니다. 실제로 이 연구가 채택하고 있는 기본 설정도 "이전 프레임으로부터 1% 미만의 화소만 변화하면 스킵한다"라는 단순한 화소 게이트(Pixel gate)이며, 이는 framesieve의 발상과 일치합니다. 무거운 유사도 지표를 코어(Core)에 추가하는 PR을 기본적으로 거절하는 방침도 이 판단에서 유래합니다.
축 그 첫 번째: 무엇을 "측정"할 것인가 (algorithm)
변화 감지는 독립된 두 가지 선택의 조합으로 결정됩니다. 첫 번째인 diff.algorithm은 "무엇을 측정할 것인가"입니다.
downsample: 거칠게 축소한 명도 차이. 매우 가볍습니다.pixel: 화소별 명도 차이. 미세한 변화까지 잡아냅니다.edge: Sobel 필터로 에지(Edge)를 추출한 후 비교합니다.
주목할 점은 edge입니다. 에지로 비교하면 색상만 변한 변화를 무시할 수 있습니다. 다크 모드 전환이나 텍스트 선택 하이라이트 색상 정도에는 반응하지 않고, "문자나 도형의 형태가 변한" 구조적인 변화만을 잡아냅니다. 문서 편집과 같은 용도에서 효과적입니다.
두 번째 축: 무엇과 "비교"할 것인가 (mode)
두 번째 축인 diff.mode는 framesieve의 특징적인 부분입니다. "무엇과 비교할 것인가"를 선택합니다.
previous: 직전 프레임과 비교reference: 마지막으로 보낸(emit 한) 프레임과 비교
algorithm x mode 조합을 통해 3 x 2의 조합을 자유롭게 선택할 수 있습니다. 여기서 previous에는 무시할 수 없는 약점이 있습니다.
호버(hover) 시 표시되는 툴팁과 같은 일시적인 변화를 생각해 봅시다. 직전 프레임 비교 시에는 다음과 같이 동작합니다.
| 시점 | 화면 상태 | previous |
|---|---|---|
| t0 | 일반 | - |
| t1 | 칩(chip)이 나타남 | emit(변화) |
| t2 | 칩 표시 중 | skip |
| t3 | 칩이 사라짐 | emit(다시 변화) |
(참고: reference 모드 시 t1에서 pending, t2에서 pending, t3에서 skip(기준으로 돌아감))
나타났다 사라지는 것뿐인 변화에 대해 previous는 2번 전송해 버립니다. 반면 reference는 어떨까요?
reference mode의 내부
reference 모드는 각 프레임을 직전 프레임이 아닌 **baseline(마지막으로 보낸 프레임)**과 비교합니다. 그러면 나타났다가 곧바로 baseline으로 돌아가는 일시적인 변화는 "기준과의 차이가 사라지기" 때문에 전송되지 않습니다. 그리고 policy.referencePersistMs로 지정한 시간 이상 그대로 유지된 변화만을 전송합니다.
상태 전이로 나타내면 다음과 같습니다.
중요한 점은, emit 했을 때에만 baseline을 업데이트(commit)한다는 점입니다. 또한, 후술할 keepalive를 통해 보낸 경우에는 commit 하지 않습니다. 이 부분을 모호하게 처리하면 "지속성을 측정하는 도중에 baseline이 움직여 변화를 뭉개버리는" 버그로 이어집니다 (실제로 설계 리뷰에서 한 차례 이 문제를 겪었습니다).
텍스트 입력이나, 열어둔 채로 두는 패널과 같은 "남는 변화"는 보내고, 호버 툴팁과 같은 "사라지는 변화"는 무시합니다. 이러한 동작을 결정론적으로 얻을 수 있습니다.
busy 영역과의 양립: two-mask
여기서 또 다른 기능인 adaptiveMask와 충돌할 가능성이 있습니다. adaptiveMask는 "항상 움직이는 영역(재생 중인 영상, 카메라 영상, 깜빡이는 시계 등)"을 자동으로 감쇠시켜, 그곳으로부터의 연속적인 전송을 억제하는 메커니즘입니다. 하지만 reference 모드는 baseline을 고정하기 때문에, 단순하게 구현하면 "계속 움직이는 영역"을 제대로 검출하거나 감쇠시키지 못합니다.
framesieve는 마스크를 2장 가짐으로써 이 문제를 해결합니다.
- adaptiveMask의 가중치는 **motion(직전 프레임과의 비교)**에 의해 구동
- 게이트의 emit 판정은 **score(baseline과의 차이)**에 의해 구동
이를 통해 정지된 오버레이는 motion이 0이므로 가중치가 유지되어 전송되고, 재생 중인 영상은 motion이 지속되므로 감쇠되어 억제됩니다. 상반된 두 가지 요구사항을 동시에 만족할 수 있습니다.
가중치 계산 자체는 간단하며, 최근 N 프레임(기본값 20) 동안의 각 블록 변화 빈도로부터 구합니다.
weight(b) = 1 - changeFrequency(b, 최근 N 프레임)
score = Σ (변화한 블록 b의 weight(b))
매 프레임 변화하는 영역은 weight가 거의 0에 수렴하여 기여하지 않게 되고, 그 옆의 정적인 영역은 풀(full) 가중치를 유지합니다. 영상이 멈추고 다른 장면으로 바뀌면 weight가 회복되어 다시 검출됩니다. 학습은 전혀 없으며, 단순한 이동 평균입니다.
블록 그리드와 정책(Policy) 계층
조금 더 내부를 살펴보겠습니다. framesieve는 화면을 블록 그리드(예: 16x9)로 분할하고, 블록별로 변화를 채점합니다. 단일한 "전체 변화율"이 아니라 블록 단위로 보기 때문에, "화면의 일부만 변한 것"을 국소적으로 포착할 수 있습니다. 민감도는 minChangedBlocks(얼마나 많은 블록이 변해야 반응할지)나 blockChangeRatio로 조정합니다.
채점의 후단에는 policy 계층이 있습니다. 이곳이 "전송 빈도"를 조절하는 부분입니다.
debounceMs: 변화가 안정된 후 전송 (previous 모드용)referencePersistMs: 지속 판정 윈도우 (reference 모드용)minIntervalMs: 최소 전송 간격 (Rate Limit)maxSilenceMs: 무음이 지속될 경우 정기적으로 전송하는 keepaliveprimeOnFirstFrame: 첫 번째 프레임은 반드시 전송
판정 결과인 decision은 실제로는 4가지 상태를 가집니다.
emit: 전송skip: 변화 없음debounced: 변화는 있으나 아직 지속 중 / settle 대기 중throttled: 전송하고 싶으나minIntervalMs에 의해 억제됨
또한 emit에는 이유(reason) (prime / threshold / keepalive)가 함께 붙습니다. 이 정도의 입도(granularity)로 반환되기 때문에, 로그를 통해 "왜 전송했는지, 왜 전송하지 않았는지"를 추적할 수 있습니다. 또한 출력은 결정론적(deterministic)으로 직렬화(serialize)되므로(점수는 6자리 고정, 키는 정렬됨), timeline.jsonl은 1바이트 단위까지 동일하게 재현할 수 있습니다.
설계의 경계: 의미는 판단하지 않는다
강조하고 싶은 점은, reference 모드는 시간 필터(time filter)일 뿐, 의미를 판단하는 것이 아니라는 점입니다. "그 지속된 변화가 '중요'한가"를 framesieve는 판단하지 않습니다.
겉모습은 완전히 같지만 의미만 다른 두 가지 변화는 어떤 파라미터를 사용하더라도 구분해낼 수 없습니다. 시간 필터에는 "2초간 지속된 불필요한 변화"와 "2초간 지속된 중요한 변화"를 구별할 방법이 없기 때문입니다. 그렇다면 무엇을 위해 존재하는가? framesieve의 역할은 전송량을 확실히 줄이면서도, 필요한 프레임을 놓치지 않는 것이며, 중요도에 대한 최종 판단은 후단에 있는 VLM에 위임한다는 역할 분담입니다. 이 경계를 모호하게 만들지 않는 것이 설계 방침입니다 (PII 마스크용 @framesieve/redact를, 텍스트 마스킹이 본질적으로 best-effort인 이상 일부러 pre-1.0 단계에 머물게 한 것도 같은 이유입니다).
결정론의 이점: record -> replay -> sweep
마지막으로 결정론의 가장 큰 이점을 소개합니다. referencePersistMs의 최적값은 대상 화면의 성격에 따라 달라집니다. 이를 추측에 의존하지 않고, **실제 화면 녹화본에 대해 기계적으로 교정(calibration)**할 수 있습니다.
- 실제 사용 사례를 녹화한다 (프레임 열과 판정 결과를 저장)
- 해당 녹화본에 대해 파라미터를 바꿔가며 리플레이(replay)하여, 전송 수의 "무릎(knee, 최적 지점)"을 찾는다
CLI에서는 다음과 같이 실행합니다.
npx @framesieve/cli replay ./rec --mode reference \--sweep referencePersistMs=0:4000:1000
referencePersistMs emits
0 11
1000 10
...
한 번 녹화해 두면, 재캡처 없이 무수히 많은 설정을 오프라인에서 비교할 수 있습니다. 동일한 입력에 대해 동일한 출력이 보장되기 때문에 가능한 교정 흐름입니다.
마치며
- 매 프레임을 보내지 않고, "보내야 할 변화"만을 결정론적으로 선택합니다.
- **무엇을 측정할 것인가(algorithm) x 무엇과 비교할 것인가(mode)**의 2축으로 동작을 구성합니다.
reference모드는 일시적인 변화를 걸러내는 시간 필터입니다. Busy 영역은 two-mask로 양립합니다.- 의미는 판단하지 않습니다 (그 역할은 VLM의 몫입니다).
- 결정론적이기 때문에 record -> replay -> sweep으로 교정할 수 있습니다.
1.0 버전에서는 코어인 framesieve, 캡처 소스와 레코더를 제공하는 @framesieve/adapters, 그리고 @framesieve/cli를 모두 포함하여 공개했습니다. 공개 API는 semver를 따라 안정화되었으며(파괴적 변경은 메이저 업데이트 시 발생), ESM / CJS를 모두 지원하고 SLSA provenance를 포함합니다. 편하게 사용해 보세요. 피드백이나 Issue도 환영합니다.
Bojie Li, Noah Shi. "Agent-Computer Observation Interfaces Enable Dynamic Computer Use." arXiv:2606.29472 (2026). 화면 관측의 키프레임 선택(Keyframe selection)에 대해, 균등 간격(1/3 FPS)·화소 차분(Pixel difference)·랜덤·CLIP의 각 수법을 비교하였으며, 다운스트림 태스크(Downstream task)의 정확도가 거의 동일한 수준으로 수렴한다고 보고하고 있습니다(3.2절·6.2절). 기본값은 "이전 프레임으로부터 1% 미만의 화소만 변화하면 스킵한다"는 단순한 화소 게이트(Pixel gate)입니다. ↩︎
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기