Skip to content

VisualLM 2.0: real 3D, multi-provider AI, faster + self-validating generation#2

Open
Maxpeng59 wants to merge 18 commits into
mainfrom
refine-generation-repair-pipeline
Open

VisualLM 2.0: real 3D, multi-provider AI, faster + self-validating generation#2
Maxpeng59 wants to merge 18 commits into
mainfrom
refine-generation-repair-pipeline

Conversation

@Maxpeng59

Copy link
Copy Markdown
Owner

Makes VisualLM an industrial-grade app: real 3D rendering, multi-provider AI, and a generation pipeline that is fast and reliable instead of slow and error-prone. Production smoke-tested (binds 0.0.0.0 on injected PORT; /api/health reports validator: true, library_size: 50).

Real 3D (was flat point clouds)

  • Software 3D pipeline in the sandbox: lit, depth-sorted surface3d/mesh3d, shaded cam.sphere, 3D lines/polys/grid/axes with numeric ticks, drag-to-orbit + scroll zoom.
  • System prompt rewritten with solid-3D worked examples and 2D-vs-3D rules.

Right animation, faster + fewer errors

The root cause of "errors after 3 tries" + "slow" was that the browser was the validator, so each bad scene cost a full round-trip + a slow model repair. Restructured to catch/fix errors without model calls:

  • validate_scene.js — headless server-side validator in a hardened Node vm (fresh empty context — no process/require/import; constructor escape contained; 2s timeout). Reports throws / blank / off-screen (data-vs-pixel coordinate mixups) / no-motion / no-labels in ~50ms. Repairs happen server-side; the browser receives a runnable scene. Degrades gracefully without Node.
  • autofix_code() — deterministically prefixes bare Math.*/PI/TAU outside strings; fixes the Refine generation→repair pipeline: fewer crashes, faster loads, converging repairs #1 "X is not defined" crash with zero model calls.
  • Curated STEM library (scene_library.py + scene_library_generated.json, ~50 verified scenes across ~24 domains, authored by a multi-agent workflow + re-validated) with keyword retrieval: common prompts render in ~5ms instead of 10–60s. The practical, retrieval-augmented realization of "feed it STEM data."
  • Validated-scene cache for instant repeats.

Multi-provider + production

  • Generation chain: Claude → ChatGPT (OpenAI) → Gemini → Ollama, REST-only.
  • Per-IP rate limiting, optional access code, PORT/0.0.0.0 cloud support, Dockerfile (bundles Node) + Render blueprint.
  • Frontend: orbit controls, elapsed-time ticker, curated/cached badges, engine labels.
  • 69-test stdlib suite (validator, auto-fixer, library, cache, rate limit, auth, every endpoint).

Verified in-browser

2D plots with tick values/legends/live readouts; shaded 3D surfaces, DNA helix, torus; dipole field lines; 3D moon-phases. Console clean.

🤖 Generated with Claude Code

彭敬进 and others added 18 commits June 13, 2026 21:24
…pairs

Reduce runtime crashes that exhaust the repair loop, and cut the latency of
both generation and repair, so fewer scenes hit the slow LLM repair path and
the ones that do converge (or fail fast) instead of grinding the whole budget.

Sandbox (sandbox-worker.js):
- Invented helpers (H/cam/view) degrade to a chainable no-op in any chain
  order; cam/view rebind to the proxy so real().invented() is safe too.
- Per-frame error tolerance: a scene that renders then throws on an occasional
  frame keeps running; only a first-frame failure or ~0.5s of sustained
  throwing escalates to repair.
- Heartbeat on the first clean frame (load "ready" signal ~300ms sooner).
- offendingLine(): pull the exact failing source line out of the stack.

Client (app.js, index.html):
- MAX_REPAIRS 3->2; forward the offending line to /api/repair.
- Non-convergence guards: bail when a repair repeats the same error or returns
  unchanged code, instead of grinding the full (slow) repair budget.
