File size: 10,751 Bytes
39ff632
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37e3d5a
39ff632
 
 
 
 
 
 
37e3d5a
39ff632
 
 
 
 
 
 
37e3d5a
 
39ff632
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37e3d5a
 
 
 
 
 
8d7dfcd
 
 
 
 
 
 
 
37e3d5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39ff632
 
 
a35b2b8
 
 
39ff632
37e3d5a
39ff632
37e3d5a
 
 
 
 
39ff632
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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)