Spaces:
Running
Running
| """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" | |
| 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) | |