- Bump app.js cache-buster v10->v11 (was stranding all client changes behind
  the stale ?v=10 for returning users).

Server (main.py):
- Generator-aware retry budget (cloud=2, local Ollama=3).
- Credit ".axes(" as a label so well-labeled plots stop triggering false
  "unlabeled" regenerations.
- VISUALLM_CLAUDE_EFFORT / VISUALLM_CLAUDE_MAX_TOKENS env knobs (default high/32k).
- _repair_hint(): error-class fix guidance + a minimal-change directive, wired
  into every repair provider; fold the offending line into the repair error.

Tests: +5 (repair hints, repair-visualization line folding). Suite 36->41, green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two production-robustness gaps in the public HTTP server, independent of the
generation→repair work on this branch:

- do_POST dispatched to handlers with no catch-all, so an UNEXPECTED exception
  (a bug, a new SDK error type — distinct from the RuntimeErrors that handlers
  already turn into 503s) escaped, dropped the client connection, and spilled a
  bare traceback to stderr with no response. Now logged and returned as a clean
  500.
- send_json's writes weren't guarded, so a client disconnecting mid-response
  (common during a slow generation) surfaced BrokenPipeError as an unhandled
  traceback. Now swallowed.

Test: +1 (unexpected handler exception -> 500). Suite 41->42, green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two complaints — "still errors after 3 tries" and "took a long time" —
both trace to the same root: the browser was the validator, so every bad
scene cost a full client round-trip + a slow model repair. This restructures
the pipeline around catching and fixing errors WITHOUT model calls.

- validate_scene.js: headless server-side scene validator. Runs untrusted
  generated code in a hardened Node `vm` (fresh empty context — no process/
  require/import reachable; classic constructor.constructor escape contained;
  hard 2s timeout) and reports throws / blank / no-motion / no-labels in ~50ms.
- main.py evaluate_scene(): single source of truth, validator-authoritative,
  static-gate fallback when node is absent. _try_generate now repairs fatal
  scenes SERVER-SIDE (exact runtime error folded into the prompt) so the
  browser receives an already-runnable scene.
- autofix_code(): deterministically prefixes bare Math.* calls / PI / TAU
  outside strings & comments — fixes the #1 "X is not defined" throw with
  ZERO model round-trips. Wired into sanitize_code.
- scene_library.py + library_match(): curated, hand-verified STEM corpus with
  keyword retrieval. Strong match short-circuits to an instant, known-correct
  scene (Fourier prompt: 5ms vs a 10-60s generation). This is the practical,
  retrieval-augmented realization of "feed it STEM data".
- Exact-prompt cache of validated scenes (instant repeats), only caches clean
  scenes. plan_visualization chain: cache -> strong library -> models ->
  library fallback -> placeholder.
- Frontend: elapsed-seconds ticker during generation; curated/cached badges.
- Dockerfile installs Node so the validator runs in production too.
- Health reports validator + library_size. 66 tests pass (24 new).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The weak local model's most common SEMANTIC failure isn't a crash — it's
drawing real content in the wrong coordinate space (e.g. v.line(v.X(x), ...),
which double-maps every point off the canvas). It runs, it "paints", it has
labels, so the old checks passed it — but the screen shows only axes.

The validator now tracks, per content draw, whether it lands on-canvas (data-
space view/cam methods convert via X/Y or project() first, exactly like the
real helpers). paint>0 with zero on-screen => onscreen:false, which
evaluate_scene treats as fatal and repairs with a coordinate-space hint.
'paint' now counts CONTENT draws only (grid/axes/background are scaffold), so
an axes-only "scene" is correctly seen as blank. All 6 library scenes pass;
72 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- scene_library_generated.json: 42 scenes authored by the stem-scene-library
  multi-agent workflow (one author per domain + adversarial per-scene review),
  every one re-validated through validate_scene.js (runs, paints on-screen,
  animates, labeled). Covers ~24 domains: quantum, linear algebra, waves,
  algorithms, astronomy, neuroscience, control systems, probability, etc.
