TreeMap
A free, open-source disk-space visualizer. Squarified treemap, duplicate hunting, disk-full forecasting, and cleanup that only ever moves files to the Trash.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Goals, proved by camera. Plain-English goals compile into skills over one shared embedding; state machines turn thousands of scores an hour into one event. Nebius x NVIDIA hackathon.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗You say you'll do something. The cameras confirm you did.
Type a goal in plain English. Nemotron compiles it into skills — 1152 floats each, running on one shared embedding. A state machine turns thousands of scores an hour into one event. Every question you answer becomes a label, and the skills sharpen. Two models do all the perception; four of the seven stages use no model at all.
Built for the Nebius × NVIDIA hackathon. Deployed at pact.lanvar.ai.
For judging — MIT license · Setup, from a clean clone ·
the three NVIDIA models and where each runs ·
what Token Factory did for the build ·
Nebius AI Cloud ·
Nebius Serverless Endpoints ·
Tavily. Every model id below is
the string the code actually sends — src/goals/planner.ts,
src/goals/engine.ts and perception/pact_perception/teacher.py.
cameras ──▶ one frame every 2 s
│
▼
L0 MOTION GATE empty room? stop here. no model · free
│ runs in the BROWSER, so an
│ empty room sends no bytes
▼
EMBED frame → 1152 numbers nvidia/C-RADIOv3-B
│ Nebius · 98M params
▼
SKILLS N dot products on that one no model · 4.6 KB each
│ embedding. 50 skills ≈ µs
▼
STATE MACHINES enter · dwell · exit · cooldown no model · ~90 lines
│ 7,200 scores/hour → 1 event
▼
GOAL RULES did the sub-goals fire, in no model · SQL
│ the right window?
▼
ESCALATE 4 frames + a longer prompt, Nemotron-3.5-Super-VL
│ only when stuck. ~0.1% $0.0006 per call
▼
ASK YOU one question, pre-filled you
the answer becomes a label ──┐
│
the probe refits ◀──────────────────────────┘
The models never change. C-RADIO is frozen and Nemotron is frozen. What improves is the 1152-float vector sitting on top — which is why "it gets better" is a thing we built rather than a thing we hope for.
Measured, not estimated.
| 0.5 fps (every 2 s) | 8 fps (rep counting) | |
|---|---|---|
| Every frame to the VLM, per camera | $26/day | $421/day |
| C-RADIO on one L40S, flat | $37/day | $37/day |
| Teacher on 0.1% of frames | $0.02/day | $0.28/day |
Read the first column honestly: at one camera sampling every two seconds, self-hosting the encoder loses. Break-even is 1.4 cameras. If that were the whole story this architecture would be theatre.
Two things make it real. Frame rate: the moment you want to count reps you need 8 fps, and the naive path goes to $12,600 per camera per month while the GPU line does not move. And headroom: one L40S embeds roughly 150 frames a second, so that same $37 carries about 300 cameras at 0.5 fps.
The claim is about the shape of the cost curve. Naive is linear in frames × cameras × goals. This is flat in all three until the GPU saturates, because a skill is 1152 multiply-adds against an embedding already paid for. Adding goal fifty costs nothing measurable.
And for the demo it costs less still. Measured end to end on a laptop CPU, C-RADIO runs a frame in 1164 ms at 256 px - about 0.9 fps, against the 0.5 Hz a gate samples at. So gates and actions need no GPU at all. Rep counting at 8 Hz does, which is the honest limit of the cheap path.
(The backbone alone is 217 ms at that resolution. The difference is the adaptor head and a loaded host, and the end-to-end number is the one worth planning against.)
| Status | |
|---|---|
| Browser camera / screen → wall | Working |
| Screen share judged against a goal | Working, see below |
| QR / one-time code pairing | Working |
| Live device registry over WebSocket | Working |
| RTSP / RTMP / MJPEG / ESP32 ingest | Working, via the bridge |
| Motion gate in the browser | Working |
| Frame relay → perception | Working |
| C-RADIO embedding, zero-shot skills from text | Working, semantically verified |
| State machines, near-miss diagnostics | Working, 159 tests |
| Goal ledger, instances, settlement | Working |
| Planner: a sentence → skills + a goal rule | Working, reuses existing skills |
| Teacher: escalation to Nemotron VL | Working |
| Probe refitting with before/after holdout | Working |
| Questions with pre-filled answers | Working |
Not built: no auth beyond claim codes, no recording, no cloud transcoding, audio is not published. See the bottom of this file.
Three processes, and the boundary between two of them is the whole point.
┌─ phone / laptop camera ─┐
│ screen share │
└───────────┬─────────────┘
│ WebRTC (media) ┌──────── the wall, in any browser
▼ │
Cloudflare Realtime SFU ───────────┘
▲
│ SDP only, never media
┌───────────┴──────────────────────────────────────────────┐
│ Worker (src/) pact.lanvar.ai │
│ /api/sfu/* proxies SDP, holds the app secret │
│ /api/hub/* Hub DO: what is live right now │
│ /api/engine/* Engine DO: goals, skills, the ledger │
│ /api/frames/* a sampled JPEG, on its way to B │
└───────────┬──────────────────────────────────────────────┘
│ ONE WebSocket. frames down, transitions up.
│ Perception dials out: no inbound port, no TLS, no CORS.
▼
┌──────────────────────────────────────────────────────────┐
│ perception/ (Python) laptop, or a cheap VM │
│ sample → gate → embed → score → state machine │
│ escalate → Nemotron VL → label → refit │
└──────────────────────────────────────────────────────────┘
Perception computes several scores a second per skill. The control plane never sees one. Only state transitions cross the wire — that is the "7,200 scores an hour becomes one event" property expressed as a process split rather than a convention. If a score could be posted, eventually it would be, and the ledger would drown.
So the design's observations table does not live in the Engine. Scores and
the embeddings a label is fitted against stay in perception's own SQLite,
next to the frames. The schema is one logical model split across two stores,
and the Engine keeps the half that is cheap to hold forever.
It holds one WebSocket to the Engine, exactly as bridge/ does to
the Hub. No inbound port, no public address, no TLS to terminate, no CORS. The
identical process runs on a laptop behind a router and on a cloud VM with
nothing changed but --hub. It also sidesteps decoding WebRTC in Python,
which was the riskiest part of the original plan.
The Hub stores nothing about devices: "what is live" is derived from the set of open publisher sockets, so socket-open and device-live are the same fact recorded once. Nothing to expire, nothing to reconcile.
The Engine is the opposite — its whole job is the part that must outlive every socket. Folding them together would cost the Hub the property that makes it simple, so they stay apart.
A publisher can share a screen instead of a camera, and a goal can be about what is on it: "study Python for an hour" rather than "brush my teeth". The plumbing is identical — same tap, same relay, same state machines — but five of the assumptions underneath it are about rooms, and each one fails quietly rather than loudly on a screen.
Every skill declares a surface. camera, screen, or any, and a skill
only ever scores frames from its own. The reason is not that a screen skill
scored against a bathroom camera returns a wrong answer — it returns a low
one, which looks like the thing simply not happening. It is that scoring also
calibrates: bias and temperature are fitted from whatever frames a skill
has seen, so mixing surfaces means a sink detector learns its idea of an
ordinary frame partly from screenshots of a code editor, and drifts in a way
nothing reports. any is the default, so every skill written before this
existed behaves exactly as it did.
A still screen is not an empty room. The motion gate rests on an assumption that is true of a room and false of a screen: that stillness means nothing is happening. Reading a document for twenty minutes is motionless and is exactly the behaviour a goal wants to credit, so gating it would make the system go quiet during the thing it is watching. A screen is therefore never hard-gated. It falls back to a five-second heartbeat in the publisher, which is a slowdown rather than a blindfold — roughly a fifth the cost of an active screen instead of zero cost and zero information.
The cheap layer cannot read, and the split is the design rather than a limitation. C-RADIO recognises the look of a screen — an editor, a spreadsheet, a video player, a terminal — and cannot tell one file from another. So the gate names a recognisable kind of screen and the child names the specific thing, and when the child is undecided the teacher, which can read, settles it. Its verdict becomes a label like any other, so the question gets cheaper the more often it is asked. The planner is told this explicitly, because a screen gate written as "VS Code with main.py open" is a detector that can never fire.
On a screen the content is the noise, so a skill chooses which picture it
sees. Every skill also declares a view: full for the frame as it
arrived, layout for the frame averaged down to a coarse grid and back,
which destroys the detail and leaves the structure. The reason is that for a
gate the chrome is the entire signal. "Is this a short-form video player" is
answered by a bright upright panel on a dark ground with a column of round
buttons down one side; the clip inside it changes every few seconds, and
because the encoder averages the whole frame, that clip is what the score
actually follows. Measured on the synthetic player in
tests/test_layout_only.py, in pixel
levels: two such frames with different clips in them sit 21.7 apart raw and
0.73 apart under layout — a 29.8× collapse — while the panel still stands
110.7 levels above the ground beside it. A signal that
swings that far is one no fixed threshold can sit under, which is the failure
layout removes, and the privacy of it is free: at that grid a page of text
is a texture, so the embedding records that you were reading and not what.
So the planner marks a screen gate layout and what sits beneath it full —
the gate names the kind of screen, which is a shape, and the child names the
state that counts, which is usually in the content. A camera skill is always
full: averaging a room away removes the person the question was about. The
cost is bounded by grouping rather than by restraint: perception encodes a
frame once per distinct view wanted on it, so the usual case where every
eligible skill agrees is the single encode it always was, and a second is paid
only when two skills on one surface genuinely ask different questions of the
same pixels. Both vectors are then stored under the view that produced them,
because a cosine measured on a blurred frame is not comparable to one measured
on a sharp frame, and a store that mixed them would corrupt every bias and
temperature fitted from it.
Escalation needs legible frames. 640px in the browser and 512px at the teacher are right for a camera and quietly wrong for a screen: at that size a 1920px desktop's body text is a grey smear, so the one component that could tell Python from JavaScript answers UNSURE forever — and that reads as a model that cannot do the task rather than as a picture that never held the answer. Screens upload at 1280px and reach the teacher at 1024px. A teacher call on a screen costs roughly four times the input tokens of one on a camera, which is affordable only because escalation is rare.
Screen actions also get different timings, for a reason the camera defaults get backwards. A camera action has a 60-second cooldown so one trip to the bathroom is not recorded as three. An accumulating screen goal sums sessions rather than counting them, so a cooldown charges a minute of lost credit for every glance at a message, and a choppy hour scores as half of one. Screen actions get no cooldown and a 20-second exit, long enough to ride out a switched tab.
L0 is the only level in the design that skips real work. Every skill below it is a dot product against an embedding already paid for, so skipping skills saves microseconds while skipping the frame saves all of it. Running that test in the publisher means an empty room does not merely avoid inference, it never sends the bytes. Nothing is cheaper than a request that was not made.
The gated percentage is shown on the publisher page, because it is the number that tells you whether a device is costing anything.
Four things, each confirmed against the released checkpoint rather than the model card.
| Assumption | Reality |
|---|---|
The adaptor is siglip2-g | It is siglip2. This checkpoint's teachers are clip, siglip2, dino_v2, sam. siglip2-g belongs to the H and v4 variants; asking for it raises. |
| Embeddings are 768-d | They are 1152. 768 is the backbone's per-token width; the summary is 2304 and the adaptor projects to 1152, which is also what the text tower emits. |
AutoModel can serve it | It cannot do text at all. That repo ships empty adaptor_configs and no adaptor tensors. The adaptor weights exist only inside the .pth.tar reached through torch.hub. |
| One model does image and text | The adaptor wraps google/siglip2-so400m-patch16-naflex and keeps its text tower — a separate 3.5 GB download. |
This one matters most. sigmoid(w·e) with no scale or offset puts every
score between 0.475 and 0.599, so an enter threshold of 0.75 is not badly
tuned — it is unreachable, and the system would report nothing forever while
looking perfectly healthy. Borrowing SigLIP's own learned scale fails the
other way: the 0.45-to-0.75 band collapses into 0.013 of cosine, narrower
than frame-to-frame noise, so the hysteresis debounces nothing.
So every skill carries its own two numbers, fitted from its own room:
score = sigmoid((w · e - bias) / temperature)
bias sits at a high quantile of what the room looks like; temperature is
set so the hysteresis band spans a stated number of standard deviations of
that background. Needs no labels. A skill does not score until it is
calibrated — before that every score is ~0.5 regardless of the frame, so
any threshold fires immediately or never.
Three NVIDIA open models, each doing the job it is actually good at.
| Model | Where | Called | |
|---|---|---|---|
| The 24/7 encoder | nvidia/C-RADIOv3-B + siglip2 | Nebius AI Cloud (or a laptop) | every sampled frame |
| The planner | nvidia/nemotron-3-super-120b-a12b | Nebius Token Factory | once per goal written |
| The assistant | nvidia/nemotron-3-super-120b-a12b | Nebius Token Factory | once per thing said to it |
| The teacher | nvidia/Nemotron-3.5-Super-VL | Nebius Token Factory | only when a moment is ambiguous |
C-RADIO is the vision encoder inside Nemotron 3.5 Super VL. That is the whole reason this works rather than a coincidence worth mentioning: the edge index and the cloud teacher share one visual representation, so the teacher's verdict is a label in the same space the skill's own vector lives in. A verdict from a model with a different encoder would be an opinion the skill could not learn from.
The encoder runs on everything and the reasoning models run on almost nothing. A camera at 8 Hz is 28,800 frames an hour through C-RADIO; the teacher sees four of them, and only when the state machine genuinely cannot decide. Token Factory is what makes that affordable - a 120B model priced per token is free at a tenth of a percent of frames and would be unthinkable at all of them.
It also made the label loop possible at all. Nemotron 3.5 Super VL can look at the frames a person just complained about and write candidate labels for them, which closes a gap nothing else could: an embedding cannot be turned back into words, so when a skill stops matching there is no way to ask the vector what it should have said. Something that can see has to write it, and then the recorded embeddings choose between the candidates by separation over spread. Generating and choosing are different jobs and neither model can do the other one.
The runtime argument above is about cost. This one is about the week.
One base URL and one key, from two languages. The Worker reaches Token
Factory in TypeScript (src/goals/nebius.ts) and perception reaches it in
Python (perception/pact_perception/teacher.py), both at
https://api.tokenfactory.nebius.com/v1, both with NEBIUS_API_KEY. Because
it is OpenAI-compatible, the entire client is a base URL, a bearer token and
one method — no SDK to adopt on either side, and the Python half was written
against the same mental model as the TypeScript half.
Three roles, two models, swapped by editing a string. PLANNER_MODEL in
planner.ts, ASSISTANT_MODEL in engine.ts, and the teacher's default in
teacher.py. That is the whole reason the planner and the teacher ended up on
different models: trying a 120B text model for compiling a sentence and a
multimodal one for adjudicating frames cost a one-line change each, not two
deployments of two serving stacks. Moving the teacher from text-only to
Nemotron-3.5-Super-VL — which is what made the label-writing loop possible
at all — was the same one line.
No serving stack for anything we did not have to host. The only GPU this project provisions is the one running C-RADIO, and that machine stops itself when nothing is publishing. A 120B planner and a multimodal teacher needed no capacity planning, no cold-start engineering and no idle spend.
And the boundary, stated honestly, because finding it took a day. Token
Factory cannot host a custom model: its dedicated-endpoint API takes a
model_name from a fixed catalogue, and custom weights is a gated beta
limited to fine-tunes of text LLMs. A 98M vision encoder needing
trust_remote_code is not reachable that way. That is exactly why the encoder
goes to AI Cloud or a Serverless Endpoint and the reasoning models stay on
Token Factory — the split below is a measured conclusion, not a preference.
The encoder is the only part of this that can be moved. Everything else in perception is stateful - a WebRTC subscription, per-room calibration, state machines with dwell timers - and belongs beside the cameras. The encoder is a pure function: a frame in, 1152 numbers out, nothing remembered between calls.
So it runs in two places, and both are real:
| where | how | |
|---|---|---|
| In the perception process | a Nebius Compute VM | --embed local, the default |
| As its own service | a Nebius Serverless Endpoint | --embed-url https://... |
The same image serves both. ENTRYPOINT is python -m and the module is the
command, so pact_perception.run is the watcher and pact_perception.serve
is the encoder behind HTTP - which is also what a serverless endpoint needs,
since those let you set the arguments and not the entrypoint.
The endpoint is live and answers with what it is, because a vector is only meaningful against a weight vector from the same encoder and being wrong there is silent - the numbers look fine and mean nothing:
GET /health
{"ok":true,"encoderId":"c-radio_v3-b/siglip2@384","dim":1152,"device":"cpu","threads":16}
What the split costs, measured. Inside one region the hop is a couple of
milliseconds; the rest is JPEG encode and serialising 1152 floats back. About
twelve milliseconds a frame on top of the encode. Across regions it is ninety,
which is the version not to build. The demo runs --embed local because
twelve milliseconds times eight frames a second is still twelve milliseconds
nobody needs - but the endpoint exists, works, and is the honest answer to
"could the encoder be serverless".
The endpoint does not scale to zero, so the control plane does it. Nebius bills an endpoint for wall-clock time while it runs, whatever the traffic, and nothing there stops it on its own. The Hub already knows when a camera arrives, because a publisher socket opening is a camera arriving: the Engine starts the machine on the first publisher and stops it ten minutes after the last one leaves. Ten minutes because a tab refresh is not an empty house and a cold start is a minute of somebody watching a blank console.
That same loop is what makes preemptible usable. A preempted endpoint goes to
STOPPED and Nebius does not bring it back - it stays down until something
starts it. Something does: the next camera to publish.
Two traps, both of which cost us an evening.
A stop/start through the Compute API does not preserve local disk, and the
store is where every embedding and label the skills have learned lives. /data
must be a network disk or they forget everything the first time the control
plane saves money.
And os.cpu_count() reports the machine, not the share of it you were given.
A sixteen-core endpoint encoded a frame in 208 ms; a four-thread laptop
does it in 195. In a container allocated sixteen cores on a host with a
hundred and ninety-two, torch starts a hundred and ninety-two threads and they
contend rather than work. More hardware, less throughput, nothing in the logs.
The cgroup quota is asked first now, because that is what actually limits the
process and it is invisible to both cpu_count and the affinity mask -
and /health reports the thread count, because the only thing worse than this
bug is this bug with no way to see it from outside.
Pact's claim is that it does not take your word for whether something happened. That sits oddly beside a target it made up. "Do some squats every day" names no number, so one gets looked up and the goal keeps the URL - a person can then disagree with the guidance rather than with Pact. Only when the sentence has no number in it: "ten squats" means ten, and a cited URL is kept only if it was actually among the results, because a model asked for a citation will sometimes produce the shape of a reference rather than a reference.
Two traps worth knowing. Every model id in Nebius's own documentation
examples is decommissioned, including the one in their quickstart, so treat
the live catalogue as the only trustworthy source. And Nemotron spends on the
order of a hundred tokens reasoning before answering, counted against the
same budget — too low a max_tokens returns a 200 with empty content,
which looks exactly like a network failure. The floor here is 512 and
reasoning_content is read separately.
npm install
npx wrangler secret put REALTIME_APP_ID # dash.cloudflare.com → Realtime
npx wrangler secret put REALTIME_APP_SECRET
npx wrangler secret put NEBIUS_API_KEY # tokenfactory.nebius.com
For local dev, copy .dev.vars.example to .dev.vars and fill it in.
npm run dev # localhost:8787
npm run deploy # pact.lanvar.ai
npm run typecheck
Then the perception service, in a second terminal:
cd perception
uv venv --python 3.12 && uv pip install -e ".[dev]"
# No uv? The stdlib works the same: python -m venv .venv && pip install -e ".[dev]"
# The demo frames are generated, not committed: shipping footage is how a
# repository ends up carrying pictures of someone's bathroom. Same seed, same
# frames, same scores. Without this, traces/demo does not exist.
python make_demo_frames.py
# No model, no download: watch the whole pipeline work in seconds
python -m pact_perception.run --room gym --embed stub \
--source folder --folder traces/demo --loop-folder --hub http://localhost:8787
# The real encoder, against a webcam
uv pip install -e ".[dev,encoder,camera]"
python -m pact_perception.run --room office --embed local --source camera \
--hub http://localhost:8787
--embed stub is a fake encoder with no model behind it. Frames flow, the
gate gates, calibration fits and the state machine runs — but it understands
nothing, and no threshold fitted against it transfers. It exists so the wiring
can be exercised without a four gigabyte download.
Open /goals to add a goal. See perception/README.md
for tuning, the log contract, and the measured latencies.
src/
index.ts Router. /api/{hub,engine,frames,sfu}/*
hub.ts Hub DO: the live registry, derived from open sockets
sfu.ts Realtime SFU client
log.ts The structured logger. The only console.log in the Worker.
compute.ts Where the encoder's machine should be, and why
goals/
engine.ts Engine DO: the ledger, the frame relay, the alarm
schema.ts The SQL, in one place
evaluate.ts Goal rules. Pure: transitions in, verdict out.
sequence.ts Order within a goal: a step that requires the step before
sweep.ts Settlement, run off the alarm
planner.ts Nemotron tool-calling: a sentence → skills + a goal
assistant.ts One turn of talking, with the ledger as its memory
ground.ts Tavily: look the target up rather than invent it
nebius.ts Token Factory client. The max_tokens trap lives here.
types.ts Every row and wire shape
public/
index.html The wall goals.html The console: goals, questions
join.html The publisher rig.html Clips published as feeds
js/
sfu-client.js publish() and subscribe(). The WebRTC core.
frame-tap.js The motion gate and the frame sampler, in the browser
join.js console.js rig.js
perception/ Python. Own pyproject. See its README.
make_demo_frames.py Generates the synthetic recording the quickstart replays
pact_perception/
run.py The loop. The only file that knows the order.
config.py Every flag, and what it refuses
sources.py folder | camera | relay | sfu, behind one interface
gate.py L0 motion gate embed.py C-RADIO client, views
skills.py Dot products, softmax, calibration
machine.py The pure state machine. No I/O.
teacher.py Nemotron VL escalation
probe.py Logistic refit with before/after holdout
store.py Local SQLite: scores, embeddings, labels
ledger.py The Engine WebSocket
serve.py The encoder behind HTTP, for a Serverless Endpoint
device.py The cores we were given, not the ones that exist
sleep.py Stop this machine when nothing is publishing
tests/ 159 tests. The arithmetic, mutation-checked.
bridge/ Node. RTSP/RTMP/MJPEG → the wall. Own README.
m5stackchan/ ESP32 camera firmware.
No build step and no framework on the frontend — modules the browser loads directly, which keeps the interesting parts readable.
Workers can't carry WebRTC: no UDP, no long-lived sockets. The Worker only brokers the SDP handshake, because that step needs the SFU app secret and the browser can't be trusted with it. Video goes browser ↔ Cloudflare edge directly.
One honest amendment. Sampled frames do pass through the Worker — one JPEG every two seconds per camera, about 25 KB/s, relayed to perception and never stored. That is a trickle rather than a stream, and the WebRTC path to the wall is untouched, but it is a real change to a property this file used to claim without qualification.
The direction of the SDP offer flips depending on what you're doing, which is the one genuinely confusing part of the SFU API:
/renegotiate. Two round trips.Both paths have a race worth knowing about: setRemoteDescription can fire
ICE and track events synchronously, so listeners must be attached before
it is called. See the comments in
public/js/sfu-client.js.
When pact says "you did your workout", we have to be able to show why. One JSON object per line, one logger per process, a closed set of event names so a typo is a compile error rather than a line that never matches a grep.
Every line carries a trace_id, minted when a gate opens and retired when it
closes, so one grep reconstructs one whole session:
curl -s localhost:8787/api/engine/explain/<trace_id>
The single most useful line is skill.rejected, and it distinguishes two
near-misses that want opposite fixes:
{"ev":"skill.rejected","reason":"dropped below exit before dwell",
"peak":0.51,"held_ms":306,"needed_ms":1000,"needed_conf":0.0}
{"ev":"skill.rejected","reason":"held past dwell but sagged below enter",
"peak":0.51,"held_ms":1210,"needed_ms":1000,"needed_conf":0.44}
The first ran out of time: shorten dwell_s. The second held plenty long but
the score had sagged out of reach by the time dwell elapsed: lower
enter_conf, or get more labels. The peak cannot tell them apart — crossing
enter_conf is how a candidacy starts, so the peak always clears it.
A system that only logs successes cannot answer "why didn't that count?", which is the question anyone actually asks.
Zero-shot is weakest exactly where it hurts most. The first time you do something genuinely new in front of a camera that has never seen it, you get zero-shot quality, and if that is weak for your room the session may be missed entirely. Running loose at the start is a mitigation, not a cure — which is why manual entry has to stay first class.
The refit needs more labels than the design claims. "Five beats zero-shot, sixteen makes it solid" is optimistic; measured on synthetic data the fit first wins at around forty. Real labels are not random draws in 1152 dimensions so the true number is likely better, but it is not five.
The calibration background is biased by the gate. Still frames never reach the encoder, so the distribution a skill calibrates against is "frames interesting enough to embed", not the room at rest. A screen skill escapes this one — screens are not hard-gated — and walks into a worse version of it: if you code all day, coding is your screen's background, so a "code for an hour" skill calibrates its bias against the very thing it is meant to detect and may never cross its own threshold. Goals about what you do unusually work better here than goals about what you do constantly.
Thresholds must bracket the real distribution. If exit_conf sits below
the median score, sessions open and never close. Read the numbers off a
recorded trace before trusting any of them.