us final version
This commit is contained in:
+91
@@ -0,0 +1,91 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from openai import OpenAI
|
||||
from pydantic import BaseModel
|
||||
|
||||
import config
|
||||
from config import SERVOS, HOME, clamp_pose
|
||||
from presets import EXAMPLES
|
||||
|
||||
MAX_STEPS = 16
|
||||
MIN_MS, MAX_MS = 80, 6000
|
||||
|
||||
|
||||
class Step(BaseModel):
|
||||
s1: int
|
||||
s2: int
|
||||
s3: int
|
||||
s4: int
|
||||
ms: int
|
||||
|
||||
|
||||
class Reply(BaseModel):
|
||||
summary: str
|
||||
steps: list[Step]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Plan:
|
||||
summary: str
|
||||
steps: list # [(pose, ms), ...]
|
||||
|
||||
|
||||
def _fmt(steps):
|
||||
return "\n".join(f" {list(pose)} over {ms}ms" for pose, ms in steps)
|
||||
|
||||
|
||||
def _instructions():
|
||||
servos = "\n".join(
|
||||
f"- {s.key} ({s.name}): {s.role}. range {s.lo}-{s.hi}, home {s.home}"
|
||||
for s in SERVOS
|
||||
)
|
||||
examples = "\n\n".join(f'"{name}":\n{_fmt(steps)}' for name, steps in EXAMPLES.items())
|
||||
return f"""You drive a small 4-servo robot arm. Turn the user's spoken command into keyframes.
|
||||
|
||||
Servos (angles in degrees):
|
||||
{servos}
|
||||
|
||||
Each step is an absolute pose [s1, s2, s3, s4] reached by moving all servos together over `ms` milliseconds.
|
||||
Repeat the same pose with a duration to hold still.
|
||||
|
||||
Rules:
|
||||
- Stay inside each servo's range.
|
||||
- {MIN_MS}-{MAX_MS} ms per step, at most {MAX_STEPS} steps. ~4 degrees per 30 ms looks natural; go faster only for snappy moves.
|
||||
- Unless the user asks to hold a position, finish back at home {list(HOME)}.
|
||||
- Relative commands ("a bit more left", "lower") start from the current pose.
|
||||
- If the command has nothing to do with moving the arm, return no steps and say why in the summary.
|
||||
- summary: a few words describing the motion, in the user's language.
|
||||
|
||||
Reference moves:
|
||||
|
||||
{examples}"""
|
||||
|
||||
|
||||
class Planner:
|
||||
def __init__(self):
|
||||
if not config.OPENAI_API_KEY:
|
||||
raise RuntimeError("OPENAI_API_KEY is not set")
|
||||
self._client = OpenAI(api_key=config.OPENAI_API_KEY)
|
||||
|
||||
def plan(self, text, current_pose):
|
||||
kwargs = {}
|
||||
if config.OPENAI_REASONING_EFFORT:
|
||||
kwargs["reasoning"] = {"effort": config.OPENAI_REASONING_EFFORT}
|
||||
|
||||
resp = self._client.responses.parse(
|
||||
model=config.OPENAI_MODEL,
|
||||
instructions=_instructions(),
|
||||
input=f"Current pose: {list(current_pose)}\nCommand: {text}",
|
||||
text_format=Reply,
|
||||
timeout=30,
|
||||
**kwargs,
|
||||
)
|
||||
reply = resp.output_parsed
|
||||
if reply is None:
|
||||
raise RuntimeError("model returned nothing usable")
|
||||
|
||||
steps = [
|
||||
(clamp_pose((s.s1, s.s2, s.s3, s.s4)), max(MIN_MS, min(MAX_MS, s.ms)))
|
||||
for s in reply.steps[:MAX_STEPS]
|
||||
]
|
||||
return Plan(reply.summary.strip(), steps)
|
||||
Reference in New Issue
Block a user