- Hand-written electromagnetism scenes (dipole field lines, RC circuit) to
  cover the two UI example chips the workflow batch missed.
- library_match: weight title/tag tokens, drop stopwords, dominance gap so an
  ambiguous near-tie doesn't fast-path; high-confidence matches fast-path even
  when a duplicate-topic scene also scores well. Library fallback now requires
  real relevance (>=2.0) instead of any score, so a failed generation never
  shows a wrong-topic curated scene.
- Dockerfile copies the generated JSON; README documents the reliability/speed
  architecture + how to regenerate the library. 69 tests pass.

7/10 example chips now render instantly from the verified library; the rest
generate. Verified in-browser: dipole + 3D moon-phases scenes render correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Watched the local model write a Doppler scene with pos = t*4 (unbounded): the
source and all wavefronts sail off the right edge within seconds, leaving only
a static observer line. It passed validation because early frames (t<=3) still
had content on-screen. Now the validator samples a late frame (t=30) and tracks
the FINAL frame's on-screen fraction: if <15% of the last frame's content is on
canvas, the scene "drifted off and never loops" -> fatal, repaired with a hint
to bound/loop the motion (t % period, Math.sin(t)). All 50 library scenes still
pass; 71 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ge conflicts)

PR #1 was squash-merged into main (948adc3); this branch kept its original
commits (b58ae9e, b426c44) plus newer work - validator drift-detection, the
50-scene verified library, and the stem-viz-plugin - so re-merging main
conflicted on main.py, tests/test_main.py, and validate_scene.js.

For all three the branch is a content-superset of main (it contains PR #1's
exact commits, which main carries as the squash, plus additive work on top), so
the conflicts were resolved in favor of the branch. Verified: 71/71 tests pass
and the merged validator still runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live re-run exposed the subtler case: the local model wrote xSource = -speed*t
again, and fixed labels (title, origin marker) kept the last-frame on-screen
fraction just above 15%, so the moving subject sailed off-screen undetected.

- Validator now also flags a COLLAPSE: content abundant on-screen early
  (>=5 draws in some early frame) but the late frame retains <40% of that
  peak => the subject drifted away while only fixed annotations remain.
  Catches the exact generated Doppler; no false-flags across the 50-scene
  library.
- System prompt Rule 1 now bans `x = speed*t` outright and requires looping/
  bounded motion (Math.sin(t), t % period) so moving subjects stay in frame —
  fixing the root cause for every generation, not just repairs.
