Let the signed-out sample reach a video result - #376
Merged
Conversation
A signed-out visitor could Run the starter graph and see real canned results, but only text and an image. genVideo was wired to demoDeny. Video is the most impressive thing nanoodle does, so the first-run demo stopped one step before the payoff. It was worse than the deny message suggested. demoMode() returns null the moment the topology changes, so a visitor who ADDED a video node lost demo mode altogether: runGroup opened the sign-in wall and returned before any node ran. The reward for exploring was an empty canvas and a login prompt. This adds ONE covered extension: the starter plus an Image→Video node fed from the image output, with nothing downstream of it. That shape now serves demo-sample.mp4. Everything else still gets the sign-in wall. - demoVideoNode() matches the shape structurally, not by a baked signature. The appended node's id depends on what the visitor added and removed first. - DEMO_CTX.genVideo serves the clip only when opts.runKey is that node's id, so a Text→Video or Video-edit node still lands on the wall. - The clip is read into a data URL like demo-sample.jpg, so save and open behave like a real result. It carries the same sample badge, and the pill says outright that the clip ignores the visitor's motion prompt. - A missing or non-video response degrades to the wall, never a broken player. The content-type test catches a static host answering 200 with the SPA shell. - The deny copy no longer claims samples cover "text and images only", and it now runs through t() — the 5 translations of it were dead before. The asset is real. scripts/gen-demo-clip.mjs renders it from the shipped demo-sample.jpg with bytedance-seedance-v1-pro-fast (720p, 5s, 0.12 USD), then encodes it to 640x360 H.264, no audio, 223 KB. Frame 0 of the clip is the still. The script is hand-run, refuses to write over 2 MB, and records the provenance in its header. index.html only. play.html carries no demo-mode code: DEMO_CTX substitutes for CTX at the editor's runGroup boundary and never changes a request body, so the dual-engine payload rule does not reach it. check-js-parity and check-run-compat both pass unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
nanoodle | e88ee5e | Commit Preview URL Branch Preview URL |
Jul 30 2026, 12:46 AM |
…deo proof The chip used to hide whenever a signed-out visitor could free-sample Run. That looked harmless — nothing is billed, so nothing is concealed — and it was not. The one thing a visitor can add to the starter and still keep the sample is an Image→Video node, the priciest node on the canvas, so the cost figure vanished at exactly the moment it became worth reading. A price that disappears when it gets big reads as a dark pattern however it got there. The chip now stays up in every demo mode, relabelled "to run for real", with a title that says this run is not billed and that the figure prices running the graph on your own key. Measured in the browser: the starter quotes ~$0.04, and appending the Image→Video node takes it to ~$1.08. Adds proof/demo-video/ — the verification this branch was missing. It drives the real index.html in headless Edge over CDP with the whole nano-gpt.com origin blocked, and records every request the page attempts, so zero spend is proven and not assumed. 36 assertions, all passing: the covered shape samples, the near-miss shapes still hit the sign-in wall, the clip decodes to 640x360 / 5.04 s, the ✨ sample result badge and the ⬇ save / ↗ open buttons are present, and sw.js caches the clip on first use rather than precaching it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mikkel
added a commit
that referenced
this pull request
Jul 30, 2026
…e moved surfaces #374 and #376 both changed index.html and play.html, so the two artifacts this guard pins had to be re-derived rather than merged. Nothing here loosens a rule. Baseline. The guard reported exactly one change and it is #376's: `async genVideo(model, prompt, opts, imageDataUrl, onProgress){` gained a second copy in index.html at 9046, the DEMO_CTX override that lets a signed-out sample reach a video result. The distinct count held at 893, nothing entered the shared set and nothing left it, so the only ratchet movement is index.html occurrences 957 -> 958 (and multi-occurrence lines 74 -> 75). The refresh also recorded 2 new look-alike hashes, both from the same commit: index.html:9022 `if(!r.ok) throw new Error(String(r.status));` and index.html:9025's FileReader line. 796 -> 798 hashes over 219 lines. Every delta is attributable to #376; none is a one-sided edit. Sandbox matrix. Its ranges are absolute and #376 pushed index.html down by up to 111 lines and #374 pushed play.html down by 19, so all 25 ranges moved. Each was re-derived and checked byte for byte against its pre-merge content. Left stale, 9 of the 16 cases return the wrong verdict — measured, not assumed. All 16 now pass, row 8 still reporting the same 5 one-sided deletions and 1 occurrence drift at their new positions. docs/twin-drift.md and scripts/twin-drift-worklist.mjs carry the same ranges, so all 216 file:line references in them moved too; the work-list table is unchanged (893, and rows 1 to 6 still free 255 lines). Hook. #372 landed touches_llmsfull and #373 landed touches_launchfacts on the same early-exit line this PR adds touches_twindrift and touches_twindriftcases to. All 4 are unioned in, and each was proved to fire by staging one file for it and watching which check ran; an unrelated file still early-exits. The 2 new checks are renumbered 43 and 44, main's launch-facts and llms-full keeping 41 and 42. Re-measured, and figures corrected where the merge moved them: timings (30 samples per case, load average 3.19-5.68), the baseline byte sizes, the raw unblanked occurrence count, and the guard-suite total, now 55 of 55 passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What breaks today
A signed-out visitor can Run the starter graph and see real canned results. The demo covers text
and an image. It stops there.
genVideo,genAudioandtranscribeare all wired todemoDeny.Video is the most impressive thing nanoodle does, so the first-run demo ends one step before the
payoff.
The real path is worse than the deny message suggests.
demoMode()returns null as soon as thetopology changes. A visitor who ADDS an Image→Video node does not get
demoDeny. That visitorloses demo mode:
runGroupopens the sign-in wall and returns before any node runs. The reward forexploration is an unchanged canvas and a login prompt.
This was confirmed in a real browser before the change.
What this changes
One covered extension: the starter, plus an Image→Video node fed from the image output, with
nothing after it. That shape serves
demo-sample.mp4. Each other shape gets the sign-in wall.demoVideoNode()matches the shape structurally, not with a second baked signature. The id ofthe added node depends on what the visitor added and removed first. An id-for-id comparison would
miss the path that this change covers.
DEMO_CTX.genVideoserves the clip only whenopts.runKeyis the id of that node. A Text→Videonode or a Video-edit node still gets the wall, also in a covered canvas.
demo-sample.jpg. Thus ⬇ save and ↗ openbehave as they do for a real result.
demoMode()gets avideomode. The pill text for that mode says that the clip is a cannedexample, and that the clip ignores the motion prompt of the visitor. That warning is always true
here: the motion prompt on an added node is always the prompt of the visitor.
markDemoResult: "✨ sample result — sign in to run it for real".broken player. The content-type test is necessary: a static host answers an absent path with the
SPA shell at status 200.
t(). Its 5 translations were already in the maps, but no code applied them.The price chip stays visible (review finding, now fixed)
Review found that
refreshRunEstimate()hid the#runestprice chip wheneverinDemoRun()wastrue. Because this change makes
inDemoRun()true for the video extension, the price disclosuredisappeared on the most expensive node that a visitor can add. That is now fixed instead of
documented.
The chip stays visible in each demo mode. It is relabelled
to run for real, and its tooltip says"The sample results are canned, so this run isn’t billed. This is what running the graph on your own
key costs." Both strings are translated into all 5 languages.
The old behaviour looked safe, because nothing is billed in a demo run and therefore nothing is
concealed. It was not safe. A price that disappears when it becomes large reads as a dark pattern,
whatever the cause. The tooltip removes the other risk: the visitor must not think that this click
is billed.
Measured in the browser, signed out, with prices from the real catalog:
▶ ~$0.04 to run for real▶ ~$1.08 to run for realThe video node also shows its own price on the node,
~$1.04/clip.updateNodePrice()is notgated on demo mode, so that label was never hidden.
The asset is real
scripts/gen-demo-clip.mjsrenders the clip from the SHIPPEDdemo-sample.jpgwith a realimage-to-video model. It is hand-run, it is a dry run by default, it refuses to write a file larger
than 2 MB, and it holds the provenance in its header.
bytedance-seedance-v1-pro-fastvid_ms5jr1es67cfaa050a06)The file holds one stream. There is no audio track.
moovis at byte 36 andmdatis at byte2306, therefore the file is faststart. Each figure in the table was measured on the committed file,
except the cost: only the header and the NanoGPT meter record that.
Frame 0 of the clip is the shipped still. The camera moves in, the steam moves, and the rain runs
down the glass. The clip is not a pan over the still.
Engine scope
index.htmlonly.play.htmlholds no demo-mode code.DEMO_CTXreplacesCTXat therunGroupboundary of the editor, and it never changes a request body. Therefore the dual-engine payload rule
does not apply.
check-js-parityandcheck-run-compatboth pass.sw.jsgets a comment only. The clip is deliberately not precached: 223 KB at each install, for ascreen that most visitors never open. The fetch handler caches it at first use.
How this was verified
The branch was merged with
origin/mainatfd9b459(after #372, #373 and #374). The merge wasclean: #374 touches the same two files, but different regions. Each result below is from the merged
tree.
Guards — 53 of 53 pass. Each
scripts/check-*.mjspasses, and this includes the two guards that#372 and #373 added (
check-llms-full.mjsandcheck-launch-facts.mjs). The extra CI stepnode scripts/gen-llms-full.mjs --checkalso passes.check-js-parityreally ran; it did not skip.Both sibling repositories were exported fresh at their
origin/main, as CI does.Browser — 36 of 36 assertions pass. This PR now commits its verification, at
proof/demo-video/. Run it withnode proof/demo-video/verify-demo-video.mjs. It serves theworktree, starts headless Microsoft Edge, and drives the real
index.htmlover CDP. The wholenano-gpt.comorigin is blocked, and each request that the page tries to make is recorded. Zerospend is therefore proven, not assumed. The check covers:
exact. The starter plus the added node is modevideo, anddemoVideoNode()returns that node.done, withnano-gpt.comunreachable.data:video/mp4URL of 304,566 characters. The<video>srcequals it.
readyStateis 4, duration is 5.041667 s, and the size is 640x360.nodes carry the badge.
to run for real, for the starter and after the video nodeis added. The figure increases.
sw.jsdoes not precache the clip. After the run, the fetch handler has put it in thenanoodle-v6cache, and the cached blob is 228,408 bytes ofvideo/mp4— the shipped file.returns to mode
videowhen the shape is correct again.nano-gpt.comURLs that the page tried were the 4 freemodel-list reads of the boot revalidate, and the block stopped all of them.
proof/is gitignored, so these 3 files were added withgit add -f.proof/prompt-caps/from#369 is the precedent.
catalog-fixture.jsonholds 3 normalized catalog entries. The check puts them inlocalStorage,because the price chip needs prices and the network is off.
--capturerecords the fixture againfrom the free public model lists. That mode never runs a graph and it spends nothing.
Two figures in an earlier version of this description were wrong and are corrected above: the guard
count was 51 before #372 and #373 added theirs, and the description claimed a 22-assertion
node:vmharness that was never committed. The committed browser check replaces it.