Generative Movement Design System for Physical AI
PURE는 Physical AI(물리적 형태를 가진 로봇·사물)의 움직임을 하나의 커뮤니케이션 언어로 설계하는 시스템입니다. 움직임을 다음 흐름으로 다룹니다.
Context(맥락) → State(상태) → Movement(움직임) → Function(기능)
먼저 제품(역할)을 선택하고(아키타입 선택 또는 자사 3D 파일 업로드), 상황이나 감정을 입력하면 — 시스템은 제품을 바꾸지 않고 그 제품이 가져야 할 내부 상태(state) 와 그 상태가 드러나는 구체적인 움직임·인터랙션(interaction) 을 추론합니다. 이로부터 모션 토큰과 3D 움직임 시뮬레이션이 만들어집니다.
라이브 데모: https://pure-omega.vercel.app
- Guideline (
/guideline) — Foundation / Semantic / Components / Patterns로 구성된 디자인 시스템 문서와 상태별 상세 페이지. - Lab (
/lab) — 실험 워크플로:/lab/archetype— 6가지 아키타입 중 선택 → 프리미티브/세션 형태 설정/lab/custom— GLB/OBJ/STL 업로드 → 바운딩 박스 기반 추상화/lab/studio— 통합 경험: 상태 입력 → 상태 슬라이더 → 모션 토큰 → 실시간 3D 시뮬레이션/lab/report— 세션 기반 리포트 + PDF 출력
- AI 추론 (
/api/analyze) — 이미 선택된 제품(formId) 을 입력으로 받아, OpenAI로 상황/감정 텍스트를 분석해 그 제품의 5차원 상태 벡터(curiosity / attention / confidence / comfort / energy)와 구체적인 움직임(interaction) 을 추론합니다. 제품은 절대 바꾸지 않습니다. API 키가 없거나 요청이 실패하면 로컬 휴리스틱으로 자동 폴백합니다.
- Next.js 15 (App Router) · React 19 · TypeScript
- Tailwind CSS v4
- three.js · @react-three/fiber · drei
- OpenAI API (상태·움직임 추론)
- Vercel (배포)
npm install.env.example을 복사해 .env.local을 만들고 OpenAI 키를 입력합니다.
cp .env.example .env.local# .env.local
OPENAI_API_KEY=sk-... # 필수: 정밀 추론에 사용
# OPENAI_MODEL=gpt-4o-mini # 선택: 기본값 gpt-4o-mini키가 없어도 앱은 로컬 휴리스틱 폴백으로 동작합니다. 키를 추가/변경한 뒤에는 dev 서버를 재시작하세요.
npm run devhttp://localhost:3000 에서 확인합니다. (dev 서버는 Turbopack을 사용합니다.)
npm run build
npm run startapp/
api/analyze/route.ts # OpenAI 추론 엔드포인트 (고정 제품 → 상태 + 움직임)
guideline/ # 디자인 시스템 문서
lab/ # 실험 워크플로 (archetype/custom → studio → report)
session.tsx # 세션 상태 (shape + formId + state, localStorage)
components/ # 재사용 UI (StateInput, MotionSimulator, ui 등)
lib/ # 순수 로직
ai-analyze.ts # 분석 타입 · 클라이언트 wrapper · 로컬 폴백
forms.ts # 6가지 Physical AI 폼(역할) 정의
state-generator.ts # 상태 차원 · 텍스트/픽셀 휴리스틱
motion-engine.ts # 상태 → 모션 합성
motion-tokens.ts # 모션 토큰
report.ts # 리포트 생성
- Lab에서 제품(역할)을 먼저 선택합니다 — 6가지 아키타입(
companion · humanoid · lamp · mobility · animal · ambient) 중 하나를 고르거나, 자사 3D 파일을 업로드해 추상화합니다. - Studio에서 상황·감정을 텍스트로 입력하고 상태 생성을 누릅니다.
/api/analyze가 선택된 제품(formId)을 입력으로 받아 OpenAI를 호출(JSON schema 구조화 출력)하고 다음을 반환합니다. 제품은 바꾸지 않습니다.state— 그 제품이 맥락에서 가져야 할 5차원 상태 벡터 (각 0~1)interaction— 그 상태가 드러나는 구체적인 움직임 한 문장 (예: "반가워서 제자리를 빠르게 맴돈다.")reasoning— 추론 근거(한국어)
- 상태가 세션에 반영되어 3D 시뮬레이터·모션 토큰이 즉시 갱신되고, "추론된 움직임" 패널에 움직임 설명과 근거가 표시됩니다.
선택된 제품(formId)과 상황/감정 입력을 받아, 그 제품의 5차원 상태 벡터와 구체적인 움직임을 반환합니다.
요청 본문
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
text |
string |
text·images 중 하나 |
상황·감정 텍스트 |
images |
string[] |
text·images 중 하나 |
data:image/... 형식의 base64 이미지. 여러 장이면 15초 영상의 연속 프레임으로 해석 |
formId |
"companion" | "humanoid" | "lamp" | "mobility" | "animal" | "ambient" |
선택 | 제품(역할). 기본값 "companion" |
text와images둘 다 없으면400, 서버에OPENAI_API_KEY가 없으면503을 반환합니다. (클라이언트 헬퍼analyzeInput/analyzeImages는 이 실패를 잡아 로컬 휴리스틱으로 폴백합니다 — 아래 2번 참고.)
curl
curl -X POST http://localhost:3000/api/analyze \
-H "Content-Type: application/json" \
-d '{
"text": "사용자가 오랜만에 반갑게 인사를 건넨다",
"formId": "companion"
}'응답 (Analysis)
fetch (브라우저 / 클라이언트 컴포넌트)
const res = await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: "조용히 집중해서 책을 읽는다", formId: "lamp" }),
});
const analysis = await res.json(); // → Analysis
console.log(analysis.state, analysis.interaction);이미지/웹캠 프레임으로 추론하려면 images에 data:image/... 문자열 배열을 보냅니다.
await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
images: ["data:image/jpeg;base64,/9j/4AAQ..."], // 여러 장이면 15초 프레임 시퀀스로 처리
formId: "animal",
}),
});API를 거치지 않고 lib의 순수 함수를 그대로 쓸 수 있습니다. (경로 alias @/ → 저장소 루트)
클라이언트 래퍼 — 자동 폴백 포함
import { analyzeInput } from "@/lib/ai-analyze";
// /api/analyze 호출 → 실패(키 없음/네트워크 오류) 시 로컬 휴리스틱으로 투명하게 폴백
const analysis = await analyzeInput("신나서 방 안을 빠르게 돈다", "mobility");
// ↳ Analysis: { formId, role, state, interaction, reasoning, fallback }API 없이 — 텍스트 → 상태 → 모션 토큰 (Node/서버에서도 동작)
import { analyzeText } from "@/lib/state-generator";
import { composeMotion } from "@/lib/motion-engine";
import { localInteraction } from "@/lib/ai-analyze";
const state = analyzeText("calm and focused"); // 키워드 휴리스틱 → StateVector
// 예시 결과: { curiosity: 0.31, attention: 0.49, confidence: 0.45, comfort: 0.83, energy: 0.27 }
const motion = composeMotion(state);
// motion.properties → [{ id: "amplitude", label: "Amplitude", value: 0.43 }, ...]
// motion.tokens → [{ category: "Direction", token: "Approach" }, { category: "Timing", token: "Pause" }, ...]
const sentence = localInteraction("companion", state);
// → "Companion feels at ease and moves closer calmly."제품(폼) 정의 조회
import { FORMS, getForm, suggestForm } from "@/lib/forms";
getForm("lamp")?.label; // → "Lamp"
FORMS.map((form) => form.id); // → ["companion", "humanoid", "lamp", "mobility", "animal", "ambient"]
suggestForm({ x: 0.3, y: 1.8, z: 0.3 }); // 바운딩 박스 비율 → "humanoid"composeMotion 결과를 모션 타임라인과 액추에이터 제약(DOF·각속도·제어 주기 등)으로 변환합니다.
import { composeMotion } from "@/lib/motion-engine";
import { buildTimeline, buildConstraints, periodMs } from "@/lib/report";
import { analyzeText } from "@/lib/state-generator";
const motion = composeMotion(analyzeText("excited and playful"));
periodMs(motion); // → 한 모션 사이클 길이(ms)
buildTimeline(motion); // → [{ label: "Move", fromMs, toMs, token, description }, ...]
buildConstraints(motion); // → [{ label: "Degrees of Freedom (DOF)", value: "1", note: "Yaw (Y, continuous)" }, ...]본 저장소는 졸업 프로젝트로 제작되었습니다.
{ "formId": "companion", "role": "Companion", "state": { "curiosity": 0.82, "attention": 0.66, "confidence": 0.54, "comfort": 0.61, "energy": 0.78 }, "interaction": "Spins in place quickly out of delight.", "reasoning": "반가운 재회 맥락이라 각성도(energy)와 호기심(curiosity)을 높게 잡고...", "fallback": false // 로컬 휴리스틱으로 폴백되면 true }