- 72 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bake the product's teaching philosophy into the generator. Scenes must build
understanding of the mechanism (sweep the parameter, explain in labels, provoke
prediction) rather than just computing and displaying the answer to a student's
specific problem. Applied consistently to SCENE_SYSTEM_PROMPT ("Style and
pedagogy") and the stem-viz skill (SKILL.md + concepts.md).

Guarded against over-correction: scenes stay concrete and labeled, and a live
readout of a CHANGING quantity remains good teaching (it makes the relationship
visible). The line is illuminate-the-mechanism vs. hand-over-the-answer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make it runnable without touching the terminal after a one-time key setup.
- launch.py: loads .env, picks a free port, starts main.py, waits until
  /api/health is green, then opens the UI in a chrome-less app window
  (Chrome/Edge/Brave --app, else the default browser). Flags: --no-browser, --smoke.
- VisualLM.command: macOS double-click wrapper (chmod +x).
- desktop.py: optional TRUE native window via pywebview (one pip install).
- .env.example: API-key template (.env stays gitignored).
- README: new "Run it (as an app)" section + Files entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shutil.which("node") is `str | None`, and pyright/Pylance can't carry the
non-None proof across the separate node_validator_available() helper (it's a
module global), so the subprocess.run arg list was typed `list[str | None]`
(reportCallIssue + reportArgumentType). Rebind to a local and guard it inline
so it narrows to `str`. Behavior-identical — the inline check is De
Morgan-equivalent to the old `not node_validator_available()`. pyright now
reports 0 errors on main.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
make_app.sh assembles VisualLM.app — a proper bundle (Info.plist + a generated
multi-resolution .icns from make_icon.py, a dependency-free stdlib PNG writer).
Its launcher resolves the repo by the bundle's own location and opens VisualLM
in a native window (pywebview if installed) or an app-style browser window
otherwise. The python3 path is baked at build time so a Finder launch — which
gets a minimal PATH — still finds the right interpreter.

The .app is a git-ignored build artifact; run ./make_app.sh to (re)build. A
fully self-contained PyInstaller bundle is the next step (in progress).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
app_main.py runs the VisualLM server IN-PROCESS (server in a daemon thread,
window on the main thread) instead of spawning `python main.py` — required for a
PyInstaller bundle, where sys.executable is the app binary, not a Python
interpreter. main.py's BASE_DIR now resolves to sys._MEIPASS when frozen so the
static assets + validate_scene.js load from the bundle (no-op unfrozen). Opens a
native pywebview window if installed, else an app-style browser window.

Verified: `app_main.py --smoke` serves /api/health; 72/72 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a committed PyInstaller spec + build_app.sh that freeze app_main.py into a
standalone double-click VisualLM.app — bundles Python, the static assets,
validate_scene.js, and the scene library; runs the server in-process and serves
from the bundle (BASE_DIR -> sys._MEIPASS). Verified: the frozen binary's
`--smoke` boots the server and serves index.html from the bundle (~22 MB app).

Also: app_main.py --smoke now confirms static assets serve (the real bundle-path
test), and the packaged app reads .env from next to VisualLM.app or ~/.visuallm/
(when frozen, its __file__ is inside the bundle). Build artifacts (build/, dist/)
stay git-ignored. 72/72 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests/test_launcher.py (stdlib, headless): .env loading (quote-stripping,
skipping comments/junk lines, never overriding an existing shell env var,
missing-file no-op), free_port (returns the preferred port when free; falls back
to a bindable port when it's taken), wait_healthy (true against a live 200
server, false on a dead port within the timeout), and app_main._env_files
(unfrozen -> local .env; frozen -> alongside VisualLM.app + ~/.visuallm). The new
launcher/app code was previously untested. Suite 72 -> 81, green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n names

autofix_code blindly prefixed every bare PI / TAU / Math-fn name. A scene that
aliases one — `const PI = Math.PI` or `const TAU = 6.28` — was rewritten to
`const Math.PI = ...` (a SYNTAX error), and a local `function log(){}` /
`const log = ...` became `Math.log`. Now we collect the names the scene declares
itself (const/let/var/function) and never rewrite those; genuinely bare calls
and constants are still fixed. Regression test added; suite 81 -> 82, green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_rewrite_declared_names renamed a reserved binding (H/ctx/t) after ANY comma in
a const/let/var span — including expression commas inside ()/[]/{}. So a very
common pattern like `const y = H.lerp(a, b, t);` became `H.lerp(a, b, _t)` (an
undefined reference), and `const p = [Math.cos(t), t]` lost its `t` — silently
feeding the server-side repair loop and burning round-trips. The docstring
already said "top level of the statement" but the regex never enforced bracket
depth.

Replaced the naive regex with a depth-aware scan that renames only genuine
declarators (at depth 0, right after const/let/var or a top-level comma).
Real redeclarations still rename (`const W = H.W, H = H.H` -> `_H`; `let H, ctx`
-> `let _H, _ctx`). Also removed a SyntaxWarning (a `\s` in a non-raw docstring).
2 regression tests; suite 82 -> 84, green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `function scene(...) { ... }` unwrap (rfind-based) and the fence strip run
on every generated scene but were only tested on the trivial case. Verified and
pinned the tricky edges: nested arrow-function braces, a '}' inside a string
literal (rfind must still land on the function's own closing brace), and a
fenced + wrapped scene where both layers are stripped. No code change — coverage
only. Suite 84 -> 86, green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant