Spaces:
Running
Running
Commit ·
c46fef8
1
Parent(s): 0bcd0b1
refactor(server): migrate demo routes to server/ task+env API
Browse filesReplace env/ environment with server/origami_environment.OrigamiEnvironment.
Switch /targets to server/tasks.available_task_names with difficulty/material
metadata. Replace /episode/run with /episode/demo using new DEMO_SEQUENCES
format (type/line/angle) and new OrigamiAction model. Remove old parse_fold_list
completion string approach.
- openenv_server/app.py +78 -85
openenv_server/app.py
CHANGED
|
@@ -19,123 +19,116 @@ app = create_app(
|
|
| 19 |
|
| 20 |
|
| 21 |
# ---------------------------------------------------------------------------
|
| 22 |
-
# Demo
|
| 23 |
-
# These must be registered BEFORE the StaticFiles catch-all mount.
|
| 24 |
# ---------------------------------------------------------------------------
|
| 25 |
|
| 26 |
-
|
| 27 |
-
"
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
"
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
@app.get("/targets", include_in_schema=True)
|
| 39 |
def get_targets() -> dict:
|
| 40 |
-
"""Return available
|
| 41 |
-
from
|
| 42 |
|
| 43 |
-
env = OrigamiEnvironment()
|
| 44 |
result: dict[str, dict] = {}
|
| 45 |
-
for name in
|
| 46 |
-
t =
|
| 47 |
result[name] = {
|
| 48 |
"name": name,
|
| 49 |
-
"level": t.get("
|
| 50 |
"description": t.get("description", ""),
|
| 51 |
-
"n_creases":
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
return result
|
| 54 |
|
| 55 |
|
| 56 |
-
@app.get("/episode/
|
| 57 |
-
def
|
| 58 |
-
"""
|
| 59 |
-
from
|
| 60 |
-
from
|
| 61 |
-
from
|
| 62 |
-
|
| 63 |
-
env = OrigamiEnvironment(mode="step")
|
| 64 |
-
obs = env.reset(target_name=target)
|
| 65 |
|
| 66 |
-
if not
|
| 67 |
-
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
except ValueError as exc:
|
| 72 |
-
return {"error": str(exc), "steps": []}
|
| 73 |
|
| 74 |
steps: list[dict] = []
|
| 75 |
-
for i, fold in enumerate(folds):
|
| 76 |
-
result = env.paper.add_crease(fold["from"], fold["to"], fold["assignment"])
|
| 77 |
-
reward = compute_reward(env.paper, result, env.target)
|
| 78 |
-
|
| 79 |
-
paper_state = {
|
| 80 |
-
"vertices": {str(k): list(v) for k, v in env.paper.graph.vertices.items()},
|
| 81 |
-
"edges": [
|
| 82 |
-
{
|
| 83 |
-
"id": k,
|
| 84 |
-
"v1": list(env.paper.graph.vertices[v[0]]),
|
| 85 |
-
"v2": list(env.paper.graph.vertices[v[1]]),
|
| 86 |
-
"assignment": v[2],
|
| 87 |
-
}
|
| 88 |
-
for k, v in env.paper.graph.edges.items()
|
| 89 |
-
],
|
| 90 |
-
"anchor_points": [list(p) for p in env.paper.anchor_points()],
|
| 91 |
-
}
|
| 92 |
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
step=i + 1,
|
| 97 |
-
max_steps=env.max_steps,
|
| 98 |
-
last_reward=reward,
|
| 99 |
-
)
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
"from_point": fold["from"],
|
| 106 |
-
"to_point": fold["to"],
|
| 107 |
-
"assignment": fold["assignment"],
|
| 108 |
-
"instruction": fold.get("instruction", ""),
|
| 109 |
-
},
|
| 110 |
-
"paper_state": paper_state,
|
| 111 |
-
"anchor_points": [list(p) for p in env.paper.anchor_points()],
|
| 112 |
-
"reward": reward,
|
| 113 |
-
"done": reward.get("completion", 0) > 0,
|
| 114 |
-
"info": env._info(),
|
| 115 |
-
"prompt": step_prompt,
|
| 116 |
-
}
|
| 117 |
)
|
| 118 |
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
break
|
| 121 |
|
|
|
|
|
|
|
| 122 |
return {
|
| 123 |
-
"
|
| 124 |
-
"
|
| 125 |
"steps": steps,
|
| 126 |
-
"
|
| 127 |
}
|
| 128 |
|
| 129 |
|
| 130 |
-
@app.get("/episode/demo", include_in_schema=True)
|
| 131 |
-
def demo_episode(target: str = "half_horizontal") -> dict:
|
| 132 |
-
"""Return a pre-solved demo episode for the given target."""
|
| 133 |
-
completion = DEMO_COMPLETIONS.get(target, DEMO_COMPLETIONS["half_horizontal"])
|
| 134 |
-
return run_episode(target=target, completion=completion)
|
| 135 |
-
|
| 136 |
-
|
| 137 |
# ---------------------------------------------------------------------------
|
| 138 |
-
# Static file serving — must come LAST so API routes take priority
|
| 139 |
# ---------------------------------------------------------------------------
|
| 140 |
|
| 141 |
_BUILD_DIR = Path(__file__).resolve().parent.parent / "build"
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
# ---------------------------------------------------------------------------
|
| 22 |
+
# Demo fold sequences — new format: type, line {start, end}, angle
|
|
|
|
| 23 |
# ---------------------------------------------------------------------------
|
| 24 |
|
| 25 |
+
DEMO_SEQUENCES: dict[str, list[dict]] = {
|
| 26 |
+
"half_fold": [
|
| 27 |
+
{"type": "valley", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
|
| 28 |
+
],
|
| 29 |
+
"quarter_fold": [
|
| 30 |
+
{"type": "valley", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
|
| 31 |
+
{"type": "valley", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
|
| 32 |
+
],
|
| 33 |
+
"letter_fold": [
|
| 34 |
+
{"type": "valley", "line": {"start": [0.0, 0.333], "end": [1.0, 0.333]}, "angle": 180.0},
|
| 35 |
+
{"type": "mountain", "line": {"start": [0.0, 0.667], "end": [1.0, 0.667]}, "angle": 180.0},
|
| 36 |
+
],
|
| 37 |
+
"map_fold": [
|
| 38 |
+
{"type": "valley", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
|
| 39 |
+
{"type": "mountain", "line": {"start": [0.5, 0.0], "end": [0.5, 1.0]}, "angle": 180.0},
|
| 40 |
+
],
|
| 41 |
+
"solar_panel": [
|
| 42 |
+
{"type": "valley", "line": {"start": [0.0, 0.25], "end": [1.0, 0.25]}, "angle": 180.0},
|
| 43 |
+
{"type": "mountain", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
|
| 44 |
+
{"type": "valley", "line": {"start": [0.0, 0.75], "end": [1.0, 0.75]}, "angle": 180.0},
|
| 45 |
+
],
|
| 46 |
+
"shelter_wall": [
|
| 47 |
+
{"type": "valley", "line": {"start": [0.0, 0.333], "end": [1.0, 0.333]}, "angle": 180.0},
|
| 48 |
+
{"type": "valley", "line": {"start": [0.0, 0.667], "end": [1.0, 0.667]}, "angle": 180.0},
|
| 49 |
+
],
|
| 50 |
+
"stent": [
|
| 51 |
+
{"type": "valley", "line": {"start": [0.0, 0.25], "end": [1.0, 0.25]}, "angle": 90.0},
|
| 52 |
+
{"type": "mountain", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 90.0},
|
| 53 |
+
{"type": "valley", "line": {"start": [0.0, 0.75], "end": [1.0, 0.75]}, "angle": 90.0},
|
| 54 |
+
{"type": "stop", "line": {"start": [0.0, 0.0], "end": [1.0, 1.0]}, "angle": 0.0},
|
| 55 |
+
],
|
| 56 |
}
|
| 57 |
|
| 58 |
|
| 59 |
+
# ---------------------------------------------------------------------------
|
| 60 |
+
# API routes — must be registered BEFORE the StaticFiles catch-all mount
|
| 61 |
+
# ---------------------------------------------------------------------------
|
| 62 |
+
|
| 63 |
@app.get("/targets", include_in_schema=True)
|
| 64 |
def get_targets() -> dict:
|
| 65 |
+
"""Return available task names and metadata for the frontend."""
|
| 66 |
+
from server.tasks import get_task_by_name, available_task_names
|
| 67 |
|
|
|
|
| 68 |
result: dict[str, dict] = {}
|
| 69 |
+
for name in available_task_names():
|
| 70 |
+
t = get_task_by_name(name)
|
| 71 |
result[name] = {
|
| 72 |
"name": name,
|
| 73 |
+
"level": t.get("difficulty", 1),
|
| 74 |
"description": t.get("description", ""),
|
| 75 |
+
"n_creases": t.get("max_folds", 3),
|
| 76 |
+
"difficulty": t.get("difficulty", 1),
|
| 77 |
+
"material": t.get("material", "paper"),
|
| 78 |
}
|
| 79 |
return result
|
| 80 |
|
| 81 |
|
| 82 |
+
@app.get("/episode/demo", include_in_schema=True)
|
| 83 |
+
def demo_episode(target: str = "half_fold") -> dict:
|
| 84 |
+
"""Return a pre-solved demo episode for the given task."""
|
| 85 |
+
from server.origami_environment import OrigamiEnvironment
|
| 86 |
+
from server.models import OrigamiAction as NewOrigamiAction
|
| 87 |
+
from server.tasks import get_task_by_name
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
+
# Fall back to half_fold if target not found
|
| 90 |
+
folds = DEMO_SEQUENCES.get(target, DEMO_SEQUENCES["half_fold"])
|
| 91 |
|
| 92 |
+
env = OrigamiEnvironment()
|
| 93 |
+
obs = env.reset(task_name=target)
|
|
|
|
|
|
|
| 94 |
|
| 95 |
steps: list[dict] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
+
for i, fold_dict in enumerate(folds):
|
| 98 |
+
if fold_dict.get("type") == "stop":
|
| 99 |
+
break
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
+
action = NewOrigamiAction(
|
| 102 |
+
fold_type=fold_dict["type"],
|
| 103 |
+
fold_line=fold_dict["line"],
|
| 104 |
+
fold_angle=float(fold_dict.get("angle", 180.0)),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
)
|
| 106 |
|
| 107 |
+
obs = env.step(action)
|
| 108 |
+
|
| 109 |
+
steps.append({
|
| 110 |
+
"step": i + 1,
|
| 111 |
+
"fold": fold_dict,
|
| 112 |
+
"paper_state": obs.paper_state,
|
| 113 |
+
"metrics": obs.metrics,
|
| 114 |
+
"done": obs.done,
|
| 115 |
+
})
|
| 116 |
+
|
| 117 |
+
if obs.done:
|
| 118 |
break
|
| 119 |
|
| 120 |
+
task_def = get_task_by_name(target) if target else {}
|
| 121 |
+
|
| 122 |
return {
|
| 123 |
+
"task_name": target,
|
| 124 |
+
"task": task_def,
|
| 125 |
"steps": steps,
|
| 126 |
+
"final_metrics": obs.metrics if steps else {},
|
| 127 |
}
|
| 128 |
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
# ---------------------------------------------------------------------------
|
| 131 |
+
# Static file serving — must come LAST so API routes take priority
|
| 132 |
# ---------------------------------------------------------------------------
|
| 133 |
|
| 134 |
_BUILD_DIR = Path(__file__).resolve().parent.parent / "build"
|