Spaces:
Running on Zero
Running on Zero
File size: 8,738 Bytes
7bb2fda f9a8763 7bb2fda e0f9b43 7bb2fda e0f9b43 f9a8763 7bb2fda | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | """Mini HTML returned next to every picture.
HTML is the final destination: a chat model reads this page, edits it and
builds its own on it. The page is self-contained Three.js. Geometry is inlined
at low poly (a rung of the simplification ladder), so it outlives Gradio's
temporary file URLs. Even low poly costs thousands of tokens, so the tools
return the page as a URL unless the caller asks for its text.
"""
import json
import numpy as np
from .scene import GLOW, _sky
from .simplify import base_color, decimate
THREE = "https://unpkg.com/three@0.160.0/"
SCENE_FACE_BUDGET = 400 # per object in a scene page
LADDER_MAX_FACES = 1000 # rungs above this are left to the GLB files
def _hex(rgb):
return "#%02x%02x%02x" % tuple(int(c) for c in rgb[:3])
def _sky_hex(sun):
sky = _sky(sun)
return sky if isinstance(sky, str) else _hex([c * 255 for c in sky])
def _geometry(mesh, budget):
"""Inline mesh; "floor" marks a mesh that wouldn't decimate down to the budget."""
m = decimate(mesh, budget) if len(mesh.faces) > budget else mesh
return {"faces": int(len(m.faces)), "budget": budget, "floor": bool(len(m.faces) > budget),
"p": np.round(m.vertices, 2).ravel().tolist(), # cm: plenty at low poly, fewer tokens
"i": m.faces.ravel().astype(int).tolist()}
def _object(oid, word, mesh, position, yaw_deg, cand=None, glow=False, budget=SCENE_FACE_BUDGET, label=None):
o = {"id": oid, "word": word, "label": label, "position": [round(float(c), 3) for c in position],
"yaw_deg": round(float(yaw_deg), 1), "color": _hex(GLOW if glow else base_color(mesh)),
"emits_light": glow}
if cand and cand.get("source") == "primitive":
o["primitive"] = {"shape": cand["shape"], "size": [round(float(e), 3) for e in mesh.extents]}
else:
o["mesh"] = _geometry(mesh, budget)
return o
def scene_page(ledger, meshes, config):
sun = config["sun"]
objects = []
for it in ledger["items"]:
p = config["placements"].get(it["id"])
if it["kind"] != "object" or p is None:
continue
dec = it["decision"]
objects.append(_object(it["id"], it["word"], meshes[it["id"]], p["position"], p["yaw_deg"],
cand=dec["chosen"], glow=bool(p.get("light")), label=dec.get("label")))
ground = None
if any(i["kind"] == "environment" and i["role"] == "plane" for i in ledger["items"]):
reach = max([abs(c) for o in objects if not o["emits_light"] for c in (o["position"][0], o["position"][2])]
+ [0.3])
ground = {"radius": round(1.6 * reach + 0.5, 3)}
data = {
"title": ledger["sentence"],
"reading": [{"word": t["text"], "kind": t["kind"], "commits": t["commits"]} for t in ledger["tokens"]],
"relations": [{"subject": i["subject"], "relation": i["relation"], "object": i["object"]}
for i in ledger["items"] if i["kind"] == "relation"],
"unresolved": [{"question": q.get("question"), "default": q.get("default_chosen")}
for q in ledger["unresolved"]],
"sky": _sky_hex(sun), "sun": sun, "ground": ground, "objects": objects,
}
return page(data)
def ladder_page(item, rungs):
"""Rungs left to right, from the most detailed embeddable one down to the box."""
objects, x = [], 0.0
for label, rung in rungs:
if len(rung.faces) > LADDER_MAX_FACES:
continue
hw = 0.5 * max(rung.extents[0], rung.extents[2])
objects.append(_object(f"rung{len(objects)}", item["word"], rung, [x + hw, 0, 0], 0,
budget=LADDER_MAX_FACES, label=label))
x += 2 * hw + 0.3 * max(rung.extents[1], 0.1)
for o in objects:
o["position"][0] = round(o["position"][0] - x / 2, 3)
data = {"title": f"{item['word']}: simplification ladder",
"caption": "left → right: " + " · ".join(o["label"] for o in objects),
"sky": "#2a3346", "sun": None, "ground": None, "objects": objects}
return page(data)
def page(data):
payload = json.dumps(data, separators=(",", ":")).replace("</", "<\\/")
return TEMPLATE.replace("__THREE__", THREE).replace("__TITLE__", _escape(data["title"])) \
.replace("__DATA__", payload)
def _escape(s):
return s.replace("&", "&").replace("<", "<").replace(">", ">")
TEMPLATE = """<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>__TITLE__</title>
<style>html,body{margin:0;height:100%;overflow:hidden;font:13px system-ui,sans-serif}
#cap{position:fixed;left:12px;bottom:12px;color:#fff;background:#0008;padding:6px 10px;border-radius:6px}</style>
<script type="importmap">{"imports":{"three":"__THREE__build/three.module.js","three/addons/":"__THREE__examples/jsm/"}}</script>
</head><body><div id="cap"></div>
<script type="module">
import * as THREE from "three";
import {OrbitControls} from "three/addons/controls/OrbitControls.js";
// Scene data from VisualLearningIdea: positions in metres, Y up, yaw about Y.
// Objects carry inline low-poly geometry ("mesh") or a primitive spec.
const D = __DATA__;
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(D.sky);
scene.add(new THREE.HemisphereLight(0xffffff, 0x445533, 0.9));
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.01, 1000);
function geometryOf(o) {
if (o.primitive) {
const [w, h, d] = o.primitive.size;
const g = {sphere: () => new THREE.SphereGeometry(h / 2, 24, 16),
cylinder: () => new THREE.CylinderGeometry(w / 2, w / 2, h, 24),
cone: () => new THREE.ConeGeometry(w / 2, h, 24),
plane: () => new THREE.BoxGeometry(w, h, d),
box: () => new THREE.BoxGeometry(w, h, d)}[o.primitive.shape]();
g.translate(0, h / 2, 0); // rest on y = 0 like every asset
return g;
}
const g = new THREE.BufferGeometry();
g.setAttribute("position", new THREE.Float32BufferAttribute(o.mesh.p, 3));
g.setIndex(o.mesh.i);
g.computeVertexNormals();
return g;
}
const frame = new THREE.Box3();
for (const o of D.objects) {
const mat = o.emits_light
? new THREE.MeshBasicMaterial({color: o.color})
: new THREE.MeshStandardMaterial({color: o.color, roughness: 0.85, flatShading: true, side: THREE.DoubleSide});
const m = new THREE.Mesh(geometryOf(o), mat);
m.name = o.id;
m.position.fromArray(o.position);
m.rotation.y = THREE.MathUtils.degToRad(o.yaw_deg);
m.castShadow = !o.emits_light;
m.receiveShadow = true;
scene.add(m);
if (!o.emits_light) frame.expandByObject(m);
}
if (frame.isEmpty()) frame.setFromCenterAndSize(new THREE.Vector3(), new THREE.Vector3(1, 1, 1));
const center = frame.getCenter(new THREE.Vector3());
const radius = Math.max(frame.getSize(new THREE.Vector3()).length(), 0.3);
if (D.ground) {
const g = new THREE.Mesh(new THREE.CircleGeometry(D.ground.radius, 64),
new THREE.MeshStandardMaterial({color: 0x4d7c3a, roughness: 1}));
g.rotation.x = -Math.PI / 2;
g.receiveShadow = true;
scene.add(g);
}
const sun = new THREE.DirectionalLight(0xfff1d0, 2.2);
if (D.sun) {
const el = THREE.MathUtils.degToRad(D.sun.elevation_deg), az = THREE.MathUtils.degToRad(D.sun.azimuth_deg);
const dir = new THREE.Vector3(Math.cos(el) * Math.sin(az), Math.sin(el), Math.cos(el) * Math.cos(az));
sun.position.copy(center).addScaledVector(dir, radius * 3);
sun.castShadow = !!D.sun.shadows;
} else {
sun.position.copy(center).add(new THREE.Vector3(radius, radius * 2, radius * 1.2));
}
sun.target.position.copy(center);
Object.assign(sun.shadow.camera, {left: -radius * 1.5, right: radius * 1.5, top: radius * 1.5,
bottom: -radius * 1.5, far: radius * 8});
sun.shadow.mapSize.set(1024, 1024);
scene.add(sun, sun.target);
camera.position.set(center.x + radius * 0.9, center.y + radius * 0.6, center.z + radius * 1.2);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.copy(center);
controls.update();
document.getElementById("cap").textContent = D.caption || D.title;
addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
renderer.setAnimationLoop(() => { controls.update(); renderer.render(scene, camera); });
</script></body></html>
"""
|