us final version

This commit is contained in:
2026-08-12 13:15:11 +09:00
commit 75e9bb8cf4
10 changed files with 947 additions and 0 deletions
+91
View File
@@ -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)