Spaces:
Running
Running
jasondo OpenAI Codex commited on
Commit ·
f24826e
1
Parent(s): 55428b1
Split vision prompt roles
Browse filesCo-authored-by: OpenAI Codex <codex@openai.com>
- AGENTS.md +18 -0
- modal_app.py +67 -33
- snap2sim/prompts.py +73 -59
AGENTS.md
CHANGED
|
@@ -311,6 +311,24 @@ technical cutaway animation.
|
|
| 311 |
real Modal analysis for `piston mechanism` at `0.85` confidence with 3 parts,
|
| 312 |
`/generate_scene` returned `renderer: three`, `render_mode: three`, and no
|
| 313 |
HTML field, and a high confidence threshold returned `render_mode: annotate`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
## Next Work
|
| 316 |
|
|
|
|
| 311 |
real Modal analysis for `piston mechanism` at `0.85` confidence with 3 parts,
|
| 312 |
`/generate_scene` returned `renderer: three`, `render_mode: three`, and no
|
| 313 |
HTML field, and a high confidence threshold returned `render_mode: annotate`.
|
| 314 |
+
- Implemented the `FEATURE4.md` prompt-role split on June 15, 2026: moved the
|
| 315 |
+
invariant analysis contract into `VISION_SYSTEM_PROMPT`, reduced
|
| 316 |
+
`build_vision_prompt()` to a backward-compatible per-image user-prompt alias,
|
| 317 |
+
added `build_vision_messages()`, and wired Modal llama.cpp analysis calls to
|
| 318 |
+
pass `-sys` with a fallback that prepends the system text only if the deployed
|
| 319 |
+
binary rejects the system-prompt flag.
|
| 320 |
+
- Verification after the `FEATURE4.md` pass: prompt-helper checks,
|
| 321 |
+
schema/parser/coercion checks, FastAPI `TestClient` checks for `/`,
|
| 322 |
+
`/analyze_image`, and `/generate_scene`, and a `modal_app.py` import/helper
|
| 323 |
+
check all passed locally. Modal dev raw analysis parsed strict JSON in
|
| 324 |
+
`28.59s` with `coerced_render_mode: three`, and the endpoint-style Modal
|
| 325 |
+
diagnostic returned a validated two-part mechanism payload. Stable Modal
|
| 326 |
+
deploy completed on June 15, 2026, and the stable unauthenticated
|
| 327 |
+
`runtime_probe` request returned `401 Unauthorized`. Authenticated stable
|
| 328 |
+
Modal and private-Space verification could not be run from the local shell
|
| 329 |
+
because `SNAP2SIM_API_TOKEN` / Hugging Face auth tokens were not set locally.
|
| 330 |
+
GitHub push/sync and authenticated private-Space verification still need to
|
| 331 |
+
run before public demo claims rely on the prompt-role split.
|
| 332 |
|
| 333 |
## Next Work
|
| 334 |
|
modal_app.py
CHANGED
|
@@ -17,7 +17,7 @@ import modal
|
|
| 17 |
from fastapi import Header, HTTPException
|
| 18 |
|
| 19 |
from snap2sim.model_io import coerce_analysis_response, parse_analysis_response
|
| 20 |
-
from snap2sim.prompts import
|
| 21 |
from snap2sim.schema import EXAMPLE_ANALYSIS, select_render_mode, validate_analysis
|
| 22 |
|
| 23 |
|
|
@@ -291,6 +291,7 @@ def smoke_test_llamacpp_image() -> dict[str, Any]:
|
|
| 291 |
def run_llamacpp_prompt(
|
| 292 |
prompt: str,
|
| 293 |
image_path: Path | None = None,
|
|
|
|
| 294 |
max_tokens: int = 3072,
|
| 295 |
timeout_seconds: int = 300,
|
| 296 |
ctx_size: int = 8192,
|
|
@@ -299,41 +300,72 @@ def run_llamacpp_prompt(
|
|
| 299 |
import subprocess
|
| 300 |
|
| 301 |
model_path, mmproj_path = ensure_runtime_assets()
|
| 302 |
-
cmd = [
|
| 303 |
-
"/opt/llama.cpp/build/bin/llama-mtmd-cli",
|
| 304 |
-
"-m",
|
| 305 |
-
str(model_path),
|
| 306 |
-
"--mmproj",
|
| 307 |
-
str(mmproj_path),
|
| 308 |
-
"-p",
|
| 309 |
-
prompt,
|
| 310 |
-
"-n",
|
| 311 |
-
str(max_tokens),
|
| 312 |
-
"-c",
|
| 313 |
-
str(ctx_size),
|
| 314 |
-
"--temp",
|
| 315 |
-
"0.2",
|
| 316 |
-
]
|
| 317 |
-
if image_path is not None:
|
| 318 |
-
cmd.extend(["--image", str(image_path)])
|
| 319 |
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
output = "\n".join(part for part in [proc.stdout, proc.stderr] if part).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
if proc.returncode != 0:
|
| 333 |
raise RuntimeError(f"llama.cpp exited with {proc.returncode}: {output[-2000:]}")
|
| 334 |
return output
|
| 335 |
|
| 336 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
def write_payload_image(payload: dict[str, Any]) -> Path:
|
| 338 |
"""Decode an image_base64 payload into a temporary RGB JPEG."""
|
| 339 |
import base64
|
|
@@ -385,9 +417,11 @@ def write_payload_image(payload: dict[str, Any]) -> Path:
|
|
| 385 |
def analyze_image_llamacpp_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
| 386 |
image_path = write_payload_image(payload)
|
| 387 |
try:
|
|
|
|
| 388 |
response = run_llamacpp_prompt(
|
| 389 |
-
|
| 390 |
image_path=image_path,
|
|
|
|
| 391 |
max_tokens=4096,
|
| 392 |
timeout_seconds=300,
|
| 393 |
ctx_size=8192,
|
|
@@ -485,8 +519,6 @@ def runtime_probe(authorization: str = Header(default="")) -> dict[str, Any]:
|
|
| 485 |
@modal.fastapi_endpoint(method="POST")
|
| 486 |
def analyze_image(payload: dict[str, Any], authorization: str = Header(default="")) -> dict[str, Any]:
|
| 487 |
require_authorization(authorization)
|
| 488 |
-
_ = payload.get("image_base64", "")
|
| 489 |
-
_prompt = build_vision_prompt()
|
| 490 |
if runtime_config()["runtime_mode"] != "placeholder":
|
| 491 |
raise NotImplementedError(
|
| 492 |
"Use the analyze_image_llamacpp endpoint for the verified Nemotron "
|
|
@@ -509,9 +541,11 @@ def analyze_image_llamacpp_raw_task(payload: dict[str, Any]) -> dict[str, Any]:
|
|
| 509 |
image_path = write_payload_image(payload)
|
| 510 |
start = time.monotonic()
|
| 511 |
try:
|
|
|
|
| 512 |
response = run_llamacpp_prompt(
|
| 513 |
-
|
| 514 |
image_path=image_path,
|
|
|
|
| 515 |
max_tokens=4096,
|
| 516 |
timeout_seconds=300,
|
| 517 |
ctx_size=8192,
|
|
|
|
| 17 |
from fastapi import Header, HTTPException
|
| 18 |
|
| 19 |
from snap2sim.model_io import coerce_analysis_response, parse_analysis_response
|
| 20 |
+
from snap2sim.prompts import build_vision_messages
|
| 21 |
from snap2sim.schema import EXAMPLE_ANALYSIS, select_render_mode, validate_analysis
|
| 22 |
|
| 23 |
|
|
|
|
| 291 |
def run_llamacpp_prompt(
|
| 292 |
prompt: str,
|
| 293 |
image_path: Path | None = None,
|
| 294 |
+
system_prompt: str | None = None,
|
| 295 |
max_tokens: int = 3072,
|
| 296 |
timeout_seconds: int = 300,
|
| 297 |
ctx_size: int = 8192,
|
|
|
|
| 300 |
import subprocess
|
| 301 |
|
| 302 |
model_path, mmproj_path = ensure_runtime_assets()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
+
def build_cmd(user_prompt: str, system: str | None) -> list[str]:
|
| 305 |
+
cmd = [
|
| 306 |
+
"/opt/llama.cpp/build/bin/llama-mtmd-cli",
|
| 307 |
+
"-m",
|
| 308 |
+
str(model_path),
|
| 309 |
+
"--mmproj",
|
| 310 |
+
str(mmproj_path),
|
| 311 |
+
]
|
| 312 |
+
if system:
|
| 313 |
+
cmd.extend(["-sys", system])
|
| 314 |
+
if image_path is not None:
|
| 315 |
+
cmd.extend(["--image", str(image_path)])
|
| 316 |
+
cmd.extend(
|
| 317 |
+
[
|
| 318 |
+
"-p",
|
| 319 |
+
user_prompt,
|
| 320 |
+
"-n",
|
| 321 |
+
str(max_tokens),
|
| 322 |
+
"-c",
|
| 323 |
+
str(ctx_size),
|
| 324 |
+
"--temp",
|
| 325 |
+
"0.2",
|
| 326 |
+
]
|
| 327 |
+
)
|
| 328 |
+
return cmd
|
| 329 |
+
|
| 330 |
+
def run_cmd(cmd: list[str]) -> subprocess.CompletedProcess[str]:
|
| 331 |
+
try:
|
| 332 |
+
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_seconds)
|
| 333 |
+
except subprocess.TimeoutExpired as exc:
|
| 334 |
+
partial_output = "\n".join(
|
| 335 |
+
part.decode("utf-8", errors="replace") if isinstance(part, bytes) else part
|
| 336 |
+
for part in [exc.stdout, exc.stderr]
|
| 337 |
+
if part
|
| 338 |
+
).strip()
|
| 339 |
+
raise TimeoutError(
|
| 340 |
+
f"llama.cpp timed out after {timeout_seconds}s: {partial_output[-2000:]}"
|
| 341 |
+
) from exc
|
| 342 |
+
|
| 343 |
+
proc = run_cmd(build_cmd(prompt, system_prompt))
|
| 344 |
output = "\n".join(part for part in [proc.stdout, proc.stderr] if part).strip()
|
| 345 |
+
if proc.returncode != 0 and system_prompt and _system_prompt_flag_unsupported(output):
|
| 346 |
+
combined_prompt = f"{system_prompt.strip()}\n\nUser request:\n{prompt}"
|
| 347 |
+
proc = run_cmd(build_cmd(combined_prompt, None))
|
| 348 |
+
output = "\n".join(part for part in [proc.stdout, proc.stderr] if part).strip()
|
| 349 |
if proc.returncode != 0:
|
| 350 |
raise RuntimeError(f"llama.cpp exited with {proc.returncode}: {output[-2000:]}")
|
| 351 |
return output
|
| 352 |
|
| 353 |
|
| 354 |
+
def _system_prompt_flag_unsupported(output: str) -> bool:
|
| 355 |
+
lowered = output.lower()
|
| 356 |
+
return any(
|
| 357 |
+
marker in lowered
|
| 358 |
+
for marker in [
|
| 359 |
+
"unknown argument: -sys",
|
| 360 |
+
"unknown argument '-sys'",
|
| 361 |
+
"unknown option: -sys",
|
| 362 |
+
"unrecognized option '-sys'",
|
| 363 |
+
"invalid option -- 'sys'",
|
| 364 |
+
"error: unknown argument",
|
| 365 |
+
]
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
|
| 369 |
def write_payload_image(payload: dict[str, Any]) -> Path:
|
| 370 |
"""Decode an image_base64 payload into a temporary RGB JPEG."""
|
| 371 |
import base64
|
|
|
|
| 417 |
def analyze_image_llamacpp_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
| 418 |
image_path = write_payload_image(payload)
|
| 419 |
try:
|
| 420 |
+
system_prompt, user_prompt = build_vision_messages()
|
| 421 |
response = run_llamacpp_prompt(
|
| 422 |
+
user_prompt,
|
| 423 |
image_path=image_path,
|
| 424 |
+
system_prompt=system_prompt,
|
| 425 |
max_tokens=4096,
|
| 426 |
timeout_seconds=300,
|
| 427 |
ctx_size=8192,
|
|
|
|
| 519 |
@modal.fastapi_endpoint(method="POST")
|
| 520 |
def analyze_image(payload: dict[str, Any], authorization: str = Header(default="")) -> dict[str, Any]:
|
| 521 |
require_authorization(authorization)
|
|
|
|
|
|
|
| 522 |
if runtime_config()["runtime_mode"] != "placeholder":
|
| 523 |
raise NotImplementedError(
|
| 524 |
"Use the analyze_image_llamacpp endpoint for the verified Nemotron "
|
|
|
|
| 541 |
image_path = write_payload_image(payload)
|
| 542 |
start = time.monotonic()
|
| 543 |
try:
|
| 544 |
+
system_prompt, user_prompt = build_vision_messages()
|
| 545 |
response = run_llamacpp_prompt(
|
| 546 |
+
user_prompt,
|
| 547 |
image_path=image_path,
|
| 548 |
+
system_prompt=system_prompt,
|
| 549 |
max_tokens=4096,
|
| 550 |
timeout_seconds=300,
|
| 551 |
ctx_size=8192,
|
snap2sim/prompts.py
CHANGED
|
@@ -4,67 +4,81 @@ from __future__ import annotations
|
|
| 4 |
|
| 5 |
VISION_SYSTEM_PROMPT = """You are a mechanical teardown analyst for an annotated
|
| 6 |
technical cutaway demo. Reason briefly if useful, then emit exactly one JSON
|
| 7 |
-
object
|
| 8 |
If the photo is ambiguous, lower confidence and use visible photo annotations
|
| 9 |
-
instead of forcing speculative 3D geometry.
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
return (
|
| 14 |
-
"Analyze the
|
| 15 |
-
"
|
| 16 |
-
"Required top-level keys: component, confidence, summary, trigger, "
|
| 17 |
-
"motion_sequence, parts. Optional top-level render_mode is three, "
|
| 18 |
-
"annotate, or unavailable.\n"
|
| 19 |
-
"Each part requires: id, name, role, and either geometry plus motion, "
|
| 20 |
-
"or annotation when the visible component can be located but 3D "
|
| 21 |
-
"geometry is uncertain.\n"
|
| 22 |
-
"Shapes, with size always [x, y, z] extents:\n"
|
| 23 |
-
"- box: plates, housings, blocks, levers, selectors\n"
|
| 24 |
-
"- cylinder: shafts, sleeves, bushings, drums, pins\n"
|
| 25 |
-
"- cone: valve cones, tips, nozzles, tapers\n"
|
| 26 |
-
"- capsule: pistons, rollers, dowel pins, plungers, bearings\n"
|
| 27 |
-
"- sphere: balls, detents, ball bearings, nodes\n"
|
| 28 |
-
"- rod: links, tie rods, thin axles, connecting rods\n"
|
| 29 |
-
"- gear: toothed wheels, ratchets, cogs; set teeth when useful\n"
|
| 30 |
-
"- torus: o-rings, snap rings, seals, washers, single coils\n"
|
| 31 |
-
"- spring: helical springs and coils; set coils when useful\n\n"
|
| 32 |
-
"Motions, with axis as a numeric vector like [0, 1, 0]:\n"
|
| 33 |
-
"- static: fixed structure or housing\n"
|
| 34 |
-
"- rotate: continuous spin; use speed\n"
|
| 35 |
-
"- oscillate: sinusoidal twist; use amplitude and speed\n"
|
| 36 |
-
"- translate: slide along axis; use range [min, max]\n"
|
| 37 |
-
"- screw: spin plus advance along axis; use pitch for helical action\n"
|
| 38 |
-
"- orbit: revolve around pivot [x, y, z]\n"
|
| 39 |
-
"- pulse: scale breathing for diaphragms, springs, valves, pumps\n\n"
|
| 40 |
-
"Every geometry must use size: [x, y, z] and position: [x, y, z]. "
|
| 41 |
-
"Do not use radius, height, length, width, or depth fields. Every "
|
| 42 |
-
"motion axis must be a numeric vector such as [0, 1, 0], never a string "
|
| 43 |
-
"like x, y, or z.\n\n"
|
| 44 |
-
"Use 2 to 6 parts; prefer the fewest that explain the mechanism. "
|
| 45 |
-
"Keep names and descriptions short. When possible, "
|
| 46 |
-
"include annotation.point in normalized image coordinates [x, y] with "
|
| 47 |
-
"origin at top-left, plus a short annotation.note. Optional "
|
| 48 |
-
"annotation.box is [x, y, width, height], also normalized from 0 to 1.\n\n"
|
| 49 |
-
"Use this compact shape:\n"
|
| 50 |
-
"{\n"
|
| 51 |
-
' "component": "short component name",\n'
|
| 52 |
-
' "confidence": 0.7,\n'
|
| 53 |
-
' "summary": "one or two sentences",\n'
|
| 54 |
-
' "trigger": "what starts the mechanism",\n'
|
| 55 |
-
' "motion_sequence": ["step one", "step two"],\n'
|
| 56 |
-
' "parts": [\n'
|
| 57 |
-
" {\n"
|
| 58 |
-
' "id": "part_id",\n'
|
| 59 |
-
' "name": "part name",\n'
|
| 60 |
-
' "role": "mechanical role",\n'
|
| 61 |
-
' "geometry": {"shape": "spring", "size": [0.4, 1.2, 0.4], "position": [0, 0, 0], "coils": 6},\n'
|
| 62 |
-
' "motion": {"type": "pulse", "speed": 2, "amplitude": 0.08},\n'
|
| 63 |
-
' "annotation": {"point": [0.5, 0.5], "label": "visible label", "note": "short visible clue"}\n'
|
| 64 |
-
" }\n"
|
| 65 |
-
" ]\n"
|
| 66 |
-
"}\n\n"
|
| 67 |
-
"Optional geometry fields: rotation, teeth, coils, wire, color. Optional "
|
| 68 |
-
"motion fields: axis, speed, amplitude, phase, range, pitch, pivot. "
|
| 69 |
-
"Include optional fields only when useful."
|
| 70 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
VISION_SYSTEM_PROMPT = """You are a mechanical teardown analyst for an annotated
|
| 6 |
technical cutaway demo. Reason briefly if useful, then emit exactly one JSON
|
| 7 |
+
object with no markdown. Prefer the simplest truthful primitive mechanism.
|
| 8 |
If the photo is ambiguous, lower confidence and use visible photo annotations
|
| 9 |
+
instead of forcing speculative 3D geometry.
|
| 10 |
|
| 11 |
+
Required top-level keys: component, confidence, summary, trigger,
|
| 12 |
+
motion_sequence, parts. Optional top-level render_mode is three, annotate, or
|
| 13 |
+
unavailable.
|
| 14 |
+
Each part requires: id, name, role, and either geometry plus motion, or
|
| 15 |
+
annotation when the visible component can be located but 3D geometry is
|
| 16 |
+
uncertain.
|
| 17 |
|
| 18 |
+
Shapes, with size always [x, y, z] extents:
|
| 19 |
+
- box: plates, housings, blocks, levers, selectors
|
| 20 |
+
- cylinder: shafts, sleeves, bushings, drums, pins
|
| 21 |
+
- cone: valve cones, tips, nozzles, tapers
|
| 22 |
+
- capsule: pistons, rollers, dowel pins, plungers, bearings
|
| 23 |
+
- sphere: balls, detents, ball bearings, nodes
|
| 24 |
+
- rod: links, tie rods, thin axles, connecting rods
|
| 25 |
+
- gear: toothed wheels, ratchets, cogs; set teeth when useful
|
| 26 |
+
- torus: o-rings, snap rings, seals, washers, single coils
|
| 27 |
+
- spring: helical springs and coils; set coils when useful
|
| 28 |
+
|
| 29 |
+
Motions, with axis as a numeric vector like [0, 1, 0]:
|
| 30 |
+
- static: fixed structure or housing
|
| 31 |
+
- rotate: continuous spin; use speed
|
| 32 |
+
- oscillate: sinusoidal twist; use amplitude and speed
|
| 33 |
+
- translate: slide along axis; use range [min, max]
|
| 34 |
+
- screw: spin plus advance along axis; use pitch for helical action
|
| 35 |
+
- orbit: revolve around pivot [x, y, z]
|
| 36 |
+
- pulse: scale breathing for diaphragms, springs, valves, pumps
|
| 37 |
+
|
| 38 |
+
Every geometry must use size: [x, y, z] and position: [x, y, z]. Do not use
|
| 39 |
+
radius, height, length, width, or depth fields. Every motion axis must be a
|
| 40 |
+
numeric vector such as [0, 1, 0], never a string like x, y, or z.
|
| 41 |
+
|
| 42 |
+
Use 2 to 6 parts; prefer the fewest that explain the mechanism. Keep names and
|
| 43 |
+
descriptions short. When possible, include annotation.point in normalized image
|
| 44 |
+
coordinates [x, y] with origin at top-left, plus a short annotation.note.
|
| 45 |
+
Optional annotation.box is [x, y, width, height], also normalized from 0 to 1.
|
| 46 |
+
|
| 47 |
+
Use this compact shape:
|
| 48 |
+
{
|
| 49 |
+
"component": "short component name",
|
| 50 |
+
"confidence": 0.7,
|
| 51 |
+
"summary": "one or two sentences",
|
| 52 |
+
"trigger": "what starts the mechanism",
|
| 53 |
+
"motion_sequence": ["step one", "step two"],
|
| 54 |
+
"parts": [
|
| 55 |
+
{
|
| 56 |
+
"id": "part_id",
|
| 57 |
+
"name": "part name",
|
| 58 |
+
"role": "mechanical role",
|
| 59 |
+
"geometry": {"shape": "spring", "size": [0.4, 1.2, 0.4], "position": [0, 0, 0], "coils": 6},
|
| 60 |
+
"motion": {"type": "pulse", "speed": 2, "amplitude": 0.08},
|
| 61 |
+
"annotation": {"point": [0.5, 0.5], "label": "visible label", "note": "short visible clue"}
|
| 62 |
+
}
|
| 63 |
+
]
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
Optional geometry fields: rotation, teeth, coils, wire, color. Optional motion
|
| 67 |
+
fields: axis, speed, amplitude, phase, range, pitch, pivot. Include optional
|
| 68 |
+
fields only when useful."""
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def build_vision_user_prompt() -> str:
|
| 72 |
return (
|
| 73 |
+
"Analyze the hardware component in this photo as a cutaway mechanism. "
|
| 74 |
+
"Return the analysis JSON for the visible component."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def build_vision_prompt() -> str:
|
| 79 |
+
"""Backward-compatible alias for the per-image user prompt."""
|
| 80 |
+
return build_vision_user_prompt()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def build_vision_messages() -> tuple[str, str]:
|
| 84 |
+
return VISION_SYSTEM_PROMPT, build_vision_user_prompt()
|