feat(sim): Right axis = deterministic real-time painterly dream
Replace the pre-baked SD restyle variants (too fluid — the painting re-rolled every keyframe and the flow-warp melted frames) with a deterministic, real-time dream that holds STILL across the loop; only the footage's own motion moves. Design: docs/superpowers/specs/2026-06-22-right-axis-deterministic-dream-design.md Engine (player/alteration.py): Restyle(variant) -> Dream(strength, intensity); Calibration.right_variant_map -> dream_gain. player/state.py: a Right change is a LIVE_UPDATE now, not a crossfade (only a clip swap crossfades). Plan dict restyle -> dream. Tests migrated; 233 green. (No new Python behavior for the client-render swap below.) Renderer (simulator front-end): the Right dream is a WebGL2 Kuwahara shader on a <canvas> over the live video — edge-preserving, so motion/structure stay crisp; the dream is the same footage, just stylized (no blur). Ramped by the knob; max goes "trippy" (vivid saturation + ~45deg hue drift + posterize, concentrated near max via t=intensity^2). Phase-1 luminous-haze (CSS+bloom) was built then retired by eye (blur read as out-of-focus video). Mood grade rides as a CSS filter on the canvas; bloom layer removed. Dev ergonomics that fell out of the iteration: - /dev/version + a 1s client poll = live-reload (open tab never runs stale renderer code; reloads on my edits AND server restarts). - no-cache on the dev server; an on-screen error banner + crash-proof render so a silent throw can't masquerade as "nothing is changing". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+29
-2
@@ -7,6 +7,9 @@ endpoints (/api/select, /api/catalog/meta) and the X-ray are retired.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -30,6 +33,24 @@ STATIC_DIR = Path(__file__).parent / "static"
|
||||
MEDIA_DIR = Path(__file__).parent / "sample_media"
|
||||
DEFAULT_MANIFEST = MEDIA_DIR / "manifest.json"
|
||||
|
||||
# Per-process boot token: changes on every server restart so the dev live-reload
|
||||
# (below) also fires when Python code changes (which needs a restart), not just
|
||||
# when a static asset's mtime changes.
|
||||
_BOOT_ID = f"{os.getpid()}-{int(time.time())}"
|
||||
|
||||
|
||||
def _asset_version() -> str:
|
||||
"""A short digest of the renderer assets' mtimes + this process's boot token.
|
||||
Changes whenever app.js/style.css/index.html change on disk OR the server is
|
||||
restarted — the signal the browser polls to know it's running stale code."""
|
||||
parts = [_BOOT_ID]
|
||||
for name in ("app.js", "style.css", "index.html"):
|
||||
try:
|
||||
parts.append(f"{name}:{(STATIC_DIR / name).stat().st_mtime_ns}")
|
||||
except OSError:
|
||||
parts.append(f"{name}:?")
|
||||
return hashlib.sha1("|".join(parts).encode()).hexdigest()[:16]
|
||||
|
||||
|
||||
class ControlsModel(BaseModel):
|
||||
content: str
|
||||
@@ -44,7 +65,7 @@ class ControlsModel(BaseModel):
|
||||
class CalibrationModel(BaseModel):
|
||||
mood_gain: float = 1.0
|
||||
overlay_gain: float = 1.0
|
||||
right_variant_map: list[int] = [0, 1, 2, 3, 4]
|
||||
dream_gain: float = 1.0
|
||||
|
||||
|
||||
class AlterationRequest(BaseModel):
|
||||
@@ -86,7 +107,7 @@ def create_app(manifest_path: Optional[Path] = None) -> FastAPI:
|
||||
Calibration(
|
||||
mood_gain=req.calibration.mood_gain,
|
||||
overlay_gain=req.calibration.overlay_gain,
|
||||
right_variant_map=tuple(req.calibration.right_variant_map),
|
||||
dream_gain=req.calibration.dream_gain,
|
||||
)
|
||||
if req.calibration
|
||||
else DEFAULT_CALIBRATION
|
||||
@@ -98,6 +119,12 @@ def create_app(manifest_path: Optional[Path] = None) -> FastAPI:
|
||||
"content": {"audio_source": content.audio_source, "video": content.video},
|
||||
}
|
||||
|
||||
@app.get("/dev/version")
|
||||
def dev_version():
|
||||
# Dev live-reload signal: the browser polls this and reloads when it
|
||||
# changes, so it never silently runs a stale renderer during iteration.
|
||||
return {"version": _asset_version()}
|
||||
|
||||
@app.get("/api/clips")
|
||||
def api_clips():
|
||||
return {"clips": [c.to_dict() for c in app.state.clips]}
|
||||
|
||||
Reference in New Issue
Block a user