Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 6 additions & 96 deletions benchmark/rdp_ladder/render_presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
The renderer consumes:

* exact RDP frames and input events from the isolated presentation capture;
* the exact ``ProgramGraphSpec`` emitted by ``build_program_graph``;
* the exact ``ProgramGraphSpec`` emitted by ``build_program_graph`` and shown
beside the video;
* the exact run parameters used by the first governed replay;
* the independent SQL rows retained in the run summary.

Expand Down Expand Up @@ -36,7 +37,6 @@
)
BG = "#071113"
PANEL = "#0d1c1f"
PANEL_2 = "#13272a"
TEXT = "#f2f8f7"
MUTED = "#a8bcba"
TEAL = "#45d6c3"
Expand Down Expand Up @@ -466,7 +466,7 @@ def _demonstration_frames(
(
_overlay(
last_frame,
phase="1 · Demonstrate",
phase="Demonstrate",
detail="Human-paced input over RDP",
cursor=cursor,
),
Expand All @@ -493,7 +493,7 @@ def _demonstration_frames(
(
_overlay(
last_frame,
phase="1 · Demonstrate",
phase="Demonstrate",
detail="Mouse and keyboard are retained",
cursor=point,
),
Expand All @@ -506,7 +506,7 @@ def _demonstration_frames(
(
_overlay(
last_frame,
phase="1 · Demonstrate",
phase="Demonstrate",
detail="Action recorded",
cursor=cursor,
click=True,
Expand All @@ -521,83 +521,6 @@ def _demonstration_frames(
return images


def _graph_view(spec: dict, *, active_index: int | None = None) -> Image.Image:
image = _base()
draw = ImageDraw.Draw(image)
_brand(draw, section="Real compiled artifact")
bundle = spec["bundle"]
draw.text(
(44, 104),
bundle["name"],
font=_font(34, bold=True),
fill=TEXT,
)
params = [f"${item['name']}" for item in bundle.get("params", [])]
draw.text(
(45, 150),
"Parameters: " + (" · ".join(params) if params else "none"),
font=_font(16),
fill=MUTED,
)

nodes = spec["nodes"]
positions: list[tuple[int, int, int, int]] = []
for index, node in enumerate(nodes):
col, row = index % 3, index // 3
x = 38 + col * 414
y = 194 + row * 176
box = (x, y, x + 378, y + 138)
positions.append(box)
highlighted = active_index is None or index <= active_index
fill = PANEL_2 if highlighted else PANEL
outline = TEAL if index == active_index else "#284248"
draw.rounded_rectangle(box, 18, fill=fill, outline=outline, width=3)
draw.text(
(x + 18, y + 14),
f"{index + 1:02d}",
font=_font(15, bold=True),
fill=TEAL if highlighted else MUTED,
)
title_lines = _wrap(node["title"], 35)[:2]
title_y = y + 12
for line in title_lines:
draw.text((x + 58, title_y), line, font=_font(16, bold=True), fill=TEXT)
title_y += 23
details: list[str] = []
resolution = node.get("resolution")
if resolution and resolution.get("top_rung"):
details.append(f"resolve: {resolution['top_rung']}")
if node.get("param"):
details.append(f"input: ${node['param']}")
if node.get("badges"):
details.extend(node["badges"][:2])
detail = " · ".join(details) or node.get("kind", "")
for line in _wrap(detail, 47)[:2]:
draw.text((x + 18, title_y + 8), line, font=_font(13), fill=MUTED)
title_y += 19

for index in range(len(positions) - 1):
left = positions[index]
right = positions[index + 1]
if index % 3 != 2:
start = (left[2] + 4, (left[1] + left[3]) // 2)
end = (right[0] - 6, (right[1] + right[3]) // 2)
else:
start = ((left[0] + left[2]) // 2, left[3] + 3)
end = ((right[0] + right[2]) // 2, right[1] - 5)
draw.line((start, end), fill="#527277", width=3)

provenance = bundle.get("provenance") or {}
digest = provenance.get("content_digest") or "not available"
draw.text(
(45, 742),
f"Graph spec v{spec['spec_version']} · bundle {str(digest)[:20]}",
font=_font(14),
fill=MUTED,
)
return image


def _selected_frames(
frames: list[tuple[dict, Image.Image]],
limit: int = 18,
Expand Down Expand Up @@ -872,26 +795,13 @@ def render(presentation_dir: Path, output: Path) -> dict:
)
presented_counts["01-demonstration"] = len(demo_images)

nodes = graph["nodes"]
for index in range(len(nodes)):
count = _write_repeated(
process, _graph_view(graph, active_index=index), 1.1
)
timeline.append(
count,
phase="compiled_workflow",
compiled_graph={"node_id": nodes[index]["id"], "node_index": index},
)
count = _write_repeated(process, _graph_view(graph), 3.6)
timeline.append(count, phase="compiled_workflow")

replay_images = _selected_frames(phase_frames["02-verified-replay"])
for source_event, image in replay_images:
count = _write_repeated(
process,
_overlay(
image,
phase="3 · Execute",
phase="Execute",
detail="The correct record is checked before input",
),
0.38,
Expand Down
11 changes: 11 additions & 0 deletions docs/REMOTE_FRAME_CONTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Remote frame contract

`remote_frame_contract` is a versioned deployment field for remote RDP and
remote-display settling. It binds exact frame dimensions and reviewed volatile
rectangles. The runtime rejects a geometry change or an overlap with a declared
protected region. It retains raw frame bytes and raw lease hashes. It masks only
transient derived inputs used for pointer-settle and final pre-input content
comparisons.

The current deployment schema can declare protected regions. Desktop editing
of these reviewed regions is a separate follow-up; no runtime learning occurs.
2 changes: 1 addition & 1 deletion openadapt_flow/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""openadapt-flow: record once, compile, replay deterministically, heal on drift."""

__version__ = "1.27.0"
__version__ = "1.27.1"

from openadapt_flow.ir import ( # noqa: F401
ActionKind,
Expand Down
13 changes: 12 additions & 1 deletion openadapt_flow/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,9 @@ class RemoteActuationBackend(Protocol):
The backend owns a one-shot content lease for the returned frame. Its next
input method captures once more under the backend input lock and refuses
before the first input edge if the window/session, dimensions, readiness,
or exact frame content changed. The lease is consumed once so a
or exact frame content changed. A sealed remote frame contract can exclude
reviewed volatile regions from a derived comparison. Raw frame evidence
and the exact lease stay unmodified. The lease is consumed once so a
multi-character type or double-click gesture cannot invalidate itself.
"""

Expand All @@ -537,6 +539,15 @@ def acquire_actuation_frame(self) -> bytes:
...


@runtime_checkable
class RemoteFrameContractBackend(Protocol):
"""Optional pre-input protected-region binding for remote comparison masks."""

def arm_remote_frame_contract(
self, *, protected_regions: tuple[tuple[int, int, int, int], ...]
) -> None: ...


@runtime_checkable
class FreshActuationReacquisitionBackend(Protocol):
"""Reset one proved zero-input invalidation for bounded reacquisition.
Expand Down
3 changes: 3 additions & 0 deletions openadapt_flow/backends/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ def _build_rdp_backend(
application_version_marker=cfg.rdp_application_version_marker,
environment_marker=cfg.rdp_environment_marker,
session_marker=cfg.rdp_session_marker,
remote_frame_contract=cfg.remote_frame_contract,
)

if rdp_transport is not None or has_host:
Expand All @@ -220,6 +221,7 @@ def _build_rdp_backend(
application_version_marker=cfg.rdp_application_version_marker,
environment_marker=cfg.rdp_environment_marker,
session_marker=cfg.rdp_session_marker,
remote_frame_contract=cfg.remote_frame_contract,
)

if window_client is not None or has_window:
Expand All @@ -236,6 +238,7 @@ def _build_rdp_backend(
kwargs["application_version_marker"] = cfg.rdp_application_version_marker
kwargs["environment_marker"] = cfg.rdp_environment_marker
kwargs["session_marker"] = cfg.rdp_session_marker
kwargs["remote_frame_contract"] = cfg.remote_frame_contract
return RemoteDisplayBackend(window_client, **kwargs)

raise ValueError(
Expand Down
29 changes: 27 additions & 2 deletions openadapt_flow/backends/rdp_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
StructuralResolutionRefused,
)
from openadapt_flow.ir import ActionDeliveryReceipt, Point
from openadapt_flow.remote_frame_contract import RemoteFrameContract
from openadapt_flow.runtime.resolver import visual_resolution_point_fingerprint

# What a transport may hand back as the current frame: a PIL image, or raw
Expand Down Expand Up @@ -307,8 +308,10 @@ def __init__(
session_marker: Optional[str] = None,
session_marker_probe: Optional[Callable[[bytes], bool]] = None,
session_identity_observer: Optional[Callable[[], Optional[str]]] = None,
remote_frame_contract: Optional["RemoteFrameContract"] = None,
) -> None:
self._transport = transport
self._remote_frame_contract = remote_frame_contract
self._viewport = viewport
self._max_frame_age_s = float(max_frame_age_s)
if self._max_frame_age_s <= 0:
Expand Down Expand Up @@ -357,6 +360,7 @@ def __init__(
self._session_identity_observer = session_identity_observer
self._last_frame_monotonic: Optional[float] = None
self._last_frame_digest: Optional[bytes] = None
self._last_comparison_digest: Optional[bytes] = None
self._last_session_identity: Optional[str] = None
self._qualification_environment: Optional[tuple[str, str, str, str]] = None
self._qualification_input_guard: Optional[Callable[[], None]] = None
Expand Down Expand Up @@ -398,8 +402,15 @@ def screenshot(self) -> bytes:
# and screenshot can never disagree.
self._viewport = img.size
png = self._png_bytes(img)
if self._remote_frame_contract is not None:
self._remote_frame_contract.require_geometry(img.size)
self._last_frame_monotonic = time.monotonic()
self._last_frame_digest = self._canonical_frame_digest(img)
self._last_comparison_digest = (
self._remote_frame_contract.comparison_digest(png)
if self._remote_frame_contract is not None
else self._last_frame_digest
)
self._last_session_identity = self._session_identity_from_frame(png)
if self._actuation_lease_state == _LEASE_ARMED:
self._invalidate_actuation_lease()
Expand Down Expand Up @@ -444,6 +455,12 @@ def acquire_actuation_frame(self) -> bytes:
self._actuation_lease_state = _LEASE_ARMED
return png

def arm_remote_frame_contract(
self, *, protected_regions: tuple[tuple[int, int, int, int], ...]
) -> None:
if self._remote_frame_contract is not None:
self._remote_frame_contract.arm(protected_regions)

def reset_fresh_actuation_state(self) -> None:
"""Reset only a typed zero-input content invalidation.

Expand Down Expand Up @@ -1102,8 +1119,16 @@ def _ensure_input_ready(
"target resolution; refusing input"
)
if self._actuation_lease_state == _LEASE_ARMED:
digest = self._canonical_frame_digest(current_img)
if self._last_frame_digest is None or digest != self._last_frame_digest:
raw_digest = self._canonical_frame_digest(current_img)
digest = (
self._remote_frame_contract.comparison_digest(current_png)
if self._remote_frame_contract is not None
else raw_digest
)
if (
self._last_comparison_digest is None
or digest != self._last_comparison_digest
):
changed_pixel_count, changed_bbox = self._frame_difference(
self._actuation_frame_png,
current_img,
Expand Down
Loading