File size: 5,619 Bytes
19abe39
 
a2b208a
 
 
 
19abe39
 
 
 
 
 
 
 
 
 
 
 
a2b208a
85a3e59
 
c46fef8
85a3e59
 
c46fef8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85a3e59
 
 
c46fef8
 
 
 
85a3e59
 
c46fef8
 
85a3e59
 
c46fef8
 
85a3e59
 
c46fef8
85a3e59
c46fef8
 
 
85a3e59
 
 
 
c46fef8
 
 
 
 
 
85a3e59
c46fef8
 
85a3e59
c46fef8
 
85a3e59
 
 
c46fef8
 
 
85a3e59
c46fef8
 
 
 
85a3e59
 
c46fef8
 
 
 
 
 
 
 
 
 
 
85a3e59
 
c46fef8
 
85a3e59
c46fef8
 
85a3e59
c46fef8
85a3e59
 
 
 
c46fef8
85a3e59
 
a2b208a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
from __future__ import annotations

from pathlib import Path

from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from openenv.core.env_server.http_server import create_app

from openenv_runtime.environment import OpenEnvOrigamiEnvironment
from openenv_runtime.models import OrigamiAction, OrigamiObservation


app = create_app(
    env=lambda: OpenEnvOrigamiEnvironment(),
    action_cls=OrigamiAction,
    observation_cls=OrigamiObservation,
    env_name="optigami",
)


# ---------------------------------------------------------------------------
# Demo fold sequences — new format: type, line {start, end}, angle
# ---------------------------------------------------------------------------

DEMO_SEQUENCES: dict[str, list[dict]] = {
    "half_fold": [
        {"type": "valley", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
    ],
    "quarter_fold": [
        {"type": "valley", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
        {"type": "valley", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
    ],
    "letter_fold": [
        {"type": "valley", "line": {"start": [0.0, 0.333], "end": [1.0, 0.333]}, "angle": 180.0},
        {"type": "mountain", "line": {"start": [0.0, 0.667], "end": [1.0, 0.667]}, "angle": 180.0},
    ],
    "map_fold": [
        {"type": "valley", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
        {"type": "mountain", "line": {"start": [0.5, 0.0], "end": [0.5, 1.0]}, "angle": 180.0},
    ],
    "solar_panel": [
        {"type": "valley", "line": {"start": [0.0, 0.25], "end": [1.0, 0.25]}, "angle": 180.0},
        {"type": "mountain", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
        {"type": "valley", "line": {"start": [0.0, 0.75], "end": [1.0, 0.75]}, "angle": 180.0},
    ],
    "shelter_wall": [
        {"type": "valley", "line": {"start": [0.0, 0.333], "end": [1.0, 0.333]}, "angle": 180.0},
        {"type": "valley", "line": {"start": [0.0, 0.667], "end": [1.0, 0.667]}, "angle": 180.0},
    ],
    "stent": [
        {"type": "valley", "line": {"start": [0.0, 0.25], "end": [1.0, 0.25]}, "angle": 90.0},
        {"type": "mountain", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 90.0},
        {"type": "valley", "line": {"start": [0.0, 0.75], "end": [1.0, 0.75]}, "angle": 90.0},
        {"type": "stop", "line": {"start": [0.0, 0.0], "end": [1.0, 1.0]}, "angle": 0.0},
    ],
}


# ---------------------------------------------------------------------------
# API routes — must be registered BEFORE the StaticFiles catch-all mount
# ---------------------------------------------------------------------------

@app.get("/targets", include_in_schema=True)
def get_targets() -> dict:
    """Return available task names and metadata for the frontend."""
    from server.tasks import get_task_by_name, available_task_names

    result: dict[str, dict] = {}
    for name in available_task_names():
        t = get_task_by_name(name)
        result[name] = {
            "name": name,
            "level": t.get("difficulty", 1),
            "description": t.get("description", ""),
            "n_creases": t.get("max_folds", 3),
            "difficulty": t.get("difficulty", 1),
            "material": t.get("material", "paper"),
        }
    return result


@app.get("/episode/demo", include_in_schema=True)
def demo_episode(target: str = "half_fold") -> dict:
    """Return a pre-solved demo episode for the given task."""
    from server.origami_environment import OrigamiEnvironment
    from server.models import OrigamiAction as NewOrigamiAction
    from server.tasks import get_task_by_name

    # Fall back to half_fold if target not found
    folds = DEMO_SEQUENCES.get(target, DEMO_SEQUENCES["half_fold"])

    env = OrigamiEnvironment()
    obs = env.reset(task_name=target)

    steps: list[dict] = []

    for i, fold_dict in enumerate(folds):
        if fold_dict.get("type") == "stop":
            break

        action = NewOrigamiAction(
            fold_type=fold_dict["type"],
            fold_line=fold_dict["line"],
            fold_angle=float(fold_dict.get("angle", 180.0)),
        )

        obs = env.step(action)

        steps.append({
            "step": i + 1,
            "fold": fold_dict,
            "paper_state": obs.paper_state,
            "metrics": obs.metrics,
            "done": obs.done,
        })

        if obs.done:
            break

    task_def = get_task_by_name(target) if target else {}

    return {
        "task_name": target,
        "task": task_def,
        "steps": steps,
        "final_metrics": obs.metrics if steps else {},
    }


# ---------------------------------------------------------------------------
# Static file serving — must come LAST so API routes take priority
# ---------------------------------------------------------------------------

_BUILD_DIR = Path(__file__).resolve().parent.parent / "build"

if _BUILD_DIR.exists():
    app.mount("/", StaticFiles(directory=str(_BUILD_DIR), html=True), name="renderer")
else:
    @app.get("/", include_in_schema=False)
    def missing_renderer_build() -> HTMLResponse:
        return HTMLResponse(
            """
            <html><body style="font-family: sans-serif; margin: 24px;">
            <h3>Renderer build not found</h3>
            <p>No <code>build/</code> directory is present in the container.</p>
            <p>OpenEnv API docs are available at <a href="/docs">/docs</a>.</p>
            </body></html>
            """,
            status_code=200,
        )