img2threejs / app /prompt.py
Mike0021's picture
Preserve dimensions for attached axial meshes
a35b2b8 verified
Raw
History Blame Contribute Delete
10.8 kB
"""Prompt construction for the LLM spec-authoring step.
The upstream project is an interactive agent skill: a vision-capable agent
authors an ``ObjectSculptSpec`` JSON describing the object in the reference
image, and the deterministic forge scripts gate and compile it. The Space
replaces the interactive agent with a single prompted vision call (plus
validator-feedback repair rounds). The system prompt therefore carries:
* the strict-quality contract, distilled from the validator
(forge/stage2_spec/validate_sculpt_spec.py) and the grimoire rubrics;
* a complete, verified strict-passing exemplar spec (app/exemplar_spec.json)
so the model imitates an exact working shape instead of guessing;
* hard output rules (JSON only, primitives subset, honesty rules).
"""
from __future__ import annotations
import json
from functools import lru_cache
from pathlib import Path
EXEMPLAR_PATH = Path(__file__).resolve().parent / "exemplar_spec.json"
@lru_cache(maxsize=1)
def _exemplar() -> str:
return EXEMPLAR_PATH.read_text(encoding="utf-8")
RULES = """\
You author exactly ONE JSON object: an ObjectSculptSpec (schemaVersion "2.1") describing the \
single main object in the reference image. A deterministic validator will strict-gate your spec; \
a compiler turns it into a procedural Three.js factory. Follow the exemplar's STRUCTURE exactly, \
but invent content that faithfully matches THIS image.
HARD RULES (each is enforced by the validator; violations waste a round):
1. Output JSON only. No markdown fences, no prose, no comments.
2. Required top-level keys: targetName (1-3 words, letters/spaces only), targetId, schemaVersion \
"2.1", suitability ("pass" or "conditional"), sourceImage, coordinateFrame, silhouette, \
preSpecAssessment, qualityContract, qualityTargets, actionReadiness, selfCorrectLoop, \
featureReviewTargets, buildPasses, sculptPipeline, lookDevTargets, lightingFromPhoto, \
componentTree, materials, proceduralStrategy, viewEvidence.
3. preSpecAssessment.objectClass: primaryType (specific noun, e.g. "vessel", "hand-tool"), \
primaryDomain "object", and NON-EMPTY formLanguage, structureKind, motionPotential, \
materialFamilies. complexity.tier: use "simple" unless the object genuinely has 6+ distinct \
parts, then "moderate". specDepthDecision.requiredDepth must equal the tier. \
Every preSpecAssessment.complexity.scores value is a RUBRIC SCORE and MUST be an integer 0-3 \
(put real part counts only in estimatedCounts, never in scores). \
unknowns: [] (empty list).
4. qualityContract: qualityBar (match tier), definitionOfDone (>=2 concrete checks), \
minimumSpecDepth (macroComponents>=1, mesoComponents>=1, microComponents>=0, minMaterials>=1, \
minRepetitionSystems>=0, reviewViewpoints>=2), featureGroups (>=3 groups each with id, name, \
qualityCriteria list), visualDeltaChecks (>=2), antiShallowSpecRules (>=2).
5. qualityTargets.reviewViewpoints: >=2 named viewpoints. actionReadiness: filled like the exemplar.
6. selfCorrectLoop.visualAcceptance: reviewer, threshold 0.7, featureReviewPolicy \
{enabled true, maxCritical 5, maxImportant 3, thresholds}.
7. featureReviewTargets: 2-4 REAL identity-defining features of THIS object (never the generic \
starter ids from the exemplar) with tier critical/important, passIds, minimumScore.
8. buildPasses: exactly ["blockout", "structural-pass", "material-pass"] in that order, each with \
id, label, goal, acceptanceCriteria list. sculptPipeline: passGateMode "locked-sequential", \
passOrder identical, currentPass "blockout", completedPasses [].
9. lookDevTargets.qualityPriority: ALWAYS "balanced" (never "reference-fidelity").
10. lightingFromPhoto: >=3 strings that between them name a key/fill/rim light AND contain the \
words "exposure" and "tone" (tone mapping) AND the phrase "contact shadow".
11. detailInventory: targetMinDetails 3 (simple) or 6 (moderate); details list of that many \
entries {id, kind, description, mapsTo:{ref}} where kind comes from the taxonomy (gloss, bevel, \
fastener, linework, contour, seam, stitch, stain, scratch, chip, decal, emissive, hole, groove, \
ridge) and every mapsTo.ref resolves to a REAL id in your spec: a component id, a \
component localFeatures id, a material id, or a material localOverrides id. A "gloss" detail \
requires some material with roughness base < 0.35 or a clearcoat block. A "fastener" detail \
requires a repetitionSystems entry or a micro-level component.
12. componentTree: 2-8 components. Each: unique id (kebab-case), name, role, primitive from \
{box, sphere, ellipsoid, cylinder, cone, capsule, torus, tube, lathe, extrude, ground-blade, \
curve-sweep, plane-card, instanced-cluster}, level macro|meso|micro, parent (null for the root, \
else an existing component id), material (a declared material id), transform {position, rotation, \
scale} as [x,y,z] numbers, positive dimensions {width,height,depth}, importance, confidence. \
Keep transform.scale [1,1,1] unless an intentional local deformation is needed; dimensions set \
the physical mesh size in the primitive's LOCAL axes BEFORE transform.rotation, while transform \
scale belongs to the pivot. Three.js torus geometry starts in the local XY plane with its hole \
axis along +Z: a horizontal vessel rim therefore uses local width and height as the ring diameter, \
local depth as tube thickness, then rotation [1.5708,0,0]; a front-facing loop handle normally \
needs no rotation. Proportions and positions must match the image as closely as you can infer \
them. Root positions are in object space; a child's position is LOCAL to its parent pivot (do not \
repeat the parent's world-space offset). Place a vessel rim at parent-local y = half the body \
height, not at the root body's object-space position.
13. V1.3 GEOMETRY TRUTH: every component must set topologyClass to one of \
{continuous-sculpt, assembled-solid, conforming-shell, surface-relief, fiber-strand, \
material-only} and topologyRationale to a concrete sentence citing visible shape evidence. \
Do not pair continuous-sculpt with box/cylinder/cone or fiber-strand with box/plane-card. \
Advanced primitives need a measured geometryDescriptor: extrude uses \
profile2D {points:[[x,y],...], depth}; lathe uses latheProfile {points:[[radius,y],...], \
segments}; tube uses tubePath {points:[[x,y,z],...], radius, closed}; curve-sweep uses \
curveSweep {spine:[[x,y,z],...], crossSection:{points:[[x,y],...]}, closed}; ground-blade uses \
bladeSpec {stations:[[x,spineY,edgeY],...], thickness, grindFrac, swedgeFromTipFrac, spineFlat}; \
instanced-cluster sets baseGeometry. Use simple primitives when they are the truthful match.
14. V1.3 COLOUR GROUNDING: every non-material-only component must include \
colorMaterialRecipe {dominantAlbedo:"rgba(r, g, b, 1.0)", \
secondaryAlbedo:"rgba(r, g, b, 1.0)", materialClass, materialClassConfidence}. materialClass \
is one of {metal, plastic, wood, fabric, skin, glass, ceramic, rubber, stone, unknown}. If a \
visible gradient matters, add the SAME colorGradient to this recipe AND its referenced \
materials[] entry: {type:"linear"|"radial", axis:[x,y], \
stops:[{offset:0,color:"rgba(...)"},{offset:1,color:"rgba(...)"}]}. For linear gradients axis \
is a non-zero UV direction such as [1,0]; for radial gradients it is the UV centre such as \
[0.5,0.5]. Stops must have strictly increasing offsets from 0 through 1. Sample the image; never \
substitute a generic material colour.
15. ATTACHMENT CONTRACT: any component with a parent whose primitive is cylinder, cone, capsule \
or torus, or whose role/name implies handle/limb/tube, MUST include a complete "attachment": \
{parentId, parentSocket, localStart [x,y,z], localEnd [x,y,z], contactType \
("embed"|"overlap"|"flush"), embedDepth > 0 (or overlap > 0), gapTolerance}. Nothing floats in \
mid-air. For an attached cylinder, cone, or capsule, make the localStart-to-localEnd distance \
equal dimensions.height so placement and physical size agree. If you give a component \
actionProfile.sockets, each socket has id, name, position \
[x,y,z], purpose.
16. materials: >=1 entry {id, name, baseColor "#RRGGBB", roughness {base, variation} (or number), \
metalness, colorVariation {palette: >=2 hex colours}, localOverrides: >=1 entry {id, ...}, \
ambientOcclusion {cavityStrength}, optional colorGradient as defined in rule 14}. A component \
gradient must be mirrored onto its referenced material because materials are the rendering unit. \
Colours must be sampled from the image, not invented.
17. viewEvidence: >=1 entry {id, description}. reviewHistory: []. risks: optional.
18. Honesty: a single image cannot reveal hidden sides. Mark unseen geometry as an inference in \
proceduralStrategy notes; never claim it was observed or measured. Use suitability "conditional" when the image is \
ambiguous; never claim detail you cannot see.
EXEMPLAR (a verified strict-passing spec for a mug -- imitate its shape, depth and field style, \
not its content):
"""
def build_system_prompt() -> str:
return RULES + _exemplar()
def build_user_prompt(probe: dict, *, object_hint: str | None = None) -> str:
parts = [
"Author the ObjectSculptSpec JSON for the main object in this reference image.",
"",
"Deterministic image probe (metadata only, not a visual analysis):",
json.dumps({
"type": probe.get("type"),
"width": probe.get("width"),
"height": probe.get("height"),
"megapixels": probe.get("megapixels"),
"warnings": probe.get("warnings"),
}, indent=2),
]
if object_hint:
parts += ["", f"The user says the object is: {object_hint!r}. Honour this unless the image clearly contradicts it."]
parts += [
"",
"Remember: JSON only. Follow the exemplar structure. Match the image's real proportions, "
"colours and parts. targetName must be specific to this object.",
]
return "\n".join(parts)
def build_repair_prompt(errors: list[str], warnings: list[str] | None = None) -> str:
shown = [e for e in errors if isinstance(e, str)][:20]
lines = [
"Your previous spec was REJECTED by the deterministic validator "
"(validate_sculpt_spec.py --strict-quality). Fix every error below and return the "
"complete corrected spec as JSON only:",
"",
]
lines += [f"- {e}" for e in shown]
if warnings:
lines += ["", "Warnings (non-fatal, fix if cheap):"]
lines += [f"- {w}" for w in warnings[:10] if isinstance(w, str)]
lines += [
"",
"Return the ENTIRE corrected JSON object (not a diff). Keep everything that was not "
"flagged. JSON only.",
]
return "\n".join(lines)