From 2e368ae508d7a0a71c809556f8f3e83d3d75a6ba Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 31 Jul 2026 02:20:52 -0400 Subject: [PATCH 1/3] Harden live showcase execution and UI lifecycle Derive exact demo wells from prepared source metadata, use typed projection settings, and retain thin skeleton ROI outputs. Make orchestrator code-mode validation trust only external value types reachable from its config roots. Wait for manager finalization in MCP workflow polling and return control to Qt before reporting startup readiness. --- benchmark/demos/official30_lab_meeting.py | 36 +++++++++- openhcs/mcp/dev_client_core.py | 40 ++++++++--- .../backends/analysis/skeletonize_and_save.py | 10 ++- openhcs/pyqt_gui/app.py | 24 +++++-- openhcs/pyqt_gui/services/ui_agent_bridge.py | 31 +++++++- .../ui/shared/plate_manager_code_document.py | 63 ++++++++++++++++- scripts/master_multi_plate_demo.py | 7 +- scripts/mcp_assay_showcase.py | 16 ++--- tests/unit/agent/test_mcp_server.py | 70 ++++++++++++++----- .../pyqt_gui/test_gui_startup_progress.py | 19 ++--- tests/unit/pyqt_gui/test_ui_agent_bridge.py | 40 +++++++++++ tests/unit/test_master_multi_plate_demo.py | 5 ++ tests/unit/test_mcp_assay_showcase.py | 10 ++- .../unit/test_official30_lab_meeting_demo.py | 15 +++- tests/unit/test_skeletonize_and_save.py | 41 +++++++++-- 15 files changed, 351 insertions(+), 76 deletions(-) diff --git a/benchmark/demos/official30_lab_meeting.py b/benchmark/demos/official30_lab_meeting.py index 0579832fc..d05bd685f 100644 --- a/benchmark/demos/official30_lab_meeting.py +++ b/benchmark/demos/official30_lab_meeting.py @@ -23,6 +23,7 @@ JSONValue, ManifestCasePayload, ) +from openhcs.constants.constants import AllComponents from openhcs.core.aligned_image_payload import AlignedImageSliceContext from openhcs.core.artifacts import ArtifactType, MeasurementsArtifactType from openhcs.core.config import ( @@ -30,7 +31,11 @@ LazyPathPlanningConfig, LazyWellFilterConfig, ) -from openhcs.core.input_workspace import InputWorkspacePreparationRequest +from openhcs.core.input_workspace import ( + InputWorkspacePreparationRequest, + InputWorkspacePreparationResult, +) +from openhcs.core.source_matching import source_component_metadata_values from openhcs.core.steps.function_output_manifest import ( FunctionStepOutputProducerIdentityRequest, ) @@ -212,6 +217,33 @@ def _stream_only_declared_step( return streamed_steps +def _materialized_well_labels( + prepared: InputWorkspacePreparationResult, +) -> str | list[str]: + """Project exact execution wells from the prepared workspace authority.""" + + if prepared.materialization is None: + raise ValueError( + "Official30 lab-meeting inputs must materialize a source-binding " + "workspace with exact component metadata." + ) + labels = sorted( + { + value + for metadata in prepared.materialization.source_metadata.values() + for value in source_component_metadata_values( + metadata, + AllComponents.WELL, + ) + } + ) + if not labels: + raise ValueError( + "Official30 lab-meeting workspace declares no exact well labels." + ) + return labels[0] if len(labels) == 1 else labels + + def official30_lab_meeting_demo_contributions( *, session_root: Path, @@ -259,7 +291,7 @@ def official30_lab_meeting_demo_contributions( pipeline_config = replace( pipeline_config, well_filter_config=LazyWellFilterConfig( - well_filter=well_filter_config.well_filter, + well_filter=_materialized_well_labels(prepared), well_filter_mode=well_filter_config.well_filter_mode, ), path_planning_config=LazyPathPlanningConfig( diff --git a/openhcs/mcp/dev_client_core.py b/openhcs/mcp/dev_client_core.py index 5acc835ee..b50e7600f 100644 --- a/openhcs/mcp/dev_client_core.py +++ b/openhcs/mcp/dev_client_core.py @@ -44,7 +44,10 @@ ) from openhcs.serialization.json import to_jsonable from openhcs.constants.constants import AllComponents, OrchestratorState -from openhcs.core.execution_state import TerminalExecutionStatus +from openhcs.core.execution_state import ( + ManagerExecutionState, + TerminalExecutionStatus, +) from openhcs.core.native_threading import native_thread_count_environment_keys from openhcs.core.plate_file_inventory import ALL_PLATE_FILE_KINDS from openhcs.mcp.control_timeout import ( @@ -230,15 +233,17 @@ class McpDevServerSpec: def environment(self) -> dict[str, str]: """Environment entries inherited by the fresh MCP subprocess.""" - return AgentRuntimePlatformAuthority.current().project_child_process_environment( - os.environ, - include_graphical_session=True, - additional_keys=( - *self.mcp_environment_keys, - *AgentPathPolicy.environment_keys(), - *OpenHCSProcessEnvironment.child_process_environment_keys(), - *native_thread_count_environment_keys(), - ), + return ( + AgentRuntimePlatformAuthority.current().project_child_process_environment( + os.environ, + include_graphical_session=True, + additional_keys=( + *self.mcp_environment_keys, + *AgentPathPolicy.environment_keys(), + *OpenHCSProcessEnvironment.child_process_environment_keys(), + *native_thread_count_environment_keys(), + ), + ) ) def process_args(self) -> tuple[str, ...]: @@ -1852,6 +1857,8 @@ def workflow_poll_has_reached_terminal_state( target_scope_ids: tuple[str, ...], policy: WorkflowStatePollPolicy, ) -> bool: + if not workflow_poll_manager_is_idle(result): + return False rows = workflow_poll_target_rows( result, target_scope_ids=target_scope_ids, @@ -1868,6 +1875,8 @@ def workflow_poll_terminal_status( target_scope_ids: tuple[str, ...], policy: WorkflowStatePollPolicy, ) -> WorkflowPollSummaryStatus | None: + if not workflow_poll_manager_is_idle(result): + return None rows = workflow_poll_target_rows( result, target_scope_ids=target_scope_ids, @@ -1881,6 +1890,17 @@ def workflow_poll_terminal_status( return None +def workflow_poll_manager_is_idle(result: McpDevToolResult) -> bool: + """Return whether the workflow owner has completed batch finalization.""" + + state_payload = state_surface_payload(result) + manager_state = optional_str(state_payload.get("manager_execution_state")) + try: + return ManagerExecutionState(manager_state) is ManagerExecutionState.IDLE + except (TypeError, ValueError): + return False + + def workflow_poll_target_rows( result: McpDevToolResult, *, diff --git a/openhcs/processing/backends/analysis/skeletonize_and_save.py b/openhcs/processing/backends/analysis/skeletonize_and_save.py index 403ed0171..2bac9f680 100644 --- a/openhcs/processing/backends/analysis/skeletonize_and_save.py +++ b/openhcs/processing/backends/analysis/skeletonize_and_save.py @@ -36,20 +36,19 @@ class SkeletonizationResult: foreground_area_pixels: int threshold: float + @numpy @artifact_outputs( ArtifactSpec( "skeleton_measurements", MeasurementsArtifactType, - materialization=MaterializationSpec( - CsvOptions(filename_suffix="_details.csv") - ), + materialization=MaterializationSpec(CsvOptions(filename_suffix="_details.csv")), relations=(ArtifactMeasurementSubjectRelation(),), ), ArtifactSpec( "skeleton_rois", ObjectLabelsArtifactType, - materialization=MaterializationSpec(ROIOptions()), + materialization=MaterializationSpec(ROIOptions(min_area=1)), ), ) def skeletonize_and_save( @@ -91,8 +90,7 @@ def skeletonize_and_save( image_array = np.asarray(image) if image_array.ndim != 3: raise ValueError( - "skeletonize_and_save expects a 3D array with shape (Z, Y, X) " - "or (C, Y, X)" + "skeletonize_and_save expects a 3D array with shape (Z, Y, X) or (C, Y, X)" ) if image_array.shape[1] == 0 or image_array.shape[2] == 0: raise ValueError("skeletonize_and_save requires non-empty image planes") diff --git a/openhcs/pyqt_gui/app.py b/openhcs/pyqt_gui/app.py index 0eb79fbd0..0f8b7a3b8 100644 --- a/openhcs/pyqt_gui/app.py +++ b/openhcs/pyqt_gui/app.py @@ -130,7 +130,9 @@ def init_function_registry_background(): from openhcs.core.config import GlobalPipelineConfig # Set for editing (UI placeholders) - this uses threading.local() storage - set_global_config_for_editing(GlobalPipelineConfig, self.pipeline_runtime_config) + set_global_config_for_editing( + GlobalPipelineConfig, self.pipeline_runtime_config + ) # ALSO ensure context for orchestrator creation (required by orchestrator.__init__) ensure_global_config_context(GlobalPipelineConfig, self.pipeline_runtime_config) @@ -196,7 +198,8 @@ def show_main_window( self, *, on_deferred_initialization_complete: Callable[[], None] | None = None, - on_deferred_initialization_failed: Callable[[BaseException], None] | None = None, + on_deferred_initialization_failed: Callable[[BaseException], None] + | None = None, ): """Show the main window and schedule its authoritative ready boundary.""" if self.main_window is None: @@ -210,10 +213,8 @@ def show_main_window( # This includes log viewer and default windows (pipeline editor) from PyQt6.QtCore import QTimer - def _run_deferred_initialization() -> None: + def _report_deferred_initialization_complete() -> None: try: - self.main_window.deferred_initialization() - self.processEvents() if on_deferred_initialization_complete is not None: on_deferred_initialization_complete() except Exception as error: @@ -221,6 +222,19 @@ def _run_deferred_initialization() -> None: raise on_deferred_initialization_failed(error) + def _run_deferred_initialization() -> None: + try: + self.main_window.deferred_initialization() + # Return control to Qt before reporting readiness. A nested + # processEvents() call can be kept alive indefinitely by + # continuously firing timers, preventing the application's + # normal event loop and UI bridge dispatch from progressing. + QTimer.singleShot(0, _report_deferred_initialization_complete) + except Exception as error: + if on_deferred_initialization_failed is None: + raise + on_deferred_initialization_failed(error) + QTimer.singleShot(100, _run_deferred_initialization) def on_config_changed(self, new_config: GlobalPipelineConfig): diff --git a/openhcs/pyqt_gui/services/ui_agent_bridge.py b/openhcs/pyqt_gui/services/ui_agent_bridge.py index 99eac29dc..8ce4f140e 100644 --- a/openhcs/pyqt_gui/services/ui_agent_bridge.py +++ b/openhcs/pyqt_gui/services/ui_agent_bridge.py @@ -202,6 +202,9 @@ def validate(self, source: str) -> tuple[AgentError, ...]: visitor = DeclarativeCodeDocumentAstValidator( allowed_import_roots=self.allowed_import_roots, + allowed_external_value_types=( + PlateManagerCodeDocumentAuthority.declared_external_value_types() + ), allowed_builtin_references=self.allowed_builtin_references, ) visitor.visit(tree) @@ -215,9 +218,11 @@ def __init__( self, *, allowed_import_roots: frozenset[str], + allowed_external_value_types: frozenset[type[object]], allowed_builtin_references: frozenset[str], ) -> None: self._allowed_import_roots = allowed_import_roots + self._allowed_external_value_types = allowed_external_value_types self._allowed_builtin_references = allowed_builtin_references self._imported_names: set[str] = set() self._path_constructor_names: set[str] = set() @@ -251,13 +256,22 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None: self._path_constructor_names.add(imported_name) return root_name = module_name.split(".", maxsplit=1)[0] - if root_name not in self._allowed_import_roots: - self._error("unsafe_import", f"Import is not allowed: {module_name}") - return for alias in node.names: if alias.name == "*": self._error("unsafe_import", "Wildcard imports are not allowed.") continue + if ( + root_name not in self._allowed_import_roots + and not self._is_declared_external_value_import( + module_name, + alias.name, + ) + ): + self._error( + "unsafe_import", + f"Import is not allowed: {module_name}.{alias.name}", + ) + continue self._imported_names.add(alias.asname or alias.name) def visit_Assign(self, node: ast.Assign) -> None: @@ -416,6 +430,17 @@ def _is_path_expression(self, node: ast.expr) -> bool: def _error(self, code: str, message: str) -> None: self.errors.append(AgentError(code=code, message=message)) + def _is_declared_external_value_import( + self, + module_name: str, + imported_name: str, + ) -> bool: + return any( + declared_type.__module__ == module_name + and declared_type.__name__ == imported_name + for declared_type in self._allowed_external_value_types + ) + class UiCodeDocumentExecutionService: """Executes validated code-mode source through existing manager hooks.""" diff --git a/openhcs/ui/shared/plate_manager_code_document.py b/openhcs/ui/shared/plate_manager_code_document.py index c4f5555ce..cb9e47acb 100644 --- a/openhcs/ui/shared/plate_manager_code_document.py +++ b/openhcs/ui/shared/plate_manager_code_document.py @@ -3,10 +3,11 @@ from __future__ import annotations from collections.abc import Mapping, Sequence -from dataclasses import dataclass +from dataclasses import dataclass, fields, is_dataclass from enum import Enum +from functools import cache from pathlib import Path -from typing import Self +from typing import Any, Self, get_args, get_origin, get_type_hints from openhcs.core.config import GlobalPipelineConfig, PipelineConfig from openhcs.core.function_step_transport import FunctionStepTransportAuthority @@ -57,6 +58,18 @@ class PlateManagerCodeDocumentAuthority: HEADER = "# Edit this orchestrator configuration and save to apply changes" + @classmethod + def declared_external_value_types(cls) -> frozenset[type[object]]: + """Return external value types reachable from the document config roots. + + The document renderer emits the defining import for enum and dataclass + values. Derive those trusted external types from the same config classes + accepted by :meth:`from_namespace`, so UI source validation cannot drift + into a separately maintained dependency allowlist. + """ + + return _declared_external_value_types() + @classmethod def from_values( cls, @@ -250,3 +263,49 @@ def _scope_id(value: object, *, field_name: str) -> str: if not isinstance(value, (str, Path)): raise TypeError(f"{field_name} must be a plate path string or Path.") return PlateScopeIdentity.from_scope_id(str(value)).scope_id + + +@cache +def _declared_external_value_types() -> frozenset[type[object]]: + declared_types = _reachable_declared_types((GlobalPipelineConfig, PipelineConfig)) + return frozenset( + declared_type + for declared_type in declared_types + if declared_type.__module__.split(".", maxsplit=1)[0] + not in {"builtins", "openhcs"} + ) + + +def _reachable_declared_types( + roots: Sequence[type[object]], +) -> frozenset[type[object]]: + pending = list(roots) + visited: set[type[object]] = set() + while pending: + declared_type = pending.pop() + if declared_type in visited: + continue + visited.add(declared_type) + if not is_dataclass(declared_type): + continue + annotations = get_type_hints(declared_type, include_extras=True) + for declared_field in fields(declared_type): + annotation = annotations.get(declared_field.name, declared_field.type) + pending.extend(_annotation_types(annotation)) + return frozenset(visited) + + +def _annotation_types(annotation: object) -> tuple[type[object], ...]: + if annotation is Any: + return () + if isinstance(annotation, type): + return (annotation,) + return ( + tuple( + nested_type + for member in get_args(annotation) + for nested_type in _annotation_types(member) + ) + if get_origin(annotation) is not None + else () + ) diff --git a/scripts/master_multi_plate_demo.py b/scripts/master_multi_plate_demo.py index a844fc933..74d14152b 100644 --- a/scripts/master_multi_plate_demo.py +++ b/scripts/master_multi_plate_demo.py @@ -873,7 +873,10 @@ def prepare_all(self, schedule: Sequence[ScheduledDemo]) -> None: ), "--json", ), - timeout_seconds=30.0, + timeout_seconds=max( + 30.0, + self.workflow_timeout_seconds, + ), retry_window_seconds=30.0, ), tool_name="openhcs_inspect_pipeline_source_artifact_plan", @@ -1006,6 +1009,8 @@ def _wait_for_operation( sort_keys=True, ), "--json", + "--timeout-seconds", + str(timeout_seconds + 5.0), ), timeout_seconds=timeout_seconds + 5.0, ) diff --git a/scripts/mcp_assay_showcase.py b/scripts/mcp_assay_showcase.py index 1d4f3e9a8..a566b6b11 100644 --- a/scripts/mcp_assay_showcase.py +++ b/scripts/mcp_assay_showcase.py @@ -445,7 +445,6 @@ def _segmentation_step_source(name: str, *, stream_to_napari: bool) -> str: "min_cell_area": 20, "max_cell_area": 800, "remove_border_cells": False, - "return_segmentation_mask": True, }}, ), processing_config=LazyProcessingConfig( @@ -536,18 +535,18 @@ def _dual_channel_source(plate_path: Path, output_path: Path) -> str: { "w1": MetaXpressWavelengthSettings( channel_index=0, - approx_min_width=6.0, - approx_max_width=20.0, + approx_min_width=3.0, + approx_max_width=12.0, intensity_above_local_background=2500.0, ), "w2": MetaXpressW2Settings( channel_index=1, - approx_min_width=6.0, - approx_max_width=20.0, - intensity_above_local_background=1500.0, + approx_min_width=3.0, + approx_max_width=12.0, + intensity_above_local_background=100.0, stained_area=StainedArea.NUCLEUS, ), - "minimum_stained_area": 20.0, + "minimum_stained_area": 10.0, }, ), processing_config=LazyProcessingConfig( @@ -577,6 +576,7 @@ def _colocalization_source(plate_path: Path, output_path: Path) -> str: measure_colocalization, ) from openhcs.processing.backends.processors.numpy_processor import ( + NumpyStackProjectionMethod, create_projection, ) from openhcs.processing.backends.cellprofiler.spreadsheet_export import ( @@ -613,7 +613,7 @@ def _colocalization_source(plate_path: Path, output_path: Path) -> str: func=( create_projection, { - "method": "min_projection", + "method": NumpyStackProjectionMethod.MIN, }, ), processing_config=LazyProcessingConfig( diff --git a/tests/unit/agent/test_mcp_server.py b/tests/unit/agent/test_mcp_server.py index 389ab686d..fd5c2b5e8 100644 --- a/tests/unit/agent/test_mcp_server.py +++ b/tests/unit/agent/test_mcp_server.py @@ -285,9 +285,7 @@ def test_mcp_server_publishes_canonical_instructions(): assert "complete selected surface" in built.instructions health_index = built.instructions.index("openhcs_health_check") first_use_index = built.instructions.index("kind='first_use'") - capability_search_index = built.instructions.index( - "openhcs_search_capabilities" - ) + capability_search_index = built.instructions.index("openhcs_search_capabilities") capability_index = built.instructions.index("openhcs_list_capabilities") assert health_index < first_use_index < capability_search_index < capability_index assert "kind='first_use' before choosing tools" in built.instructions @@ -712,9 +710,7 @@ def test_mcp_tool_descriptions_expose_debugging_result_contracts(): assert "include_response" in viewer_payload_properties assert "axis_indices" in viewer_payload_properties assert "array_slices" in viewer_payload_properties - assert "bounded image records" in descriptions[ - "openhcs_sample_viewer_window_image" - ] + assert "bounded image records" in descriptions["openhcs_sample_viewer_window_image"] viewer_sample_properties = schemas["openhcs_sample_viewer_window_image"][ "properties" ] @@ -6477,8 +6473,7 @@ def test_mcp_dev_client_sample_plate_image_command_renders_compact_summary(): "resolution_shape=1x24x24 downsample_yx=4.0x4.0" ) in rendered assert ( - "Statistics: scope=bounded_sample dtype=uint16 min=0 max=65535 " - "mean=123.457" + "Statistics: scope=bounded_sample dtype=uint16 min=0 max=65535 mean=123.457" ) in rendered assert "Sample: origin_yx=0x0 shape=1x2x2 included=True" in rendered assert "[\n [\n [\n 1," in rendered @@ -8134,9 +8129,7 @@ def test_mcp_dev_client_code_documents_command_renders_compact_summary(): import openhcs.mcp.dev_client as dev_client parser = dev_client._build_parser() - args = parser.parse_args( - ("code-documents", "--contains", "plate", "--limit", "1") - ) + args = parser.parse_args(("code-documents", "--contains", "plate", "--limit", "1")) response = { "errors": [], "results": [ @@ -10144,9 +10137,7 @@ def test_mcp_dev_client_state_surfaces_renders_compact_summary(): import openhcs.mcp.dev_client as dev_client parser = dev_client._build_parser() - args = parser.parse_args( - ("state-surfaces", "--contains", "plate", "--limit", "1") - ) + args = parser.parse_args(("state-surfaces", "--contains", "plate", "--limit", "1")) response = { "errors": [], "results": [ @@ -10718,6 +10709,7 @@ def test_mcp_dev_client_workflow_poll_filters_target_scope_ids(): payloads=( { "payload": { + "manager_execution_state": "idle", "rows": [ { "plate_scope_id": "scope-a", @@ -10729,7 +10721,7 @@ def test_mcp_dev_client_workflow_poll_filters_target_scope_ids(): "compile_pending": True, "compiled": False, }, - ] + ], } }, ), @@ -10753,6 +10745,44 @@ def test_mcp_dev_client_workflow_poll_filters_target_scope_ids(): ) +def test_mcp_dev_client_workflow_poll_waits_for_manager_finalization(): + import openhcs.mcp.dev_client as dev_client + + result = dev_client.McpDevToolResult( + tool="openhcs_ui_get_state_surface", + mcp_error=False, + payloads=( + { + "payload": { + "manager_execution_state": "running", + "rows": [ + { + "plate_scope_id": "scope-a", + "compile_pending": False, + "compiled": True, + } + ], + } + }, + ), + ) + policy = dev_client.WorkflowStatePollPolicy.from_workflow_text("compile_plate") + + assert not dev_client.workflow_poll_has_reached_terminal_state( + result, + target_scope_ids=("scope-a",), + policy=policy, + ) + assert ( + dev_client.workflow_poll_terminal_status( + result, + target_scope_ids=("scope-a",), + policy=policy, + ) + is None + ) + + def test_mcp_dev_client_workflow_poll_reports_failed_terminal_rows(): import openhcs.mcp.dev_client as dev_client @@ -10766,6 +10796,7 @@ def test_mcp_dev_client_workflow_poll_reports_failed_terminal_rows(): payloads=( { "payload": { + "manager_execution_state": "idle", "rows": [ { "plate_scope_id": "scope-a", @@ -10773,7 +10804,7 @@ def test_mcp_dev_client_workflow_poll_reports_failed_terminal_rows(): "queue_position": None, "terminal_status": "failed", } - ] + ], } }, ), @@ -10784,6 +10815,7 @@ def test_mcp_dev_client_workflow_poll_reports_failed_terminal_rows(): payloads=( { "payload": { + "manager_execution_state": "idle", "rows": [ { "plate_scope_id": "scope-a", @@ -10791,7 +10823,7 @@ def test_mcp_dev_client_workflow_poll_reports_failed_terminal_rows(): "compiled": False, "orchestrator_state": "compile_failed", } - ] + ], } }, ), @@ -10852,6 +10884,7 @@ async def fake_call_tool(session, call, timeout_seconds): { "current_revision_token": f"rev-{state_call_count}", "payload": { + "manager_execution_state": ("idle" if compiled else "running"), "object_state_token": state_call_count, "rows": [ { @@ -10973,6 +11006,7 @@ async def fake_call_tool(session, call, timeout_seconds): { "current_revision_token": f"rev-{state_call_count}", "payload": { + "manager_execution_state": ("idle" if compiled else "running"), "object_state_token": state_call_count, "rows": [ { @@ -11082,6 +11116,7 @@ async def fake_call_tool(session, call, timeout_seconds): { "current_revision_token": "terminal", "payload": { + "manager_execution_state": "idle", "object_state_token": 2, "rows": [ { @@ -11267,6 +11302,7 @@ async def fake_call_tool(session, call, timeout_seconds): { "current_revision_token": f"rev-{state_call_count}", "payload": { + "manager_execution_state": ("idle" if failed else "running"), "object_state_token": state_call_count, "rows": [ { diff --git a/tests/unit/pyqt_gui/test_gui_startup_progress.py b/tests/unit/pyqt_gui/test_gui_startup_progress.py index 0c82476b4..403e93c03 100644 --- a/tests/unit/pyqt_gui/test_gui_startup_progress.py +++ b/tests/unit/pyqt_gui/test_gui_startup_progress.py @@ -51,10 +51,7 @@ def _popen(command, **kwargs): ) controller.fail("Startup failed", "traceback") - events = [ - json.loads(line) - for line in process.stdin.getvalue().splitlines() - ] + events = [json.loads(line) for line in process.stdin.getvalue().splitlines()] assert captured["command"] == [ sys.executable, "-m", @@ -178,8 +175,8 @@ def run(self, *, on_main_window_ready, on_startup_failure): app_module = ModuleType("openhcs.pyqt_gui.app") app_module.OpenHCSPyQtApp = _Application window_utils_module = ModuleType("pyqt_reactive.utils.window_utils") - window_utils_module.install_global_window_bounds_filter = ( - lambda app: events.append(("bounds", app)) + window_utils_module.install_global_window_bounds_filter = lambda app: events.append( + ("bounds", app) ) monkeypatch.setitem(sys.modules, config_module.__name__, config_module) @@ -261,10 +258,7 @@ def main(*, arguments, startup_progress): monkeypatch.setattr(gui_startup.sys, "argv", ["openhcs"]) assert gui_startup.main() == 0 - events = [ - json.loads(line) - for line in process.stdin.getvalue().splitlines() - ] + events = [json.loads(line) for line in process.stdin.getvalue().splitlines()] assert events == [ { "kind": "output", @@ -452,9 +446,6 @@ def deferred_initialization(self): class _ApplicationHarness: main_window = _MainWindow() - def processEvents(self): - events.append("process") - monkeypatch.setattr(QtCore, "QTimer", _ImmediateTimer) OpenHCSPyQtApp.show_main_window( _ApplicationHarness(), @@ -467,7 +458,7 @@ def processEvents(self): "activate", ("timer", 100), "deferred", - "process", + ("timer", 0), "ready", ] diff --git a/tests/unit/pyqt_gui/test_ui_agent_bridge.py b/tests/unit/pyqt_gui/test_ui_agent_bridge.py index 15df01598..3b95f0297 100644 --- a/tests/unit/pyqt_gui/test_ui_agent_bridge.py +++ b/tests/unit/pyqt_gui/test_ui_agent_bridge.py @@ -51,6 +51,7 @@ from openhcs.constants.constants import OrchestratorState from openhcs.core.config import ( GlobalPipelineConfig, + LazyNapariStreamingConfig, LazyPathPlanningConfig, PipelineConfig, ) @@ -101,6 +102,10 @@ PycodifiedObjectDocumentSpec, ) from openhcs.ui.shared.plate_scope_identity import PipelineScopeIdentity +from openhcs.ui.shared.plate_manager_code_document import ( + PlateManagerCodeDocumentAuthority, +) +from zmqruntime.config import TransportMode from openhcs.pyqt_gui.services.ui_bridge_pipeline_editor import ( PipelineEditorBridgeProviderSet, ) @@ -3825,6 +3830,41 @@ def test_source_policy_allows_public_function_steps_not_runtime_factories() -> N assert any(error.code == "unsafe_call" for error in errors) +def test_source_policy_accepts_rendered_external_config_value_types() -> None: + payload = PlateManagerCodeDocumentAuthority.from_values( + plate_paths=(PLATE_SCOPE_ID,), + global_pipeline_config=GlobalPipelineConfig(), + per_plate_configs={ + PLATE_SCOPE_ID: PipelineConfig( + napari_streaming_config=LazyNapariStreamingConfig( + enabled=True, + transport_mode=TransportMode.IPC, + ) + ) + }, + pipeline_data={PLATE_SCOPE_ID: []}, + ) + + source = PlateManagerCodeDocumentAuthority.render(payload) + + assert "from zmqruntime.config import TransportMode" in source + assert UiCodeDocumentSourcePolicy().validate(source) == () + + +def test_source_policy_rejects_undeclared_external_constructor_type() -> None: + source = ( + "from zmqruntime.config import ZMQConfig\n" + f"plate_paths = ['{PLATE_SCOPE_ID}']\n" + "global_config = None\n" + f"per_plate_configs = {{'{PLATE_SCOPE_ID}': ZMQConfig()}}\n" + f"pipeline_data = {{'{PLATE_SCOPE_ID}': []}}\n" + ) + + errors = UiCodeDocumentSourcePolicy().validate(source) + + assert any(error.code == "unsafe_import" for error in errors) + + def test_source_policy_allows_safe_builtin_type_references_only() -> None: source = ( "from openhcs.core.runtime_tabular_values import FieldSpec\n" diff --git a/tests/unit/test_master_multi_plate_demo.py b/tests/unit/test_master_multi_plate_demo.py index b0a289fa8..96a00fbbf 100644 --- a/tests/unit/test_master_multi_plate_demo.py +++ b/tests/unit/test_master_multi_plate_demo.py @@ -971,6 +971,11 @@ def test_mcp_operations_wait_for_long_ui_apply_through_operation_owner(tmp_path) ) ] assert len(wait_calls) == 2 + for wait_call in wait_calls: + arguments = json.loads(wait_call[wait_call.index("--arguments") + 1]) + request_timeout = arguments["timeout_seconds"] + client_timeout = float(wait_call[wait_call.index("--timeout-seconds") + 1]) + assert client_timeout == request_timeout + 5.0 def test_mcp_operations_retry_read_only_document_without_replaying_apply(tmp_path): diff --git a/tests/unit/test_mcp_assay_showcase.py b/tests/unit/test_mcp_assay_showcase.py index 7f286d83c..80a6dd763 100644 --- a/tests/unit/test_mcp_assay_showcase.py +++ b/tests/unit/test_mcp_assay_showcase.py @@ -154,6 +154,7 @@ def test_showcase_sources_are_seven_bounded_distinct_pipeline_documents(tmp_path assert "materialize_runtime_artifacts=True" in source assert "SourceBinding" not in source assert ".cppipe" not in source + assert '"return_segmentation_mask"' not in source assert str(plate_path) in source assert str(output_path) in source if blueprint_index >= 3: @@ -185,9 +186,16 @@ def test_showcase_sources_are_seven_bounded_distinct_pipeline_documents(tmp_path _callable, streamed_kwargs = streamed_steps[0].func assert streamed_kwargs["retain_neighbor_count_image"] is True assert streamed_kwargs["retain_percent_touching_image"] is False + if blueprint.scenario_id == "dual_channel_phenotype": + assert source.count("approx_min_width=3.0") == 2 + assert source.count("approx_max_width=12.0") == 2 + assert "intensity_above_local_background=2500.0" in source + assert "intensity_above_local_background=100.0" in source + assert '"minimum_stained_area": 10.0' in source if blueprint.scenario_id == "image_colocalization": assert "create_projection" in source - assert '"method": "min_projection"' in source + assert "NumpyStackProjectionMethod.MIN" in source + assert '"method": "min_projection"' not in source assert "image_math" not in source assert len(namespace["pipeline_steps"]) == 3 diff --git a/tests/unit/test_official30_lab_meeting_demo.py b/tests/unit/test_official30_lab_meeting_demo.py index cbfdccb23..b12327215 100644 --- a/tests/unit/test_official30_lab_meeting_demo.py +++ b/tests/unit/test_official30_lab_meeting_demo.py @@ -10,6 +10,9 @@ InputWorkspacePreparationRequest, InputWorkspacePreparationResult, ) +from openhcs.core.source_binding_workspace import ( + SourceBindingWorkspaceMaterialization, +) from openhcs.core.steps.function_step import FunctionStep from openhcs.processing.backends.processors.numpy_processor import percentile_normalize from openhcs.processing.presets.demo_contribution import PipelineDemoContribution @@ -60,6 +63,16 @@ def fake_prepare( FunctionStep(name=target_name, func=percentile_normalize), ], pipeline_config=PipelineConfig(), + materialization=SourceBindingWorkspaceMaterialization( + source_root=request.selected_path, + workspace_root=execution_plate_path, + metadata_path=execution_plate_path / "openhcs_metadata.json", + plane_mappings={}, + artifact_mappings={}, + source_metadata={ + "A01_s001_w1_z001_t001.tif": {"well": "A01"}, + }, + ), ) monkeypatch.setenv("CELLPROFILER_EXAMPLES_ROOT", str(examples_root)) @@ -84,7 +97,7 @@ def fake_prepare( for contribution in contributions ) assert len(preparation_requests) == 2 - expected_axis_filters = (1, 1) + expected_axis_filters = ("A01", "A01") for contribution, expected_axis_filter in zip( contributions, expected_axis_filters, diff --git a/tests/unit/test_skeletonize_and_save.py b/tests/unit/test_skeletonize_and_save.py index 5945713b3..ab6ee1919 100644 --- a/tests/unit/test_skeletonize_and_save.py +++ b/tests/unit/test_skeletonize_and_save.py @@ -82,6 +82,7 @@ def test_skeletonize_and_save_declares_csv_and_roi_materialization(): assert csv_options.filename_suffix == "_details.csv" assert csv_options.fields is None assert isinstance(roi_options, ROIOptions) + assert roi_options.min_area == 1 assert measurement_spec.artifact_type is MeasurementsArtifactType assert measurement_spec.relations[0].measurement_subject() is not None assert label_spec.artifact_type is ObjectLabelsArtifactType @@ -89,9 +90,11 @@ def test_skeletonize_and_save_declares_csv_and_roi_materialization(): def test_skeleton_measurements_materialize_as_csv(): filemanager = FileManager({"memory": MemoryStorageBackend()}) - spec = CallableContract.from_callable( - skeletonize_and_save - ).artifact_outputs[0].materialization + spec = ( + CallableContract.from_callable(skeletonize_and_save) + .artifact_outputs[0] + .materialization + ) measurement = SkeletonizationResult( slice_index=0, skeleton_count=2, @@ -115,9 +118,35 @@ def test_skeleton_measurements_materialize_as_csv(): assert output_path == "/tmp/A01_skeleton_measurements_step1_details.csv" csv_frame = pd.read_csv(StringIO(filemanager.load(output_path, "memory"))) - assert csv_frame.to_dict(orient="records") == [ - dict(measurements.row_mappings()[0]) - ] + assert csv_frame.to_dict(orient="records") == [dict(measurements.row_mappings()[0])] + + +def test_skeleton_roi_materialization_retains_thin_components(): + image = np.zeros((1, 12, 12), dtype=np.float32) + image[0, 2:7, 2] = 1.0 + _, _, masks = _skeletonize_and_save_impl()( + image, + threshold=0.5, + min_component_size=1, + ) + filemanager = FileManager({"memory": MemoryStorageBackend()}) + spec = ( + CallableContract.from_callable(skeletonize_and_save) + .artifact_outputs[1] + .materialization + ) + + output_path = materialize( + spec, + data=masks, + path="/tmp/A01_skeleton_rois_step1", + filemanager=filemanager, + backends=["memory"], + backend_kwargs={}, + ) + + assert output_path == "/tmp/A01_skeleton_rois_step1_rois.roi.zip" + assert len(filemanager.load(output_path, "memory")) == 1 def test_skeleton_measurements_generate_metaxpress_style_summary(tmp_path): From be2e2a31e47926f6ec61786382ad9fd4fc80793d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 31 Jul 2026 02:21:15 -0400 Subject: [PATCH 2/3] Add reproducible media gallery capture tooling Capture real X11 windows losslessly, derive bounded WebP, WebM, MP4, and GIF outputs from one typed manifest, and preserve hashes and probe metadata for independent validation. Document the privacy, provenance, and reproducibility workflow. --- docs/media_gallery_capture.md | 239 ++++ scripts/capture_media_gallery.py | 1379 ++++++++++++++++++++++ tests/unit/test_capture_media_gallery.py | 504 ++++++++ 3 files changed, 2122 insertions(+) create mode 100644 docs/media_gallery_capture.md create mode 100644 scripts/capture_media_gallery.py create mode 100644 tests/unit/test_capture_media_gallery.py diff --git a/docs/media_gallery_capture.md b/docs/media_gallery_capture.md new file mode 100644 index 000000000..fac239a9e --- /dev/null +++ b/docs/media_gallery_capture.md @@ -0,0 +1,239 @@ +# Media gallery capture and delivery + +`scripts/capture_media_gallery.py` captures real X11 application windows and +derives bounded website media from immutable source captures. It does not draw, +blur, replace, synthesize, or otherwise fabricate application content. Changes +to visible UI state must happen in the application before capture. + +The workflow keeps three concerns separate: + +1. OpenHCS and its integrations create the real session. +2. A lossless PNG or FFV1 Matroska file records that session. +3. One typed JSON manifest owns every crop, trim, output name, resolution bound, + size bound, frame rate, and poster timestamp. + +Captions, scenario descriptions, feature claims, and website layout do not +belong in the manifest. + +## Host requirements + +Run the capability report before a capture session: + +```bash +python scripts/capture_media_gallery.py doctor +``` + +The report includes executable paths and versions so the exact toolchain can be +preserved with the capture report. + +- Building and validating media requires FFmpeg and FFprobe. +- `capture-still` additionally requires ImageMagick. +- `record-window` additionally requires `xdotool` and an X11 display. + +Wayland-only hosts can record a lossless PNG or Matroska source with their +compositor's capture tool, then use the same manifest-based plan, build, and +validate commands. Missing optional tools produce an actionable error rather +than an import failure. + +## Capture immutable sources + +List visible X11 windows and note the ID of the intended window: + +```bash +wmctrl -lx +``` + +Capture a still: + +```bash +python scripts/capture_media_gallery.py capture-still \ + --source-root /absolute/private/gallery-captures \ + --window-id 0x04600007 \ + --output raw/interface.png +``` + +Record a fixed window rectangle as lossless FFV1: + +```bash +python scripts/capture_media_gallery.py record-window \ + --source-root /absolute/private/gallery-captures \ + --window-id 0x04600007 \ + --output raw/interaction.mkv \ + --duration-seconds 18 \ + --fps 30 \ + --display "${DISPLAY}" +``` + +Keep the selected window stationary while recording. The command resolves its +geometry once so that another window cannot enter the frame if the target is +moved. Both capture commands refuse to overwrite an existing source. + +## Declare derivatives once + +The manifest has no separate format field. The validated filename suffix selects +the authoritative WebP, MP4, WebM, or GIF encoding declaration, so a format +string cannot drift from its codec. + +```json +{ + "schema_version": 1, + "captures": [ + { + "source": "raw/interface.png", + "crop": { + "x": 20, + "y": 40, + "width": 1800, + "height": 1080 + }, + "outputs": [ + { + "filename": "interface-overview.webp", + "max_width": 1600, + "max_height": 1000, + "max_bytes": 1200000 + } + ] + }, + { + "source": "raw/interaction.mkv", + "crop": { + "x": 20, + "y": 40, + "width": 1800, + "height": 1080 + }, + "trim": { + "start_seconds": 3.25, + "duration_seconds": 10 + }, + "outputs": [ + { + "filename": "interaction-poster.webp", + "max_width": 1600, + "max_height": 1000, + "max_bytes": 1200000, + "frame_at_seconds": 1.5 + }, + { + "filename": "interaction.webm", + "max_width": 1600, + "max_height": 1000, + "max_bytes": 6000000, + "fps": 24 + }, + { + "filename": "interaction.mp4", + "max_width": 1600, + "max_height": 1000, + "max_bytes": 8000000, + "fps": 24 + }, + { + "filename": "interaction.gif", + "max_width": 960, + "max_height": 600, + "max_bytes": 12000000, + "fps": 12 + } + ] + } + ] +} +``` + +Motion trims are limited to 30 seconds. Derived filenames must be lowercase +basenames made from letters, numbers, and single hyphens. This keeps URLs stable +and prevents an output from escaping the destination root. + +Use WebM as the primary website animation, MP4 as the broad compatibility +fallback, and the poster WebP for reduced-motion and unloaded states. GIF is +optional and intended for sharing where video is unavailable; it is usually +larger than either video encoding. + +## Plan, build, and verify + +Inspect exact commands before writing: + +```bash +python scripts/capture_media_gallery.py plan \ + --manifest /absolute/private/gallery-captures/manifest.json \ + --source-root /absolute/private/gallery-captures \ + --output-root website/assets/gallery +``` + +`plan` validates paths and typed records but does not require source files, probe +media, create directories, or write outputs. + +Build each derivative atomically and preserve the JSON report: + +```bash +python scripts/capture_media_gallery.py build \ + --manifest /absolute/private/gallery-captures/manifest.json \ + --source-root /absolute/private/gallery-captures \ + --output-root website/assets/gallery \ + > /absolute/private/gallery-captures/build-report.json +``` + +The report records source and derivative SHA-256 hashes, dimensions, byte sizes, +codecs, and durations. Existing derivatives are rejected by default. After +reviewing the manifest and source, `--force` may atomically replace derivatives; +it never permits replacing a source capture. + +Re-run verification independently before staging website assets: + +```bash +python scripts/capture_media_gallery.py validate \ + --manifest /absolute/private/gallery-captures/manifest.json \ + --source-root /absolute/private/gallery-captures \ + --output-root website/assets/gallery \ + > /absolute/private/gallery-captures/validation-report.json +``` + +Validation fails if an output is missing, uses the wrong codec, exceeds its +dimensions or byte budget, or has a duration inconsistent with the declared +trim. Encoder versions can change exact compressed bytes, so reproducibility +means preserving the manifest, source hash, command plan, toolchain report, and +build report together. + +## Privacy and scientific-integrity review + +Complete this review before capture: + +- Use redistributable example data or a session whose visible metadata is safe + to publish. +- Close unrelated windows, terminals, notification surfaces, file managers, and + private browser tabs. +- Use public-safe plate names and paths in the actual running application. +- Disable desktop notifications for the recording period. +- Avoid showing credentials, tokens, usernames, hostnames, patient or subject + identifiers, unpublished measurements, and private local paths. + +Complete this review after capture: + +- Inspect every lossless source at full resolution before deriving outputs. +- Inspect each poster, the first and last frame, and the full animation. +- Confirm that cropping excludes only irrelevant window chrome or surrounding + desktop space. Cropping is not a substitute for redaction. +- If private content appears inside the intended application frame, correct the + real session and capture it again. Do not paint over the source. +- Confirm that the final caption describes only behavior visible in the media + and verified in the running software. +- Keep lossless sources and reports in controlled storage. Publish only reviewed + derivatives. + +## Reproducible capture record + +For each accepted media group, retain: + +- the source PNG or FFV1 Matroska file; +- the JSON manifest; +- the `doctor` toolchain report; +- the dry-run command plan; +- the build and validation reports; +- dataset provenance and redistribution evidence; +- the OpenHCS commit and package version; +- the human privacy and claim-verification sign-off. + +These records are capture provenance, not a second registry of OpenHCS +scenarios or website claims. diff --git a/scripts/capture_media_gallery.py b/scripts/capture_media_gallery.py new file mode 100644 index 000000000..9f67e3091 --- /dev/null +++ b/scripts/capture_media_gallery.py @@ -0,0 +1,1379 @@ +#!/usr/bin/env python3 +"""Capture real application windows and derive validated web gallery media. + +The tool never changes pixels to fabricate UI state. It can capture a real +window, crop a source capture, trim a recording, transcode it, and validate the +derived media against one typed JSON manifest. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +import re +import shutil +import subprocess +import sys +import tempfile +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Any, Mapping, Sequence + +SCHEMA_VERSION = 1 +MAX_DERIVED_MOTION_SECONDS = 30.0 +MAX_RAW_RECORDING_SECONDS = 180.0 +OUTPUT_NAME_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*\.(?:gif|mp4|webm|webp)$") +WINDOW_ID_PATTERN = re.compile(r"^(?:0x[0-9a-fA-F]+|[1-9][0-9]*)$") +DISPLAY_PATTERN = re.compile(r"^(?:[A-Za-z0-9_.-]+)?:\d+(?:\.\d+)?$") + + +class MediaGalleryError(RuntimeError): + """Raised when capture input or generated media violates the contract.""" + + +class MediaCategory(Enum): + """Whether a media file is a still image or a time-varying recording.""" + + STILL = "still" + MOTION = "motion" + + +@dataclass(frozen=True) +class HostToolDeclaration: + """One external executable and its non-mutating version command.""" + + executable: str + version_arguments: tuple[str, ...] + + +class HostTool(Enum): + """External tools used by capture and transcoding operations.""" + + FFMPEG = HostToolDeclaration("ffmpeg", ("-version",)) + FFPROBE = HostToolDeclaration("ffprobe", ("-version",)) + MAGICK = HostToolDeclaration("magick", ("-version",)) + XDOTOOL = HostToolDeclaration("xdotool", ("version",)) + + +@dataclass(frozen=True) +class SourceFormatDeclaration: + """Source suffix and its media category.""" + + suffix: str + category: MediaCategory + + +class SourceFormat(Enum): + """Lossless and common source-capture formats accepted by the tool.""" + + PNG = SourceFormatDeclaration(".png", MediaCategory.STILL) + TIFF = SourceFormatDeclaration(".tif", MediaCategory.STILL) + TIFF_LONG = SourceFormatDeclaration(".tiff", MediaCategory.STILL) + WEBP = SourceFormatDeclaration(".webp", MediaCategory.STILL) + MATROSKA = SourceFormatDeclaration(".mkv", MediaCategory.MOTION) + QUICKTIME = SourceFormatDeclaration(".mov", MediaCategory.MOTION) + MP4 = SourceFormatDeclaration(".mp4", MediaCategory.MOTION) + WEBM = SourceFormatDeclaration(".webm", MediaCategory.MOTION) + + @classmethod + def for_path(cls, path: Path) -> SourceFormat: + """Return the declaration selected by a capture's file suffix.""" + + suffix = path.suffix.lower() + for source_format in cls: + if source_format.value.suffix == suffix: + return source_format + choices = ", ".join(item.value.suffix for item in cls) + raise MediaGalleryError( + f"Unsupported source format {suffix or ''!r}; expected one of " + f"{choices}." + ) + + +@dataclass(frozen=True) +class OutputEncodingDeclaration: + """Authoritative web encoding declaration for one output suffix.""" + + suffix: str + category: MediaCategory + codec_name: str + ffmpeg_arguments: tuple[str, ...] + map_arguments: tuple[str, ...] = ("-map", "0:v:0") + filter_option: str = "-vf" + filter_template: str = "{filters}" + + def render_filter(self, filters: str) -> tuple[str, str]: + """Render the encoding's filter option without caller-side dispatch.""" + + return self.filter_option, self.filter_template.format(filters=filters) + + +class OutputEncoding(Enum): + """Supported web derivatives and their deterministic FFmpeg contracts.""" + + WEBP = OutputEncodingDeclaration( + suffix=".webp", + category=MediaCategory.STILL, + codec_name="webp", + ffmpeg_arguments=( + "-frames:v", + "1", + "-c:v", + "libwebp", + "-preset", + "picture", + "-quality", + "84", + "-compression_level", + "6", + "-threads", + "1", + ), + ) + MP4 = OutputEncodingDeclaration( + suffix=".mp4", + category=MediaCategory.MOTION, + codec_name="h264", + ffmpeg_arguments=( + "-an", + "-c:v", + "libx264", + "-preset", + "slow", + "-crf", + "20", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + "-threads", + "1", + ), + ) + WEBM = OutputEncodingDeclaration( + suffix=".webm", + category=MediaCategory.MOTION, + codec_name="vp9", + ffmpeg_arguments=( + "-an", + "-c:v", + "libvpx-vp9", + "-crf", + "30", + "-b:v", + "0", + "-pix_fmt", + "yuv420p", + "-row-mt", + "0", + "-threads", + "1", + ), + ) + GIF = OutputEncodingDeclaration( + suffix=".gif", + category=MediaCategory.MOTION, + codec_name="gif", + ffmpeg_arguments=("-an", "-loop", "0", "-threads", "1"), + map_arguments=("-map", "[gallery_output]"), + filter_option="-filter_complex", + filter_template=( + "{filters},split[gallery_a][gallery_b];" + "[gallery_a]palettegen=max_colors=192:" + "reserve_transparent=0:stats_mode=diff[gallery_palette];" + "[gallery_b][gallery_palette]paletteuse=" + "dither=sierra2_4a:diff_mode=rectangle[gallery_output]" + ), + ) + + @classmethod + def for_path(cls, path: Path) -> OutputEncoding: + """Return the single encoding authority selected by output suffix.""" + + suffix = path.suffix.lower() + for encoding in cls: + if encoding.value.suffix == suffix: + return encoding + choices = ", ".join(item.value.suffix for item in cls) + raise MediaGalleryError( + f"Unsupported output format {suffix or ''!r}; expected one of " + f"{choices}." + ) + + +@dataclass(frozen=True) +class Crop: + """A rectangular crop in source pixels.""" + + x: int + y: int + width: int + height: int + + def __post_init__(self) -> None: + if self.x < 0 or self.y < 0: + raise MediaGalleryError("Crop x and y must be non-negative.") + if self.width <= 0 or self.height <= 0: + raise MediaGalleryError("Crop width and height must be positive.") + + @property + def ffmpeg_filter(self) -> str: + """Return the exact FFmpeg crop expression.""" + + return f"crop={self.width}:{self.height}:{self.x}:{self.y}" + + +@dataclass(frozen=True) +class Trim: + """A bounded interval selected from a source recording.""" + + start_seconds: float + duration_seconds: float + + def __post_init__(self) -> None: + if not math.isfinite(self.start_seconds) or self.start_seconds < 0: + raise MediaGalleryError( + "Trim start_seconds must be a finite non-negative number." + ) + if ( + not math.isfinite(self.duration_seconds) + or self.duration_seconds <= 0 + or self.duration_seconds > MAX_DERIVED_MOTION_SECONDS + ): + raise MediaGalleryError( + "Trim duration_seconds must be greater than zero and no more than " + f"{MAX_DERIVED_MOTION_SECONDS:g}." + ) + + @property + def end_seconds(self) -> float: + """Return the exclusive end of the selected source interval.""" + + return self.start_seconds + self.duration_seconds + + +@dataclass(frozen=True) +class Derivative: + """One output file and all bounds required to validate it.""" + + filename: str + max_width: int + max_height: int + max_bytes: int + fps: int | None = None + frame_at_seconds: float | None = None + + def __post_init__(self) -> None: + if not OUTPUT_NAME_PATTERN.fullmatch(self.filename): + raise MediaGalleryError( + "Output filename must be a lowercase caption-safe basename using " + "letters, numbers, and single hyphens." + ) + if self.max_width <= 0 or self.max_height <= 0: + raise MediaGalleryError("Output dimensions must be positive.") + if self.max_bytes <= 0: + raise MediaGalleryError("Output max_bytes must be positive.") + if self.fps is not None and not 1 <= self.fps <= 60: + raise MediaGalleryError("Output fps must be between 1 and 60.") + if self.frame_at_seconds is not None and ( + not math.isfinite(self.frame_at_seconds) or self.frame_at_seconds < 0 + ): + raise MediaGalleryError( + "Output frame_at_seconds must be a finite non-negative number." + ) + + @property + def path(self) -> Path: + """Return the validated output basename as a Path.""" + + return Path(self.filename) + + @property + def encoding(self) -> OutputEncoding: + """Resolve web encoding from the authoritative output suffix.""" + + return OutputEncoding.for_path(self.path) + + @property + def scale_filter(self) -> str: + """Fit within bounds without upscaling and keep codec-safe dimensions.""" + + return ( + f"scale=w='min(iw,{self.max_width})':" + f"h='min(ih,{self.max_height})':" + "force_original_aspect_ratio=decrease:force_divisible_by=2" + ) + + +@dataclass(frozen=True) +class CaptureRecord: + """One immutable source capture and all authorized derivatives.""" + + source: Path + outputs: tuple[Derivative, ...] + crop: Crop | None = None + trim: Trim | None = None + + def __post_init__(self) -> None: + _validate_relative_path(self.source, "Capture source") + if not self.outputs: + raise MediaGalleryError("Each capture must declare at least one output.") + output_names = tuple(output.filename for output in self.outputs) + if len(set(output_names)) != len(output_names): + raise MediaGalleryError( + f"Capture {self.source} declares duplicate output filenames." + ) + + source_category = self.source_format.value.category + if source_category is MediaCategory.STILL and self.trim is not None: + raise MediaGalleryError("Still captures cannot declare a trim interval.") + if source_category is MediaCategory.MOTION and self.trim is None: + raise MediaGalleryError( + "Motion captures must declare an explicit bounded trim interval." + ) + + for output in self.outputs: + output_category = output.encoding.value.category + if source_category is MediaCategory.STILL: + if output_category is not MediaCategory.STILL: + raise MediaGalleryError( + "Still captures can only produce still WebP derivatives." + ) + if output.fps is not None or output.frame_at_seconds is not None: + raise MediaGalleryError( + "Still derivatives cannot declare fps or frame_at_seconds." + ) + continue + + if output_category is MediaCategory.MOTION: + if output.fps is None: + raise MediaGalleryError( + f"Motion output {output.filename} must declare fps." + ) + if output.frame_at_seconds is not None: + raise MediaGalleryError( + "Motion outputs cannot declare frame_at_seconds." + ) + continue + + if output.fps is not None or output.frame_at_seconds is None: + raise MediaGalleryError( + f"Poster output {output.filename} must declare " + "frame_at_seconds and no fps." + ) + assert self.trim is not None + if output.frame_at_seconds > self.trim.duration_seconds: + raise MediaGalleryError( + f"Poster frame for {output.filename} falls outside the trim." + ) + + @property + def source_format(self) -> SourceFormat: + """Resolve source semantics from its suffix.""" + + return SourceFormat.for_path(self.source) + + +@dataclass(frozen=True) +class CaptureManifest: + """The sole authority for source transformations and output constraints.""" + + captures: tuple[CaptureRecord, ...] + schema_version: int = SCHEMA_VERSION + + def __post_init__(self) -> None: + if self.schema_version != SCHEMA_VERSION: + raise MediaGalleryError( + f"Unsupported schema_version {self.schema_version}; expected " + f"{SCHEMA_VERSION}." + ) + if not self.captures: + raise MediaGalleryError("The manifest must contain at least one capture.") + source_names = tuple(str(capture.source) for capture in self.captures) + if len(set(source_names)) != len(source_names): + raise MediaGalleryError("Manifest capture source paths must be unique.") + output_names = tuple( + output.filename for capture in self.captures for output in capture.outputs + ) + if len(set(output_names)) != len(output_names): + raise MediaGalleryError( + "Manifest output filenames must be globally unique." + ) + + +@dataclass(frozen=True) +class MediaProbe: + """Relevant FFprobe facts for one source or derivative.""" + + width: int + height: int + codec_name: str + duration_seconds: float | None + + +@dataclass(frozen=True) +class WindowGeometry: + """Absolute X11 geometry for a selected visible window.""" + + x: int + y: int + width: int + height: int + + def __post_init__(self) -> None: + if self.width <= 0 or self.height <= 0: + raise MediaGalleryError("Selected window has invalid dimensions.") + + +@dataclass(frozen=True) +class PreparedCapture: + """A source capture that has passed path, existence, probe, and crop checks.""" + + record: CaptureRecord + source_path: Path + output_paths: tuple[Path, ...] + probe: MediaProbe + + +def _expect_object(value: Any, context: str) -> Mapping[str, Any]: + if not isinstance(value, dict): + raise MediaGalleryError(f"{context} must be a JSON object.") + if not all(isinstance(key, str) for key in value): + raise MediaGalleryError(f"{context} keys must be strings.") + return value + + +def _expect_array(value: Any, context: str) -> Sequence[Any]: + if not isinstance(value, list): + raise MediaGalleryError(f"{context} must be a JSON array.") + return value + + +def _validate_keys( + payload: Mapping[str, Any], + *, + required: frozenset[str], + optional: frozenset[str], + context: str, +) -> None: + keys = frozenset(payload) + missing = sorted(required - keys) + unknown = sorted(keys - required - optional) + if missing: + raise MediaGalleryError(f"{context} is missing fields: {', '.join(missing)}.") + if unknown: + raise MediaGalleryError( + f"{context} contains unknown fields: {', '.join(unknown)}." + ) + + +def _number(value: Any, context: str) -> float: + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise MediaGalleryError(f"{context} must be a number.") + return float(value) + + +def _integer(value: Any, context: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise MediaGalleryError(f"{context} must be an integer.") + return value + + +def _optional_integer( + payload: Mapping[str, Any], + field: str, + context: str, +) -> int | None: + if field not in payload: + return None + return _integer(payload[field], f"{context}.{field}") + + +def _optional_number( + payload: Mapping[str, Any], + field: str, + context: str, +) -> float | None: + if field not in payload: + return None + return _number(payload[field], f"{context}.{field}") + + +def _parse_crop(value: Any, context: str) -> Crop: + payload = _expect_object(value, context) + fields = frozenset({"x", "y", "width", "height"}) + _validate_keys(payload, required=fields, optional=frozenset(), context=context) + return Crop( + x=_integer(payload["x"], f"{context}.x"), + y=_integer(payload["y"], f"{context}.y"), + width=_integer(payload["width"], f"{context}.width"), + height=_integer(payload["height"], f"{context}.height"), + ) + + +def _parse_trim(value: Any, context: str) -> Trim: + payload = _expect_object(value, context) + fields = frozenset({"start_seconds", "duration_seconds"}) + _validate_keys(payload, required=fields, optional=frozenset(), context=context) + return Trim( + start_seconds=_number(payload["start_seconds"], f"{context}.start_seconds"), + duration_seconds=_number( + payload["duration_seconds"], + f"{context}.duration_seconds", + ), + ) + + +def _parse_derivative(value: Any, context: str) -> Derivative: + payload = _expect_object(value, context) + _validate_keys( + payload, + required=frozenset({"filename", "max_width", "max_height", "max_bytes"}), + optional=frozenset({"fps", "frame_at_seconds"}), + context=context, + ) + filename = payload["filename"] + if not isinstance(filename, str): + raise MediaGalleryError(f"{context}.filename must be a string.") + return Derivative( + filename=filename, + max_width=_integer(payload["max_width"], f"{context}.max_width"), + max_height=_integer(payload["max_height"], f"{context}.max_height"), + max_bytes=_integer(payload["max_bytes"], f"{context}.max_bytes"), + fps=_optional_integer(payload, "fps", context), + frame_at_seconds=_optional_number(payload, "frame_at_seconds", context), + ) + + +def _parse_capture(value: Any, context: str) -> CaptureRecord: + payload = _expect_object(value, context) + _validate_keys( + payload, + required=frozenset({"source", "outputs"}), + optional=frozenset({"crop", "trim"}), + context=context, + ) + source = payload["source"] + if not isinstance(source, str): + raise MediaGalleryError(f"{context}.source must be a string.") + outputs = tuple( + _parse_derivative(output, f"{context}.outputs[{index}]") + for index, output in enumerate( + _expect_array(payload["outputs"], f"{context}.outputs") + ) + ) + crop = ( + _parse_crop(payload["crop"], f"{context}.crop") if "crop" in payload else None + ) + trim = ( + _parse_trim(payload["trim"], f"{context}.trim") if "trim" in payload else None + ) + return CaptureRecord( + source=Path(source), + outputs=outputs, + crop=crop, + trim=trim, + ) + + +def load_manifest(path: Path) -> CaptureManifest: + """Load and fully validate one capture manifest.""" + + try: + document = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise MediaGalleryError(f"Could not read manifest {path}: {error}") from error + payload = _expect_object(document, "Manifest") + _validate_keys( + payload, + required=frozenset({"schema_version", "captures"}), + optional=frozenset(), + context="Manifest", + ) + return CaptureManifest( + schema_version=_integer(payload["schema_version"], "Manifest.schema_version"), + captures=tuple( + _parse_capture(capture, f"Manifest.captures[{index}]") + for index, capture in enumerate( + _expect_array(payload["captures"], "Manifest.captures") + ) + ), + ) + + +def _validate_relative_path(path: Path, context: str) -> None: + if path.is_absolute() or path == Path(".") or ".." in path.parts: + raise MediaGalleryError( + f"{context} must be a contained relative path without '..': {path}" + ) + + +def resolve_contained_path( + root: Path, + relative_path: Path, + *, + context: str, +) -> Path: + """Resolve a relative path and reject lexical or symlink root escapes.""" + + _validate_relative_path(relative_path, context) + root = root.resolve() + candidate = root.joinpath(relative_path) + if candidate.is_symlink(): + raise MediaGalleryError(f"{context} cannot be a symbolic link: {relative_path}") + resolved_parent = candidate.parent.resolve() + try: + resolved_parent.relative_to(root) + except ValueError as error: + raise MediaGalleryError( + f"{context} escapes its declared root: {relative_path}" + ) from error + return resolved_parent / candidate.name + + +def _require_tool(tool: HostTool) -> str: + executable = shutil.which(tool.value.executable) + if executable is None: + raise MediaGalleryError( + f"Required host tool {tool.value.executable!r} is unavailable. " + "Install FFmpeg for " + "ffmpeg/ffprobe, ImageMagick for magick, or xdotool for X11 window " + "geometry." + ) + return executable + + +def run_checked(command: Sequence[str]) -> subprocess.CompletedProcess[str]: + """Run a command without a shell and return its captured output.""" + + process = subprocess.run( + command, + check=False, + capture_output=True, + text=True, + ) + if process.returncode != 0: + detail = process.stderr.strip() or process.stdout.strip() or "no output" + raise MediaGalleryError( + f"Command failed ({process.returncode}): {' '.join(command)}\n{detail}" + ) + return process + + +def probe_media(path: Path) -> MediaProbe: + """Read dimensions, codec, and duration with FFprobe.""" + + ffprobe = _require_tool(HostTool.FFPROBE) + process = run_checked( + ( + ffprobe, + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "stream=codec_name,width,height,duration:format=duration", + "-of", + "json", + str(path), + ) + ) + try: + payload = json.loads(process.stdout) + stream = payload["streams"][0] + width = int(stream["width"]) + height = int(stream["height"]) + codec_name = str(stream["codec_name"]) + if "duration" in stream: + raw_duration = stream["duration"] + elif "duration" in payload["format"]: + raw_duration = payload["format"]["duration"] + else: + raw_duration = None + duration = None if raw_duration in (None, "N/A") else float(raw_duration) + except (IndexError, KeyError, TypeError, ValueError, json.JSONDecodeError) as error: + raise MediaGalleryError( + f"FFprobe returned incomplete media metadata for {path}." + ) from error + return MediaProbe( + width=width, + height=height, + codec_name=codec_name, + duration_seconds=duration, + ) + + +def _format_seconds(value: float) -> str: + return f"{value:.6f}".rstrip("0").rstrip(".") + + +def _filters(record: CaptureRecord, output: Derivative) -> str: + filters = [] + if record.crop is not None: + filters.append(record.crop.ffmpeg_filter) + filters.append(output.scale_filter) + if output.fps is not None: + filters.append(f"fps={output.fps}") + filters.append("setsar=1") + return ",".join(filters) + + +def build_transcode_command( + record: CaptureRecord, + output: Derivative, + source_path: Path, + target_path: Path, + *, + ffmpeg_executable: str | None = None, +) -> tuple[str, ...]: + """Project a typed record into one deterministic FFmpeg command.""" + + ffmpeg = ( + ffmpeg_executable + if ffmpeg_executable is not None + else _require_tool(HostTool.FFMPEG) + ) + command: list[str] = [ + ffmpeg, + "-hide_banner", + "-loglevel", + "error", + "-nostdin", + "-y", + "-i", + str(source_path), + ] + if record.trim is not None: + seek_seconds = record.trim.start_seconds + if output.frame_at_seconds is not None: + seek_seconds += output.frame_at_seconds + command.extend(("-ss", _format_seconds(seek_seconds))) + if output.encoding.value.category is MediaCategory.MOTION: + command.extend(("-t", _format_seconds(record.trim.duration_seconds))) + command.extend(output.encoding.value.render_filter(_filters(record, output))) + command.extend(output.encoding.value.map_arguments) + command.extend(output.encoding.value.ffmpeg_arguments) + command.extend( + ( + "-bitexact", + "-map_metadata", + "-1", + "-metadata", + "creation_time=1970-01-01T00:00:00Z", + ) + ) + command.append(str(target_path)) + return tuple(command) + + +def _validate_source( + record: CaptureRecord, + source_path: Path, + probe: MediaProbe, +) -> None: + if record.crop is not None and ( + record.crop.x + record.crop.width > probe.width + or record.crop.y + record.crop.height > probe.height + ): + raise MediaGalleryError( + f"Crop for {record.source} exceeds its {probe.width}x{probe.height} " + "source bounds." + ) + if record.trim is not None: + if probe.duration_seconds is None: + raise MediaGalleryError( + f"Motion source {source_path} has no probeable duration." + ) + if record.trim.end_seconds > probe.duration_seconds + 0.02: + raise MediaGalleryError( + f"Trim ending at {record.trim.end_seconds:g}s exceeds source " + f"duration {probe.duration_seconds:g}s." + ) + + +def validate_derivative( + record: CaptureRecord, + output: Derivative, + target_path: Path, + probe: MediaProbe | None = None, +) -> dict[str, Any]: + """Validate one derivative and return reproducibility facts.""" + + if not target_path.is_file(): + raise MediaGalleryError(f"Missing derived media: {target_path}") + size_bytes = target_path.stat().st_size + if size_bytes > output.max_bytes: + raise MediaGalleryError( + f"{output.filename} is {size_bytes} bytes, above its manifest bound " + f"of {output.max_bytes}." + ) + probe = probe if probe is not None else probe_media(target_path) + if probe.width > output.max_width or probe.height > output.max_height: + raise MediaGalleryError( + f"{output.filename} is {probe.width}x{probe.height}, above its " + f"{output.max_width}x{output.max_height} manifest bounds." + ) + if probe.codec_name != output.encoding.value.codec_name: + raise MediaGalleryError( + f"{output.filename} codec is {probe.codec_name!r}, expected " + f"{output.encoding.value.codec_name!r}." + ) + if output.encoding.value.category is MediaCategory.MOTION: + if probe.duration_seconds is None: + raise MediaGalleryError( + f"Motion derivative {output.filename} has no duration." + ) + assert record.trim is not None + tolerance = max(0.1, 2.0 / (output.fps or 1)) + if abs(probe.duration_seconds - record.trim.duration_seconds) > tolerance: + raise MediaGalleryError( + f"{output.filename} duration is {probe.duration_seconds:g}s; " + f"expected {record.trim.duration_seconds:g}s within " + f"{tolerance:g}s." + ) + return { + "path": output.filename, + "bytes": size_bytes, + "sha256": sha256_file(target_path), + "width": probe.width, + "height": probe.height, + "codec": probe.codec_name, + "duration_seconds": probe.duration_seconds, + } + + +def sha256_file(path: Path) -> str: + """Hash one source or derivative without loading it into memory.""" + + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _paths_for_capture( + record: CaptureRecord, + source_root: Path, + output_root: Path, +) -> tuple[Path, tuple[Path, ...]]: + source_path = resolve_contained_path( + source_root, + record.source, + context="Capture source", + ) + output_paths = tuple( + resolve_contained_path( + output_root, + output.path, + context="Derived output", + ) + for output in record.outputs + ) + if source_path in output_paths: + raise MediaGalleryError( + f"Refusing to overwrite source capture with a derivative: {source_path}" + ) + return source_path, output_paths + + +def plan_manifest( + manifest: CaptureManifest, + source_root: Path, + output_root: Path, +) -> tuple[dict[str, Any], ...]: + """Return exact commands without writing or probing media.""" + + plans = [] + for record in manifest.captures: + source_path, output_paths = _paths_for_capture( + record, + source_root, + output_root, + ) + ffmpeg_name = HostTool.FFMPEG.value.executable + ffmpeg = shutil.which(ffmpeg_name) or ffmpeg_name + commands = [ + list( + build_transcode_command( + record, + output, + source_path, + target_path, + ffmpeg_executable=ffmpeg, + ) + ) + for output, target_path in zip( + record.outputs, + output_paths, + strict=True, + ) + ] + plans.append( + { + "source": str(source_path), + "outputs": [str(path) for path in output_paths], + "commands": commands, + } + ) + return tuple(plans) + + +def _temporary_target(target_path: Path) -> Path: + target_path.parent.mkdir(parents=True, exist_ok=True) + file_descriptor, temporary_name = tempfile.mkstemp( + prefix=f".{target_path.stem}-", + suffix=target_path.suffix, + dir=target_path.parent, + ) + os.close(file_descriptor) + temporary_path = Path(temporary_name) + temporary_path.unlink() + return temporary_path + + +def _prepare_captures( + manifest: CaptureManifest, + source_root: Path, + output_root: Path, +) -> tuple[PreparedCapture, ...]: + prepared = [] + for record in manifest.captures: + source_path, output_paths = _paths_for_capture( + record, + source_root, + output_root, + ) + if not source_path.is_file(): + raise MediaGalleryError(f"Source capture does not exist: {source_path}") + source_probe = probe_media(source_path) + _validate_source(record, source_path, source_probe) + prepared.append( + PreparedCapture( + record=record, + source_path=source_path, + output_paths=output_paths, + probe=source_probe, + ) + ) + return tuple(prepared) + + +def build_manifest( + manifest: CaptureManifest, + source_root: Path, + output_root: Path, + *, + force: bool = False, +) -> tuple[dict[str, Any], ...]: + """Build all derivatives atomically and validate every manifest bound.""" + + prepared_captures = _prepare_captures(manifest, source_root, output_root) + if not force: + existing_outputs = tuple( + target_path + for prepared in prepared_captures + for target_path in prepared.output_paths + if target_path.exists() + ) + if existing_outputs: + formatted_paths = "\n".join(f"- {path}" for path in existing_outputs) + raise MediaGalleryError( + "Derived outputs already exist. Pass --force only after reviewing " + f"the sources and manifest:\n{formatted_paths}" + ) + + report = [] + for prepared in prepared_captures: + record = prepared.record + source_path = prepared.source_path + source_probe = prepared.probe + source_report = { + "path": str(record.source), + "sha256": sha256_file(source_path), + "width": source_probe.width, + "height": source_probe.height, + "duration_seconds": source_probe.duration_seconds, + } + outputs_report = [] + for output, target_path in zip( + record.outputs, + prepared.output_paths, + strict=True, + ): + temporary_path = _temporary_target(target_path) + try: + command = build_transcode_command( + record, + output, + source_path, + temporary_path, + ) + run_checked(command) + output_report = validate_derivative( + record, + output, + temporary_path, + ) + os.replace(temporary_path, target_path) + finally: + temporary_path.unlink(missing_ok=True) + outputs_report.append(output_report) + report.append({"source": source_report, "outputs": outputs_report}) + return tuple(report) + + +def validate_manifest_outputs( + manifest: CaptureManifest, + source_root: Path, + output_root: Path, +) -> tuple[dict[str, Any], ...]: + """Validate existing sources and derivatives without modifying them.""" + + report = [] + for prepared in _prepare_captures(manifest, source_root, output_root): + record = prepared.record + source_path = prepared.source_path + outputs_report = tuple( + validate_derivative(record, output, target_path) + for output, target_path in zip( + record.outputs, + prepared.output_paths, + strict=True, + ) + ) + report.append( + { + "source": { + "path": str(record.source), + "sha256": sha256_file(source_path), + }, + "outputs": outputs_report, + } + ) + return tuple(report) + + +def _validate_window_id(window_id: str) -> str: + if not WINDOW_ID_PATTERN.fullmatch(window_id): + raise MediaGalleryError( + "Window ID must be a positive decimal X11 ID or hexadecimal 0x ID." + ) + return window_id + + +def read_window_geometry(window_id: str) -> WindowGeometry: + """Read the selected X11 window's absolute geometry through xdotool.""" + + xdotool = _require_tool(HostTool.XDOTOOL) + process = run_checked( + (xdotool, "getwindowgeometry", "--shell", _validate_window_id(window_id)) + ) + values: dict[str, int] = {} + for line in process.stdout.splitlines(): + key, separator, raw_value = line.partition("=") + if separator and key in {"X", "Y", "WIDTH", "HEIGHT"}: + try: + values[key] = int(raw_value) + except ValueError as error: + raise MediaGalleryError( + f"xdotool returned invalid {key} geometry." + ) from error + missing = sorted({"X", "Y", "WIDTH", "HEIGHT"} - values.keys()) + if missing: + raise MediaGalleryError( + f"xdotool did not report complete window geometry: {', '.join(missing)}." + ) + return WindowGeometry( + x=values["X"], + y=values["Y"], + width=values["WIDTH"], + height=values["HEIGHT"], + ) + + +def _capture_target( + source_root: Path, + output: Path, + expected_suffix: str, +) -> Path: + target_path = resolve_contained_path( + source_root, + output, + context="Raw capture output", + ) + if target_path.suffix.lower() != expected_suffix: + raise MediaGalleryError( + f"Raw capture output must use {expected_suffix}: {output}" + ) + if target_path.exists(): + raise MediaGalleryError(f"Refusing to overwrite source capture: {target_path}") + return target_path + + +def capture_window_still( + source_root: Path, + output: Path, + window_id: str, +) -> dict[str, Any]: + """Capture a real X11 window losslessly without overwriting any source.""" + + magick = _require_tool(HostTool.MAGICK) + target_path = _capture_target(source_root, output, ".png") + temporary_path = _temporary_target(target_path) + try: + run_checked( + ( + magick, + "import", + "-window", + _validate_window_id(window_id), + str(temporary_path), + ) + ) + probe = probe_media(temporary_path) + os.replace(temporary_path, target_path) + finally: + temporary_path.unlink(missing_ok=True) + return { + "path": str(output), + "sha256": sha256_file(target_path), + "width": probe.width, + "height": probe.height, + } + + +def record_window( + source_root: Path, + output: Path, + window_id: str, + *, + duration_seconds: float, + fps: int, + display: str, +) -> dict[str, Any]: + """Record a fixed real X11 window rectangle into a lossless FFV1 source.""" + + if ( + not math.isfinite(duration_seconds) + or duration_seconds <= 0 + or duration_seconds > MAX_RAW_RECORDING_SECONDS + ): + raise MediaGalleryError( + "Raw recording duration must be greater than zero and no more than " + f"{MAX_RAW_RECORDING_SECONDS:g} seconds." + ) + if not 1 <= fps <= 60: + raise MediaGalleryError("Raw recording fps must be between 1 and 60.") + if not DISPLAY_PATTERN.fullmatch(display): + raise MediaGalleryError(f"Invalid X11 DISPLAY value: {display!r}") + target_path = _capture_target(source_root, output, ".mkv") + geometry = read_window_geometry(window_id) + ffmpeg = _require_tool(HostTool.FFMPEG) + temporary_path = _temporary_target(target_path) + try: + run_checked( + ( + ffmpeg, + "-hide_banner", + "-loglevel", + "error", + "-nostdin", + "-y", + "-f", + "x11grab", + "-draw_mouse", + "1", + "-framerate", + str(fps), + "-video_size", + f"{geometry.width}x{geometry.height}", + "-i", + f"{display}+{geometry.x},{geometry.y}", + "-t", + _format_seconds(duration_seconds), + "-an", + "-c:v", + "ffv1", + "-level", + "3", + "-g", + "1", + "-threads", + "1", + str(temporary_path), + ) + ) + probe = probe_media(temporary_path) + os.replace(temporary_path, target_path) + finally: + temporary_path.unlink(missing_ok=True) + return { + "path": str(output), + "sha256": sha256_file(target_path), + "width": probe.width, + "height": probe.height, + "duration_seconds": probe.duration_seconds, + } + + +def doctor() -> dict[str, Any]: + """Report availability and versions from the typed host-tool authority.""" + + capabilities = {} + for tool in HostTool: + executable = shutil.which(tool.value.executable) + version = None + if executable is not None: + process = run_checked((executable, *tool.value.version_arguments)) + lines = process.stdout.splitlines() or process.stderr.splitlines() + version = lines[0] if lines else "version command returned no text" + capabilities[tool.value.executable] = { + "available": executable is not None, + "path": executable, + "version": version, + } + return {"schema_version": SCHEMA_VERSION, "tools": capabilities} + + +def _write_json(value: Any) -> None: + print(json.dumps(value, indent=2, sort_keys=True)) + + +def _manifest_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--source-root", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + + +def _doctor_operation(_arguments: argparse.Namespace) -> dict[str, Any]: + return doctor() + + +def _plan_operation(arguments: argparse.Namespace) -> tuple[dict[str, Any], ...]: + return plan_manifest( + load_manifest(arguments.manifest), + arguments.source_root, + arguments.output_root, + ) + + +def _build_operation(arguments: argparse.Namespace) -> tuple[dict[str, Any], ...]: + return build_manifest( + load_manifest(arguments.manifest), + arguments.source_root, + arguments.output_root, + force=arguments.force, + ) + + +def _validate_operation(arguments: argparse.Namespace) -> tuple[dict[str, Any], ...]: + return validate_manifest_outputs( + load_manifest(arguments.manifest), + arguments.source_root, + arguments.output_root, + ) + + +def _capture_still_operation(arguments: argparse.Namespace) -> dict[str, Any]: + return capture_window_still( + arguments.source_root, + arguments.output, + arguments.window_id, + ) + + +def _record_window_operation(arguments: argparse.Namespace) -> dict[str, Any]: + return record_window( + arguments.source_root, + arguments.output, + arguments.window_id, + duration_seconds=arguments.duration_seconds, + fps=arguments.fps, + display=arguments.display, + ) + + +def build_parser() -> argparse.ArgumentParser: + """Build the command-line parser.""" + + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + + doctor_parser = subparsers.add_parser( + "doctor", + help="Report capture and encoding tools.", + ) + doctor_parser.set_defaults(operation=_doctor_operation) + + plan_parser = subparsers.add_parser( + "plan", + help="Validate the manifest and print exact commands without writing.", + ) + _manifest_arguments(plan_parser) + plan_parser.set_defaults(operation=_plan_operation) + + build_parser_instance = subparsers.add_parser( + "build", + help="Build and validate manifest derivatives atomically.", + ) + _manifest_arguments(build_parser_instance) + build_parser_instance.add_argument( + "--force", + action="store_true", + help="Atomically replace existing derived outputs, never source captures.", + ) + build_parser_instance.set_defaults(operation=_build_operation) + + validate_parser = subparsers.add_parser( + "validate", + help="Probe and validate existing source and derivative media.", + ) + _manifest_arguments(validate_parser) + validate_parser.set_defaults(operation=_validate_operation) + + still_parser = subparsers.add_parser( + "capture-still", + help="Capture one real X11 window into a lossless PNG source.", + ) + still_parser.add_argument("--source-root", type=Path, required=True) + still_parser.add_argument("--output", type=Path, required=True) + still_parser.add_argument("--window-id", required=True) + still_parser.set_defaults(operation=_capture_still_operation) + + record_parser = subparsers.add_parser( + "record-window", + help="Record one fixed real X11 window rectangle into lossless FFV1 MKV.", + ) + record_parser.add_argument("--source-root", type=Path, required=True) + record_parser.add_argument("--output", type=Path, required=True) + record_parser.add_argument("--window-id", required=True) + record_parser.add_argument("--duration-seconds", type=float, required=True) + record_parser.add_argument("--fps", type=int, default=30) + record_parser.add_argument( + "--display", + default=os.environ.get("DISPLAY", ":0"), + help="X11 display used by FFmpeg x11grab.", + ) + record_parser.set_defaults(operation=_record_window_operation) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + """Run the selected capture/transcoding command.""" + + arguments = build_parser().parse_args(argv) + try: + _write_json(arguments.operation(arguments)) + return 0 + except MediaGalleryError as error: + print(f"error: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/test_capture_media_gallery.py b/tests/unit/test_capture_media_gallery.py new file mode 100644 index 000000000..38fb6ceca --- /dev/null +++ b/tests/unit/test_capture_media_gallery.py @@ -0,0 +1,504 @@ +from __future__ import annotations + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +from scripts.capture_media_gallery import ( + CaptureManifest, + CaptureRecord, + Crop, + Derivative, + MediaGalleryError, + MediaProbe, + Trim, + WindowGeometry, + _capture_target, + build_manifest, + build_transcode_command, + capture_window_still, + load_manifest, + plan_manifest, + record_window, + resolve_contained_path, + validate_derivative, + validate_manifest_outputs, +) + + +def _still_output(filename: str = "interface-overview.webp") -> Derivative: + return Derivative( + filename=filename, + max_width=1600, + max_height=1000, + max_bytes=2_000_000, + ) + + +def _motion_record(*outputs: Derivative) -> CaptureRecord: + return CaptureRecord( + source=Path("raw/session.mkv"), + crop=Crop(x=4, y=6, width=1200, height=760), + trim=Trim(start_seconds=1.25, duration_seconds=8.0), + outputs=outputs, + ) + + +def test_load_manifest_builds_typed_authority_and_rejects_unknown_fields( + tmp_path: Path, +) -> None: + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text( + json.dumps( + { + "schema_version": 1, + "captures": [ + { + "source": "raw/interface.png", + "crop": {"x": 10, "y": 12, "width": 800, "height": 500}, + "outputs": [ + { + "filename": "interface-overview.webp", + "max_width": 800, + "max_height": 500, + "max_bytes": 500000, + } + ], + } + ], + } + ), + encoding="utf-8", + ) + + manifest = load_manifest(manifest_path) + + assert manifest.schema_version == 1 + assert manifest.captures[0].crop == Crop(10, 12, 800, 500) + assert manifest.captures[0].outputs[0].filename == "interface-overview.webp" + + document = json.loads(manifest_path.read_text(encoding="utf-8")) + document["captures"][0]["caption"] = "This belongs to the website." + manifest_path.write_text(json.dumps(document), encoding="utf-8") + with pytest.raises(MediaGalleryError, match="unknown fields: caption"): + load_manifest(manifest_path) + + +@pytest.mark.parametrize( + "source", + ( + Path("../private.png"), + Path("/tmp/private.png"), + Path("."), + ), +) +def test_capture_source_must_be_contained_relative_path(source: Path) -> None: + with pytest.raises(MediaGalleryError, match="contained relative path"): + CaptureRecord(source=source, outputs=(_still_output(),)) + + +def test_containment_rejects_symlink_escape(tmp_path: Path) -> None: + root = tmp_path / "root" + root.mkdir() + outside = tmp_path / "outside" + outside.mkdir() + (root / "escape").symlink_to(outside, target_is_directory=True) + + with pytest.raises(MediaGalleryError, match="escapes its declared root"): + resolve_contained_path( + root, + Path("escape/private.png"), + context="Capture source", + ) + + +@pytest.mark.parametrize( + "filename", + ( + "Has Spaces.webp", + "camelCase.webp", + "../escape.webp", + "double--hyphen.webp", + "unsupported.png", + ), +) +def test_output_names_are_caption_safe_and_format_owned(filename: str) -> None: + with pytest.raises(MediaGalleryError): + _still_output(filename) + + +def test_motion_manifest_requires_bounded_trim_and_typed_output_fields() -> None: + motion = Derivative( + filename="lazy-update.webm", + max_width=1280, + max_height=800, + max_bytes=3_000_000, + fps=24, + ) + with pytest.raises(MediaGalleryError, match="explicit bounded trim"): + CaptureRecord(source=Path("raw/session.mkv"), outputs=(motion,)) + + with pytest.raises(MediaGalleryError, match="must declare fps"): + _motion_record( + Derivative( + filename="lazy-update.mp4", + max_width=1280, + max_height=800, + max_bytes=3_000_000, + ) + ) + + with pytest.raises(MediaGalleryError, match="frame_at_seconds"): + _motion_record(_still_output("lazy-update-poster.webp")) + + +def test_plan_is_a_write_free_exact_command_projection(tmp_path: Path) -> None: + poster = Derivative( + filename="lazy-update-poster.webp", + max_width=1280, + max_height=800, + max_bytes=1_000_000, + frame_at_seconds=2.5, + ) + webm = Derivative( + filename="lazy-update.webm", + max_width=1280, + max_height=800, + max_bytes=3_000_000, + fps=24, + ) + manifest = CaptureManifest(captures=(_motion_record(poster, webm),)) + source_root = tmp_path / "sources" + output_root = tmp_path / "outputs" + + plan = plan_manifest(manifest, source_root, output_root) + + assert not source_root.exists() + assert not output_root.exists() + poster_command, webm_command = plan[0]["commands"] + assert "-ss" in poster_command + assert poster_command[poster_command.index("-ss") + 1] == "3.75" + assert "-t" not in poster_command + assert "-t" in webm_command + assert webm_command[webm_command.index("-t") + 1] == "8" + poster_filters = poster_command[poster_command.index("-vf") + 1] + assert "crop=1200:760:4:6" in poster_filters + assert "force_original_aspect_ratio=decrease" in poster_filters + assert "fps=" not in poster_filters + webm_filters = webm_command[webm_command.index("-vf") + 1] + assert "fps=24" in webm_filters + + +def test_gif_encoding_owns_palette_filter_and_output_mapping() -> None: + output = Derivative( + filename="lazy-update.gif", + max_width=960, + max_height=600, + max_bytes=8_000_000, + fps=12, + ) + record = _motion_record(output) + + command = build_transcode_command( + record, + output, + Path("/captures/session.mkv"), + Path("/gallery/lazy-update.gif"), + ffmpeg_executable="ffmpeg", + ) + + assert "-filter_complex" in command + filter_graph = command[command.index("-filter_complex") + 1] + assert "palettegen=max_colors=192" in filter_graph + assert filter_graph.endswith("[gallery_output]") + assert command[command.index("-map") + 1] == "[gallery_output]" + + +def test_manifest_refuses_to_overwrite_source_capture(tmp_path: Path) -> None: + record = CaptureRecord( + source=Path("same.webp"), + outputs=(_still_output("same.webp"),), + ) + manifest = CaptureManifest(captures=(record,)) + + with pytest.raises(MediaGalleryError, match="overwrite source capture"): + plan_manifest(manifest, tmp_path, tmp_path) + + +def test_raw_capture_never_overwrites_existing_source(tmp_path: Path) -> None: + target = tmp_path / "raw" / "session.png" + target.parent.mkdir() + target.write_bytes(b"original") + + with pytest.raises(MediaGalleryError, match="Refusing to overwrite source"): + _capture_target(tmp_path, Path("raw/session.png"), ".png") + assert target.read_bytes() == b"original" + + +def test_capture_still_projects_real_window_without_pixel_editing( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + commands: list[tuple[str, ...]] = [] + + def fake_run(command: tuple[str, ...]) -> subprocess.CompletedProcess[str]: + commands.append(tuple(command)) + Path(command[-1]).write_bytes(b"lossless-png") + return subprocess.CompletedProcess(command, 0, "", "") + + monkeypatch.setattr( + "scripts.capture_media_gallery._require_tool", + lambda _tool: "magick", + ) + monkeypatch.setattr("scripts.capture_media_gallery.run_checked", fake_run) + monkeypatch.setattr( + "scripts.capture_media_gallery.probe_media", + lambda _path: MediaProbe(1200, 800, "png", None), + ) + + report = capture_window_still( + tmp_path, + Path("raw/interface.png"), + "0x123", + ) + + assert commands[0][1:4] == ("import", "-window", "0x123") + assert (tmp_path / "raw" / "interface.png").read_bytes() == b"lossless-png" + assert report["width"] == 1200 + + +def test_record_window_projects_fixed_geometry_and_lossless_ffv1( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + commands: list[tuple[str, ...]] = [] + + def fake_run(command: tuple[str, ...]) -> subprocess.CompletedProcess[str]: + commands.append(tuple(command)) + Path(command[-1]).write_bytes(b"lossless-ffv1") + return subprocess.CompletedProcess(command, 0, "", "") + + monkeypatch.setattr( + "scripts.capture_media_gallery._require_tool", + lambda _tool: "ffmpeg", + ) + monkeypatch.setattr( + "scripts.capture_media_gallery.read_window_geometry", + lambda _window_id: WindowGeometry(x=11, y=17, width=1400, height=900), + ) + monkeypatch.setattr("scripts.capture_media_gallery.run_checked", fake_run) + monkeypatch.setattr( + "scripts.capture_media_gallery.probe_media", + lambda _path: MediaProbe(1400, 900, "ffv1", 12.0), + ) + + report = record_window( + tmp_path, + Path("raw/interaction.mkv"), + "0x123", + duration_seconds=12.0, + fps=30, + display=":9.0", + ) + + command = commands[0] + assert command[command.index("-f") + 1] == "x11grab" + assert command[command.index("-video_size") + 1] == "1400x900" + assert command[command.index("-i") + 1] == ":9.0+11,17" + assert command[command.index("-c:v") + 1] == "ffv1" + assert (tmp_path / "raw" / "interaction.mkv").read_bytes() == b"lossless-ffv1" + assert report["duration_seconds"] == 12.0 + + +def test_build_preflights_every_existing_output_before_writing( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + source_root = tmp_path / "sources" + output_root = tmp_path / "outputs" + source_path = source_root / "raw" / "session.png" + source_path.parent.mkdir(parents=True) + source_path.write_bytes(b"lossless-source") + output_root.mkdir() + existing_output = output_root / "second.webp" + existing_output.write_bytes(b"accepted-output") + manifest = CaptureManifest( + captures=( + CaptureRecord( + source=Path("raw/session.png"), + outputs=( + _still_output("first.webp"), + _still_output("second.webp"), + ), + ), + ) + ) + monkeypatch.setattr( + "scripts.capture_media_gallery.probe_media", + lambda _path: MediaProbe(800, 500, "png", None), + ) + + with pytest.raises(MediaGalleryError, match="Derived outputs already exist"): + build_manifest(manifest, source_root, output_root) + + assert not (output_root / "first.webp").exists() + assert existing_output.read_bytes() == b"accepted-output" + + +def test_derivative_validation_enforces_size_dimensions_codec_and_duration( + tmp_path: Path, +) -> None: + output = Derivative( + filename="lazy-update.webm", + max_width=1280, + max_height=800, + max_bytes=100, + fps=20, + ) + record = _motion_record(output) + target = tmp_path / output.filename + target.write_bytes(b"x" * 50) + + report = validate_derivative( + record, + output, + target, + probe=MediaProbe( + width=1200, + height=760, + codec_name="vp9", + duration_seconds=8.0, + ), + ) + assert report["bytes"] == 50 + assert report["width"] == 1200 + + with pytest.raises(MediaGalleryError, match="above its 1280x800 manifest bounds"): + validate_derivative( + record, + output, + target, + probe=MediaProbe( + width=1300, + height=760, + codec_name="vp9", + duration_seconds=8.0, + ), + ) + with pytest.raises(MediaGalleryError, match="expected 'vp9'"): + validate_derivative( + record, + output, + target, + probe=MediaProbe( + width=1200, + height=760, + codec_name="h264", + duration_seconds=8.0, + ), + ) + with pytest.raises(MediaGalleryError, match="duration is"): + validate_derivative( + record, + output, + target, + probe=MediaProbe( + width=1200, + height=760, + codec_name="vp9", + duration_seconds=6.0, + ), + ) + + +@pytest.mark.skipif( + not shutil.which("ffmpeg") or not shutil.which("ffprobe"), + reason="FFmpeg is an optional host capture dependency", +) +def test_real_ffmpeg_derivatives_are_bounded_and_revalidate(tmp_path: Path) -> None: + source_root = tmp_path / "sources" + output_root = tmp_path / "outputs" + source_path = source_root / "raw" / "session.mkv" + source_path.parent.mkdir(parents=True) + subprocess.run( + ( + shutil.which("ffmpeg") or "ffmpeg", + "-hide_banner", + "-loglevel", + "error", + "-f", + "lavfi", + "-i", + "testsrc2=size=320x180:rate=20:duration=2", + "-c:v", + "ffv1", + "-threads", + "1", + str(source_path), + ), + check=True, + ) + outputs = ( + Derivative( + filename="session-poster.webp", + max_width=240, + max_height=140, + max_bytes=500_000, + frame_at_seconds=0.25, + ), + Derivative( + filename="session.mp4", + max_width=240, + max_height=140, + max_bytes=1_000_000, + fps=10, + ), + Derivative( + filename="session.webm", + max_width=240, + max_height=140, + max_bytes=1_000_000, + fps=10, + ), + Derivative( + filename="session.gif", + max_width=240, + max_height=140, + max_bytes=2_000_000, + fps=10, + ), + ) + manifest = CaptureManifest( + captures=( + CaptureRecord( + source=Path("raw/session.mkv"), + trim=Trim(start_seconds=0.25, duration_seconds=1.0), + outputs=outputs, + ), + ) + ) + + build_report = build_manifest(manifest, source_root, output_root) + validation_report = validate_manifest_outputs( + manifest, + source_root, + output_root, + ) + rebuilt_report = build_manifest( + manifest, + source_root, + output_root, + force=True, + ) + + assert {item["path"] for item in build_report[0]["outputs"]} == { + output.filename for output in outputs + } + assert {item["path"]: item["sha256"] for item in build_report[0]["outputs"]} == { + item["path"]: item["sha256"] for item in rebuilt_report[0]["outputs"] + } + assert validation_report[0]["source"]["sha256"] + assert all((output_root / output.filename).is_file() for output in outputs) From d923f53c1278771c17710eed30dc0cd86fd6c0c3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 31 Jul 2026 02:21:29 -0400 Subject: [PATCH 3/3] Show real workflows in the website gallery Replace the single interface still with responsive, accessible media from a verified seven-assay OpenHCS session. Include native Napari and Fiji review, local MCP state, lazy inheritance, execution, and result-review motion with reduced-motion posters and a shareable GIF. Derive published media from the HTML references so unrelated gallery files cannot ship accidentally. --- scripts/build_website.py | 38 ++- tests/unit/test_build_website.py | 192 +++++++++++-- .../gallery/execution-progress-poster.webp | Bin 0 -> 158336 bytes website/assets/gallery/execution-progress.mp4 | Bin 0 -> 781129 bytes .../assets/gallery/execution-progress.webm | Bin 0 -> 1266265 bytes website/assets/gallery/fiji-review.webp | Bin 0 -> 150594 bytes .../gallery/lazy-inheritance-poster.webp | Bin 0 -> 79658 bytes website/assets/gallery/lazy-inheritance.gif | Bin 0 -> 328324 bytes website/assets/gallery/lazy-inheritance.mp4 | Bin 0 -> 442265 bytes website/assets/gallery/lazy-inheritance.webm | Bin 0 -> 407940 bytes website/assets/gallery/mcp-control.webp | Bin 0 -> 29474 bytes .../assets/gallery/multi-plate-overview.webp | Bin 0 -> 158450 bytes website/assets/gallery/napari-review.webp | Bin 0 -> 126222 bytes website/assets/gallery/pipeline-editor.webp | Bin 0 -> 84064 bytes .../assets/gallery/result-review-poster.webp | Bin 0 -> 110258 bytes website/assets/gallery/result-review.mp4 | Bin 0 -> 419140 bytes website/assets/gallery/result-review.webm | Bin 0 -> 480060 bytes website/index.html | 267 ++++++++++++++++-- website/styles.css | 169 +++++++++-- 19 files changed, 596 insertions(+), 70 deletions(-) create mode 100644 website/assets/gallery/execution-progress-poster.webp create mode 100644 website/assets/gallery/execution-progress.mp4 create mode 100644 website/assets/gallery/execution-progress.webm create mode 100644 website/assets/gallery/fiji-review.webp create mode 100644 website/assets/gallery/lazy-inheritance-poster.webp create mode 100644 website/assets/gallery/lazy-inheritance.gif create mode 100644 website/assets/gallery/lazy-inheritance.mp4 create mode 100644 website/assets/gallery/lazy-inheritance.webm create mode 100644 website/assets/gallery/mcp-control.webp create mode 100644 website/assets/gallery/multi-plate-overview.webp create mode 100644 website/assets/gallery/napari-review.webp create mode 100644 website/assets/gallery/pipeline-editor.webp create mode 100644 website/assets/gallery/result-review-poster.webp create mode 100644 website/assets/gallery/result-review.mp4 create mode 100644 website/assets/gallery/result-review.webm diff --git a/scripts/build_website.py b/scripts/build_website.py index c8fb64061..d0606b359 100644 --- a/scripts/build_website.py +++ b/scripts/build_website.py @@ -41,7 +41,6 @@ "assets/logos/openhcs-stacked.svg": ( "openhcs/resources/assets/openhcs-lockup-stacked.svg" ), - "assets/ui.png": "docs/source/_static/ui.png", } REQUIRED_COPY = ( "PyPI", @@ -160,12 +159,42 @@ def handle_starttag( if element_id in self.ids: self.duplicate_ids.add(element_id) self.ids.add(element_id) - for attribute in ("href", "src"): + for attribute in ("href", "src", "poster"): value = attributes.get(attribute) if value: self.references.append((attribute, value)) +def referenced_source_files(source_dir: Path) -> tuple[str, ...]: + """Return local files selected by the website documents themselves. + + HTML owns which downloadable and browser-loaded media ship. Deriving the + copy set from those references prevents a second asset inventory and avoids + publishing unrelated files merely because they share an asset directory. + """ + + source_dir = source_dir.resolve() + referenced: set[str] = set() + for relative_name in HTML_SOURCE_FILES: + document_path = source_dir / relative_name + collector = _ReferenceCollector() + collector.feed(document_path.read_text(encoding="utf-8")) + for _attribute, reference in collector.references: + parsed = urlsplit(reference) + if ( + parsed.scheme + or reference.startswith("//") + or parsed.path.startswith("/") + or not parsed.path + ): + continue + candidate = (document_path.parent / unquote(parsed.path)).resolve() + if not candidate.is_relative_to(source_dir) or not candidate.is_file(): + continue + referenced.add(candidate.relative_to(source_dir).as_posix()) + return tuple(sorted(referenced)) + + def _safe_output(repo_root: Path, output_dir: Path) -> Path: repo_root = repo_root.resolve() output_dir = output_dir.resolve() @@ -276,7 +305,10 @@ def build_site(repo_root: Path, output_dir: Path) -> tuple[str, ...]: shutil.rmtree(output_dir) output_dir.mkdir(parents=True) - for relative_name in SOURCE_FILES: + source_files = tuple( + dict.fromkeys((*SOURCE_FILES, *referenced_source_files(source_dir))) + ) + for relative_name in source_files: source = source_dir / relative_name destination = output_dir / relative_name if not source.is_file(): diff --git a/tests/unit/test_build_website.py b/tests/unit/test_build_website.py index 4ef3660a4..8e2da5dc0 100644 --- a/tests/unit/test_build_website.py +++ b/tests/unit/test_build_website.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +from html.parser import HTMLParser from pathlib import Path import pytest @@ -18,7 +19,48 @@ REPO_ROOT = Path(__file__).resolve().parents[2] -def test_build_site_stages_authoritative_screenshot_and_valid_references( +class _GalleryMarkupCollector(HTMLParser): + """Collect the semantic media surface without a browser dependency.""" + + def __init__(self) -> None: + super().__init__(convert_charrefs=True) + self.in_gallery = False + self.figures = 0 + self.figcaptions = 0 + self.images: list[dict[str, str | None]] = [] + self.videos: list[dict[str, str | None]] = [] + self.sources: list[dict[str, str | None]] = [] + self.links: list[dict[str, str | None]] = [] + + def handle_starttag( + self, + tag: str, + attrs: list[tuple[str, str | None]], + ) -> None: + attributes = dict(attrs) + if tag == "section" and attributes.get("id") == "gallery": + self.in_gallery = True + if not self.in_gallery: + return + if tag == "figure": + self.figures += 1 + elif tag == "figcaption": + self.figcaptions += 1 + elif tag == "img": + self.images.append(attributes) + elif tag == "video": + self.videos.append(attributes) + elif tag == "source": + self.sources.append(attributes) + elif tag == "a": + self.links.append(attributes) + + def handle_endtag(self, tag: str) -> None: + if self.in_gallery and tag == "section": + self.in_gallery = False + + +def test_build_site_stages_authoritative_media_and_valid_references( tmp_path: Path, ): site_dir = tmp_path / "site" @@ -26,9 +68,10 @@ def test_build_site_stages_authoritative_screenshot_and_valid_references( local_targets = build_site(REPO_ROOT, site_dir) assert (site_dir / ".nojekyll").is_file() - assert (site_dir / "assets/ui.png").read_bytes() == ( - REPO_ROOT / "docs/source/_static/ui.png" - ).read_bytes() + gallery_sources = tuple(sorted((REPO_ROOT / "website/assets/gallery").iterdir())) + for source in gallery_sources: + relative_name = source.relative_to(REPO_ROOT / "website") + assert (site_dir / relative_name).read_bytes() == source.read_bytes() for logo_name in ( "bioformats.svg", "cellprofiler.png", @@ -48,7 +91,7 @@ def test_build_site_stages_authoritative_screenshot_and_valid_references( assert (site_dir / output_name).read_bytes() == ( REPO_ROOT / source_name ).read_bytes() - assert local_targets == ( + expected_non_gallery_targets = ( "assets/logos/bioformats.svg", "assets/logos/cellprofiler.png", "assets/logos/cupy.svg", @@ -61,7 +104,6 @@ def test_build_site_stages_authoritative_screenshot_and_valid_references( "assets/logos/pyclesperanto.png", "assets/logos/pytorch.svg", "assets/logos/tensorflow.svg", - "assets/ui.png", "globals.css", "index.html", "privacy.html", @@ -69,6 +111,28 @@ def test_build_site_stages_authoritative_screenshot_and_valid_references( "support.html", "terms.html", ) + assert ( + tuple( + target + for target in local_targets + if not target.startswith("assets/gallery/") + ) + == expected_non_gallery_targets + ) + collector = _GalleryMarkupCollector() + collector.feed((site_dir / "index.html").read_text(encoding="utf-8")) + gallery_references = { + attributes["src"] for attributes in (*collector.images, *collector.sources) + } + gallery_references.add(collector.videos[0]["poster"]) + gallery_references.update( + link["href"] + for link in collector.links + if link.get("href", "").startswith("assets/gallery/") + ) + assert { + target for target in local_targets if target.startswith("assets/gallery/") + } == gallery_references assert validate_site(site_dir) == local_targets @@ -133,7 +197,8 @@ def test_shipping_copy_projects_current_release_and_keeps_boundaries_explicit( assert "https://openhcs.readthedocs.io/en/latest/api/" in html assert ">Install local MCP" in html assert "https://github.com/OpenHCSDev/OpenHCS/releases" in html - assert 'src="assets/ui.png"' in html + assert 'id="gallery"' in html + assert 'src="assets/gallery/' in html assert "CellProfiler" in html and package_version in html assert "GPU libraries + custom functions" in html assert "Compute with" in html @@ -189,11 +254,11 @@ def test_landing_page_uses_factual_copy_and_readable_proportions(): assert html.count('src="assets/logos/openhcs-horizontal.svg"') == 1 assert html.count('src="assets/logos/openhcs-stacked.svg"') == 1 assert 'href="assets/logos/openhcs-favicon.svg"' in html - assert 'OpenHCS' not in html + assert "OpenHCS" not in html assert '' not in html assert 'class="hero-grid"' in html assert 'class="release-summary"' in html - assert "Plate, pipeline, and result management." in html + assert "Microscopy workflows in view." in html assert "Agent access to pipeline and runtime state." in html for removed_slogan in ( "without the black box", @@ -216,6 +281,87 @@ def test_landing_page_uses_factual_copy_and_readable_proportions(): ) in styles +def test_gallery_uses_semantic_accessible_media_and_stable_paths(): + html = (REPO_ROOT / "website/index.html").read_text(encoding="utf-8") + collector = _GalleryMarkupCollector() + collector.feed(html) + + assert 'href="#gallery"' in html + assert 'aria-labelledby="gallery-title"' in html + assert "A real imported Comet Assay run moves through compilation" in html + assert "time-lapse" not in html + assert "five-phase compilation" not in html + assert "selecting a native ROI updates its linked feature row" in html + assert "BSD-3-Clause" in html + assert ( + "https://github.com/CellProfiler/examples/tree/" + "4972b59e670a4ae96c3d453803c92eeff378d054" in html + ) + assert collector.figures == 8 + assert collector.figcaptions == collector.figures + assert len(collector.images) == 8 + for image in collector.images: + assert image["src"].startswith("assets/gallery/") + assert image["src"].endswith(".webp") + assert image.get("alt", "").strip() + assert image.get("loading") == "lazy" + assert image.get("decoding") == "async" + assert image.get("width", "").isdigit() + assert image.get("height", "").isdigit() + + motion_stems = ("lazy-inheritance", "execution-progress", "result-review") + assert len(collector.videos) == len(motion_stems) + for video in collector.videos: + for boolean_attribute in ("controls", "muted", "loop", "playsinline"): + assert boolean_attribute in video + assert "autoplay" not in video + assert video["preload"] == "metadata" + assert video["poster"].startswith("assets/gallery/") + assert video["poster"].endswith("-poster.webp") + assert video.get("aria-describedby", "").strip() + assert { + Path(video["poster"]).stem.removesuffix("-poster") for video in collector.videos + } == set(motion_stems) + assert {Path(source["src"]).stem for source in collector.sources} == set( + motion_stems + ) + assert [source["type"] for source in collector.sources] == [ + media_type for _ in motion_stems for media_type in ("video/webm", "video/mp4") + ] + + full_resolution_targets = { + link["href"] + for link in collector.links + if link.get("class") and "gallery-media-link" in link["class"].split() + } + assert full_resolution_targets == {image["src"] for image in collector.images} + assert 'href="assets/gallery/lazy-inheritance.gif"' in html + for link in collector.links: + if link.get("class") and "gallery-media-link" in link["class"].split(): + assert link.get("aria-label", "").strip() + + +def test_gallery_layout_is_responsive_and_has_reduced_motion_fallback(): + styles = (REPO_ROOT / "website/styles.css").read_text(encoding="utf-8") + + assert "grid-template-columns: repeat(12, minmax(0, 1fr));" in styles + assert "align-items: start;" in styles + gallery_card_rule = styles.split(".gallery-card {", 1)[1].split("}", 1)[0] + assert "margin: 0;" in gallery_card_rule + assert ".gallery-card-wide { grid-column: span 7; }" in styles + assert ".gallery-card-compact { grid-column: span 5; }" in styles + assert ".gallery-card-viewer { grid-column: span 6; }" in styles + assert "@media (max-width: 900px)" in styles + assert ( + ".gallery-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }" in styles + ) + assert "@media (max-width: 600px)" in styles + assert ".gallery-grid { grid-template-columns: 1fr;" in styles + reduced_motion = styles.split("@media (prefers-reduced-motion: reduce)", 1)[1] + assert ".gallery-motion video { display: none; }" in reduced_motion + assert ".gallery-motion-fallback { display: block;" in reduced_motion + + def test_public_policy_pages_are_staged_with_truthful_hosted_boundaries( tmp_path: Path, ): @@ -288,12 +434,7 @@ def test_website_source_and_workflow_follow_package_metadata_authorities(): assert "0.5.21" not in source_html assert "0.5.22" not in source_html assert workflow.count(' - "openhcs/__init__.py"') == 2 - assert ( - workflow.count( - ' - "openhcs/resources/assets/openhcs-*.svg"' - ) - == 2 - ) + assert workflow.count(' - "openhcs/resources/assets/openhcs-*.svg"') == 2 for page_name in ("privacy.html", "support.html", "terms.html"): page_source = (REPO_ROOT / "website" / page_name).read_text(encoding="utf-8") assert CONTACT_EMAIL_TOKEN in page_source @@ -317,13 +458,28 @@ def test_validation_checks_fragments_across_public_pages(tmp_path: Path): validate_site(site_dir) +def test_validation_checks_video_poster_references(tmp_path: Path): + site_dir = tmp_path / "site" + build_site(REPO_ROOT, site_dir) + index_path = site_dir / "index.html" + index_path.write_text( + index_path.read_text(encoding="utf-8").replace( + 'poster="assets/gallery/lazy-inheritance-poster.webp"', + 'poster="assets/gallery/missing-poster.webp"', + 1, + ), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match=r"poster=.*missing-poster"): + validate_site(site_dir) + + def test_readme_does_not_link_unpublished_coverage_site(): readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") assert "trissim.github.io/openhcs/coverage" not in readme - assert ( - 'src="openhcs/resources/assets/openhcs-lockup-stacked.svg"' in readme - ) + assert 'src="openhcs/resources/assets/openhcs-lockup-stacked.svg"' in readme def test_build_site_refuses_to_replace_source_or_repository_root(): diff --git a/website/assets/gallery/execution-progress-poster.webp b/website/assets/gallery/execution-progress-poster.webp new file mode 100644 index 0000000000000000000000000000000000000000..cc5e7e83d772aa4bd5bfbc646aa6cc77c5fc1228 GIT binary patch literal 158336 zcma&Nb9iOX(l#91wk9?v&cw;Ywr$&(U}D?$gcIAgZQI`ao8LL-J$l~fy}o^|KX$LR zc2`$*b?xe|`&N<^7x%CN15pi!2V4Mxo&ewYbzszw&KC(*{J#Ks51B}@ z#ntB6k7-i2@Q?G_uD{B56W=zinM$9F9`k1dx<6xYyAOOq4!3oI;O{lB#Cp|i7xF8) z7C@RC#1AH@&wBt>!ZervIZ*nQ`)>M!^WFafxC4OnrvZ!u)!%=;Gm z&%}?^%jcQv7vO96p?4L)6Zjp7{Q|tp1p)+t41h`C@W=GqHt^37D+T8LCxC?SyLSTb z-JkNSz(-;dfz|F^;L+0}umM=Jf&Xd$?ho+W5f}qj0=GWffxx>mK1fF$Y zcRKDYdnfTTp_z~cL|I`cs1;+k)B;bIo?Ee*T4`BLezHIJBPWYqj z_6OA)^27F~fU9r$hx0LU3(y0|2Izi|ZK8Vi-}@-^y1vuD1$Oy2Ke4^KuOrU@9e`he zxgY8i@*CY_zyP2!u>F0z&_4^%4DkPmebZl+kJBFpCIgcI{Q%-8>jRM!{|#UkF#et5 z0J+e=6le&b`~2R*RGBLXbp1U300N2c{UW-@`~!gOz{C&iKcmza1Gs*ge@s6k-xI$8 zd;NjH`#10p?>Fq%+-rd&fd^pVpO=;|{qKP{0u%n3!2MIUC;10~JK&g~f^VQdEg&5* z{yzLF5AZ+w^IPDD2EZKve9e8#{S?^#(+1wQKOo=vzX(kFSAMqrb>o5fwe*SUt$Wvh z&i@VQ``4|lYasBQ_yZv6cmAg}elz_Z1Ox)M0eemWh;g5A!;?`!yWbmEdKe}ZnI4o7 z8zpx)oBcH?>*$;nS&a$3Qnm~M-DOpO$L*L-t4vnsbLe1~Q!1k}6_f97oqbmEc+%+j zwug=OF^$t9tJCuqKGy5sUwpEmrdl}yZcDfLVLPOOF4|~gLy-L}! z-k6(Zing3Pr!P8z2gyoUyKi^^gE^IDrJAeV0>Yf)K_g{ClZceK3DTPQ|IdN`Ln9@k z^oi$TG)Q?Ki>B{eTV^F>WImgKgZuS?>v*hw335`Xhe*016L+CB(P)jB2tY~q8=2cw z+I1`SkSqqZDxU0C6Y|JR__o75DmCV#Ry_-Z`FHb0;Ze19V>0vE3Iha(e<~K&NU)=q zv9OqyQb@#@wdahH!CR_lbHxsv73NnpH{hW`38oJH26n#pcXP0`dXxRjN}zmv&21%4 zhg@y~Geg@AAEw-r_G;gT3R8N|BxaKkGBrxWgi4|Fxq(&LB-)5CfzAYMrA=GT(h;6X zp!kZHR$rD0z}L(_HM2aR@|od-5Oyn3kGnxjS3>PL?2pZ-OiMph?Je4=3qZU|oZQ=7 zJ!sTU_{WM_V_620J&W>s~|lssdYkRMct>^u@+ zH%kx&V#HyUOn~1OKb@PVrO%|Uo{iHYvwyX)wYVxnElOmzq)cN(tJq?FaX$!|K_3VX zo)77+pk59y>rf@cT%VLb$O7U71?lt-b?ESqInt;_GbwQ1pQ)AjMJ*S$K}me!!{yQoMBnzHje*FFjEdTMuIT7N z1t0t4{cD2H-#qkr41u1HZ;k$CmeLSWl*QpsUpsH{xoaQ(zGCuvd&)JwWk3HeMzS{OQx zI>Z=3FDFg1@P;}xGkz>&k+GiU;|?b@X1TRJrsy@-0pmR_3fQpg=Gu1NXz^LSW_6`! z^NuFn6bEXOgLAt5nJ`TbkP{U_FY2ds=&MA_aafJaYGe!SXRj<;9YH@6YXuj7BM=|4 z>^yI7u#%a~$S#M}x6%TU-4^7N3CH_5jvW|H#N8o5F{XpvO70Yv)vJwG zp%jM0m6s0hR83+bgCPG~+IeHx4bffAZoT$e)fQ%-HL+@NBWre;8Ij+^RgTf|^_{%2 z9QT23o8cqM&@6IkTcd{Z6*qwe0&}#1s00)|XbVub8Hf}1%V%6(BuwR;4fSAs+F58r3o?Ol6`yQR67*H0`n@6p4|UP-x8&NrRiJLUnA65(>@xTr zHKD@}Dka49?aBQ&1=<72WCTduO^%jdERb+7^?evlC@$h-w6N`hvV@uuJIX}jy5GPx zxg(L4{yRqhEwF!2yFhTu@)wV}>rP)`!7AURc|Rpl5!AMYxip`xl;Spr)Bw}iSsUTh zSk#9^LQ1r+fpbC5*rAm(CI$8=a6efqKt0m#`bgahq4J}_Rj28_!*W>VoCb^No!7!U z#a;k{=OYS)BkBdFBu%TVFu%pk(9TZZ+3!sLa%aB&Epnc5anI5VI32M@EvK^V2|$AJ zkRFU7l=jYpv`H4ePh{zj5}GD*`!U+DxKCDLyPePB5=@w37$=JxiK_vZv=S%mN6V_rc zKYdN2D8orwoG(DUC^#?a}<<|)oVST6P*vQUy5S9*M|MPD)g;h*8*R2?v z%DA)-woAu;+GVeQ0Hb6>igq2d!P`ZFvb-7T`u2UZ8aZE=!;W}*Ktw7Q4gGmqm(y&} z>(NT7kl$t1$+$l)mDOK?pvFd@bOFSwj-#DS`{p>JSk3qb_sO_h$18RFLN2eN1^-JO zI(Sy>Q||gC8IR_aqS5y3nwvRyy6OVOX~|2TyYVk)t8OVCY4SIm;;dduF8C>yQUpcH z%XMI@Qmn5N@zFsvxA!$-q|YIvPKlh}d%w{(w{(h@-J;Tpo<{Bd&B8anx%=kxYCRYx za9ZD~pHT`iu=A*{F3J4yyi7O?8(7QO%LGcblb;a;QlSH&6RNf#;aJ;^`}}kfx#DrZ zyXlxO<8y|HkJ4W}Hc_U>!wC8c2|Tv?pL+apa#J~^)}Yhme5fU!%Dz9TN_!b zC`s@!U@7#boP|QEs9Zai6SHV&+=nK26Dw@x=ZDaWw_;@s(CQ_8w*xLe$&TQ^RCXx_*4KP%G;B7=?JJsO6!J7WFV zs8`~kw`%TMjjs_c{u0|ssoj1&MKj9N53kWbIP37WtFzpO6=f*(Gve`Vp;+AKwEkc? z9QOUVIV$oW-}v&Ykw72)WES{rb`Zbywect z*)-&PdV*mu7IPDR#9D3xHEXoka_&?Z*oz$hITO0Ej<2=Otg~mt ziMH|Uf{3NCStm}7wGV|%_r#+wXK{5aI-Fs_qvGzh$byl5{h?OcCH(u4t0AirSdNGr zm&)V156z}vOn;N308G?w)J<)X0Btwc3G}PO<^(rR?!P6v*J#7Z;BLGawac1o5u&dO zO=bO({Pu4&ACyi&w-P-q5OGarlRsO7$AOx5N=OH@{YVkpIo4mvvGUKNZdP>(7`pBJVmM(mLZec>6z1k}yj2ksB z0c)LO*!Kt=N;KT_)#W`a43M%oii5VDPaB>mDWx5o&cArx(j)gR;T=?1yEihrfR|cW z19fWOdOUS`g}TY0Jnot6QTbO34{RH*T-}4jO@fyoYmQk-L_nc#V>Hd{bC&C8;x1-=}%cM-hbWJjhC;XHyzBWMcO*tc}1ZJExm#*u;dnV=ZAu@5Tnn=BVH!Aaa?$pSaw8+v3LDj zpRZXYHyfugW|{55&P32P{+y8|$>eLmvxec&Mr{JPUWDQf8Qc~1EOiJxo~Y`6(l-!o zZsr#RoJjj}Ze9<`OQ4Yd{dd`Up>hk=@CGPkRKs7kDy~g@)ExJ(#%WWCBqU4p@jPUr zHh3-Jec{Hl(#ul~L^})|qdsH`7O@|JTgwLGM;w)tZ0}Mj#d%@hmzr3S$4{wFp__WG zrm7hWf7;@h<=wi~NsW|7OrHQylsNpsef!e3b8#D32^EtZ<_I}JdP*bA+-oprwU%k;ok<%(p4Rm`D!aZ;*pfy$Y?kEBQ#hyFSv3#IIN~_Jk{{xNC-{T z?uXUJ%WNx_X{RnIUbvZqQye@`OQ+NPy?V@11G)R2o7WCvlriS$ie#DfYnUEqNdQOd zwHIn-i{c1nEZ!L}Z5P7cUr-OS<~R*pG`^Uyp9ZFr@jyZ+cYgP z8zv*4{FZCjym;Tm_He^vS8bL<{W_7y=_jMHxtjfW^a{ErB}A%A$cYame>v&LwXbi3 zPx@Skqgd&UB;E&`BG*h?OF~nbu2`@fxM>Me{$Ez5d2S>F?X=lD9f4NEHXQ~dMs=iir?u^LN8s6;HVy@?5{`D)A+x4*48Lzh+HJA z`#sD3HNY?;C{W_*?9-}7o;9g0=->MAws6pO1oNQ%okXs+RY~RB=4u>!M6%*ym#fh{ z+aw#b$G7YXn?9*u8N-edk@)+RG|7B4qHF|+yC}$a<`YUfjPFx+ zuXd_!=Aed@_M=rluf1c$Qz-zkfb!c-UY^y0(TWhHmOIi5C?4M6dLPZyq#B3`CJR9fYCXhaLCD zU(ue*fL0M484w@Um|*v?;Gl{2;#ZDvfeY_9Sq_lojpJYPhYTRp`I8F->uZ4q zkc^S*4HUCmX0>J~vVSQ|Zp^1>{!!t5S3SemRpv`0kNl<%V}V<2>BGxZ2m5>f!_pwaSuBX^%tn##1q z-q>96a1&L)t@Ee;J^`D<)*B~Zbn#cm-hQ(7%eOLYkxSn+?VSbVn~&94tD$x*+-;uc zDTrYiJYp9Z1o?CmsCZxQACBWp1CoC`5c9;mG&D)H4h<>VNrWO5g}=s2vu*8!gNeAaVM+#^&F_tE%CAm zqL%Iz2Ft`3g|G%TwVcOqYQ)`W3}bWBh&P- z5`eO?2qfdR0WNDPSjNn|V>4IUb4n7CJ8_&iDWWr~)RE-ln-TII={367>eF8+@@;r5xgwx-Nze-o2qmhf z-J{iQA4I%K%Y+YWO~MF-N?#8aD4#74iWp(&!lpmv1B3 zlTLE^l-GA>Zg~&bKvO6Yglb;(zhsR6y>*sQTvw0dhh_0o zOKvYULtv1$z7VBRSY>dL*t*LA;v=0^0m(3qoT8KEsDX}@*nr>);CUL0(^thSh}TNVH&=1yuZjR(taMj zm0ls+xUbwQ#tZ1b`amae*z?;WX&F8qgqxWyOzeIo?)r9XU9!Z_XQ7=?B@;tCk#qO9 z9(ySy2n>hZd`|6SX+xOnUYjr+Kr%qt3lS-nkX9;MUjMEG~BJAgRh%EA&+DehmI;b?FU44H&%?zLpW&_yD^Y-2S0r zrI?sc0rvD$Ui;&ahD`3DIR@2Ax>Q|876S{6lp;ig4P0pucy}bdKs9_Wj8_~UQzMDx zt#bYu?Y;DkFoe&L-+K9t%LYCj?~(Q#rO?~LE_*AK~iCY2;PaI<@!4^oY(T1~b|K zc=VJHntiuQDjl25BoSoy(jK|01pWzqRxaL-hilLb(IiX_1YSKAwqfjaBzirP@kZ9N z&ao+{9Mw_#;CDZHZ!tdy`y{fD6)9LG?8m+i(U3-m?eFW?i2JX+S*Zh;e>ULm?DI*E zC#UvZs!sZ-iBm0ajKygUJ+kmd_j&v2-Uuwo(Zr+H?yND-mi0qu@W9p3>QER z;J#Kwa_L04Prt0msr8x$VjFw~wh1IJ=Izn-)|3?zjIEE6lh} z1-(68l(EV;VNA8-pPfJ4oII~`eLERZqsLQx%5BT2w(1xoFoi}>yjzM{>za9Vvg{Q! zpC{NKGu&b7BF_^d2arAE=Qw-6Wb{b6g2&BqN96h0wY##LmPZrB zL$br1$?D*pRPAH}b)t5 zC0=ayI2|eYQr6c^a1-+ADa2~fNT2AtcG`};d=OP9jMrZa#XFOK;;eD~U%Syi`%f5Z z*5M-1d`Ae}C1fts=%*y+!%Y zRt@A>T#JF+_^=jkWHD1QoQ!E>FpHph;%ceV%R%Lk%6=+Wy%GGh7>msVeu5!g;QNS` zbdW;UO+sTTKd3|F;Qp-&?C!OvfHq_ZujXfZ=C-4yGqIwS;u& z!6^hDRM9z*M!tjb$}{md5%J{q*DZYiBKQ)m+6diTYxWLX=Ihr+E`O)zdV-fj*LK{Q z)b0-dLlIyTxc+2vKu6m{!?{a%XJhcV`<9Q@p@*W>YdrKf8XG8iA(BswJi`eCc2rh6N`_Axc|y3!YZ>Gh2$qrSZL zS1X3Q5=EM4W;ibMx)P8-2?1W#uLx9&9~U0_!fh;RpytsAkrOxFzh>pHlXb`8)v8W{ zjxe#Hc7JHMv^bP%kzf{jF^j|@7;+V_B8Z=%HoQ$6A|&X;-L@oHZcsP$fYI&K3Qg|9 zhu=tO=JBzo!loytw85#q#r( zj`ULccRf(mnqj0T3|a1nUbvC)OOyIWn2q9$#H%R&CJ(^+7GKc(g$D9~C6X}8MA#f& zPQ`{>7Hpu<$JnX&ER*GH4~ZMS4JcN6(&Q&Zq2yVVg3%hytik7(F0OIfpJ?T-+#}yy z&_c48OupQr$*V{sEi4VZfXFKJ#vS1(3L1}ry^{#{zw-JZv@CdAwzIvzTu+%9L6W3u zHIA>pJ~`L+XWiww@!F(`_O&+ILu~Jh7-CZxc9tN~0l^}?`Q%W@ImlV7Z{h`kY8D*s zWLPr4{2tOLtKV2#57SbWeBuqYOU9;Lq@9fD&7O5yux`tczo1s%bbK=&-{je&iS0lQ zDs1JHQGq~sgOy(ykIYnuH3;Lk)hq<_w8ue|05MD1U}kJ@7SeR&?ZOTk5KeJ>+kfPz{w|GqBbLvD3w zh+%G$NJDth+~`7q9^smrFujeJVCWkN^gH{^>{}E&9R8M2Dah0OKw$uxx_Z?XDJH-G zQgn7!)M100OykfsXuXxiqg+BRB4%0OAoFdjgI`?zIsRaTvq}8y6zJwP`jbb)F?G|1 zneIkd=;f?WjQSAAd$VWmhcEV;F6NhTk;?)q{!)TlZqt$8pZYrpgOJ@DN3@aK^6l4n zp~;6$h>w@gKOy)ptxSVU+K>`v*}ciDV6=NY>gZATzq|-~A%TV|kWG4m|FWsI(7S)Iiv;TkSRXc(Vu(AbeR#DLYzz zqZ=A}m(Odz<-l=t!FPdfF#04Hie32a8xF|yWw&ij zCN<}+ML=uA3mD;}p;CNK>rX?nmr9Med{xejsUWnP`y_)2sU315O1ht|MoW31MY4{C zasCSLQ}@hYr;9Y?h@5$C``?Q0ycNwW=no6WdJgWY8Ae*4Ls#<<3_t?hWzw5N zveGkJV@3$5FxnGXWRxofQ_@w`5EDJetC052pQ3j_cI}q~R3KXmI~54nN!p7~I6)q{ z$OWJ6rlkUmzj^Vmzu@u-I3wq&1xY8rNXD)Z$y#PU8t^}FKT!*fsj6Hfp>*=!SRao7ULK;AoqGGf27j2-hK2WvY}d(K1f%CeKchxpUxh#nsDk14h` zTV$_t)?H$^i9sJ&bvy*dsURQ8X=HgAQO;|S%8az^k+5v$LW< zNU*sa9VibDPwvOH@O037vz`!6a)s6vD22d>fDKpD`dslRJ?~ePFDxai929Uc?4-+s z;WrjV&#M9uTu?!7$Rb}7UO*mW=6XKu?-y(zTg!Y4MbdJ&P~*I9za7>{Alst{>)`oD z2r>aen5B3&6DFqNf~RJ=b%d1em8OHn%_g)G?kW%WUN#39KXc|^TrGDdCwgie^J6+# z$V;T;P^@$|9rNkQ>(Rx+Fjkz1!m8QB%Mz!t>~85nqQN0DfbiPtB=$Hh>QF#WY1i@5m$Ns6t!J z=SGuAh45ITK6F*qmPS9@cqJ%{Jf1c1bj@PJ;4JYaLFBg>`Jg6oumn42fFN`{Ta;XZ zzO)9GB|6W?STUn`AaE;LRM})w!LF2w>p{ujElAnswGOmj92HVy(x)dAQ zvLT}IU>rfN&0W7cb+P0z>L;H(J2ia&YPQN>`XJqJO2kyXGtbYG79DW)9*$|jkFI=J z3`2@()Xn`4D`&w>GWiK=`Rt`%|JJq&NZxO$id6vOoz0KWr2cexqzWfgqxE-feAdTG z5v@vWp^m~;$PMyPAfD$(aW0RZOAA}Kn=AXAzGk-dPR@_LH~n6DZx(ub(imDYG0KC( zjUr(eF*J8PQ}ck;`_Y$DxrG`D36HWw5cUFTg5c%)eh=znxWYycFO1*JaGw0LZ6phG z&Oa@?6yDMN%q&)Ajh4}c8iF&nC+5q^#1pEQI$SuSgVcQ&oSC}Bqc$Q$jN$1pm)IMh zQ)wlB6Vqux4VKv+r{mY7Pb$LtWq6$r>=W37ZYte| z8lUY4ZW4G(clmY7xo!ZJ74!+LNM{w}O7iXHp0`>k)q}ja7j4D>j)Km-@YQBXToL<7 zkR>4V6DCvyGyy~N{#l(4VGxCH-dsaxU8Yz24B$gh5P+giKq#c+QZC82q6 zYIv9ANVtZ`P>pTN2SyUJETLnBzs?|Z1pms7|6ZvH%&?>FtYsbTlsWOCig=;4JcHf-t_ZFz= z;_+K3qenu$uTENS$F5=!GMcMTeiMi3L_(tpyRV#eDNlGsxRLHF>r|{t?PFZ&6!NJ? zx|Vb+94H`jP9D#n6KJ+(zV;U2-6pbSaE0}(1l=s-6bT`U3n{$`QHHFMyAkke2HWCE z$fB(!C*sSU;y`rYhr8x4^uD)m>%8nNomN_f*OdSPpbr#j!)7f@i2ULst9-y51}S$h z?T$2AOqykg(4Rt2Xd0RU@@O|o_!o# z$0pLDw6zxu(PS@War>Vke=D|EEPNaj-(qy7RiU-T@A>ODex(RQ61zfH40cU&dPd|M zRq+x(X14@vrlBro`^YK!9cHE0w9_=p{Jdl2JaRFgph%PS!ZmilxJel{a%0{HYKRbv zhY{Zt?L|EF9fKu+gy4rpBDB_^mo{qX%n>8;R<|H zShrdIF`4lg_~U-H!BtfiSDgHCe!MGl;*qYQ5r(x1Rq+=vNxl{yxET{a*Sy^5BoRrXr75dk0 z4%yXM_NmBVu7-+c^^&G#9M*^BE(M;*v0}P0fFiWfxev}AQ=bcS=55wAxBxE)u>J|- zpwm}N@moL~m!dd+pYCDIg00k>D@^B|4ia0y^~bXQ`2D>E477tX7h(Fu^N*4lSt>v$(a5lmjZp08K0Ybq5_rioM_BexdL6rZ~V96**4OBAPgE1;6YqHkX^8 zlzVIOrP#p~cCm4$3hW}*3&t1-r8W^%m~e)CSTpsipDF6Br7j4y3?7tbY_prdQi)3v zlxYkCdHTt(2K^zZn;*2(ogH-4qE?J>)gcG5+;g5`(sgb9&Qf~Z?Q8er%mu#+Mq)fc z$!n72n*bC~;l+}=>OTY8Mp}l881X>DeS=9RZbf!xnb!O5u^Pvp$zuv=e_t}@rl-?@-xlueBduIAC{uI)O~?7k*Sn-O`;FfGSDd=j+m&NB@{wmZ-g}o|v6M zGL-jyvl7JC{v67q&smmQE?_ zjpaao_z`8uNFO{$8B6@V#oSqO%drhIB`66D1!+? zMcr3kuKzC(GM3E^VV|X#OJT9j9ej~1NH-2{zg1xKV#j$}6Ra>SBKNpy;`k7d%0ZDp zgip6Ek$PcHUW3g`mNhD?iP=vkgWxIhWO9l8CZpkN1RZJa?n4L(j#)?#Jto>5zOPu6 z;7-b8KqG#$;l|V2vGfiLDQW`}GczRLYVP_i6zZysVHIGrKDtMg9x+_sJDjQ|7|#w^ zz5;%tp%S(0j5|nqi@lWTDpaEaP^xPuH*nvsC(ROrfI<3_VkT@=}OwHQ_Mg)^}F_d9+{4#rPa zLcGH1zOV<*=O@_oSGcqkvjack`XvIDzah4B8(VhEL;8S{khh|StF0Rn1egZ*=qBdMT3ub{Wx6nJx=8G01}N&E3Ibp}yHF;~ zMj?T>_JUlMX$N9Lo3Rf?$(tuC5u4=_1MrLwnKCmGrPNrcFMRX*(R7;j^=_EUQ{b&% zX;M0tf(E|#^tFzlW&j&`$Qri?Yup0|)nI?w)DQ-olGzP$OqmB$ALx9^DRw!-P=#!a zNMDg!e+o%Y#a$;^e!_t-si$yhaL~Q&C6&~2yXag%Hq>!hhli~#ndk@0@qE^=VV5Dg zVY5wFHg^j^hw|o*sxo3oAg1tg#0 z{k?_QA68SUBna8suh*&kE~woTt1D4qiZQVgzrLpHW~JToFMvK|a)+J$V|6TDf&51U zTp{?21_h(0ujl5usg5GA2<7+IC{*U$mf(I*oo-||g|_gYO9}5dfwBV~H&B`=i5aUV z3h-7+4rXOy7iVa^_*NmMv`4E?EZ5y1GqT#=e#Oh=jhUntW+1Ygl#}XI;x42360n5a zm2U_9yO;rGzqMwPxNkA55HIsFI6Kd7#B3l^G^U=yh?MC^ip{6c4U}&NA@K`96|18=6K|lySJRG)h;54Q9S$2XBGo{-_9m4q+;xd=_ zU&I1%m=mlJ^O5}HisCf)VESh65$8uQ4wv8iy@uT=w2;KgWF}?;73-kbiF}5zpntIp z{)3vpbpv^7YOMP7cer9IO=9XFb2k4A-TBJUaSJ%l28f!rDSdNJ9ZFJG0UeolhfCRK z5mXc${ljip?*wRfm<>&4O@yN`Hmw;!tR{s#*Oqbr5zp8~{dZ!+-$XWd5Hc|SN9TW4 zkJA4Rnmj^jE}&`#o~ejAv$D!mtO71azeNi4?}eiCB^0yPKOHn!;S}x!zS9APk#U^* zT08?dPm&vCI9sqUtY9RmlN>oAuedSLh&tk-xf#o=h_BZ{hR~(k0&cVRgh~o6;{HLx+V`=bvR5qyO9R z{vFeqDrQJrm^P&GKk=%6_#?g8H=qA$!GHDQSN-sJ#O%M+{y+U1#~6@7Y5zBR^l#H~ zOR)Rj?2~`eR3tJq{%x-QCFu_a)r>|J>u=?`e;D}xNcw|Hl^6YASS`sgRSEi>t5f4 zG4#&&p9~$5l^g1uRvaz+vRmpX;?g{6M>qqUa;cEG`n8_xUkX^@ zVovt{tTy}9R2R{KZc7xkKY~rHO@@2gUSiv0JZtTTDXZSLSDeM`4AQ<9_EJf;=Vh`N z@Qjg~GVm^!$I4gP;6XM;5Eh29G{cqx5;apCMGeGF9FS2TKM)M-8dV&(fepm!?3K(`HuZD|?b;U6Rr}ZZt ztJYrmimi=*vxv@a$-kSVg4$m+36chJW(d#`kkQhB**6W)Vdh3F@8jwJBR&Wo&E2CYdv$X#7Rn?7E#Mw=zN`m4r2_( znDK85w3U6FBDI0C@!C~6AM+YA^M$)e#=W+%Z?}R0m|uSg$w(=jNe{}CCWEm9Oi}UK z9fXO-z_sg975EukYuu|e?_3yEU=-y@+~uQ`5c|^x?J4l}`HH zA~$V2?&wp4zJsc3O+L=Jrggu5ifMA}@eyV-ledM7g_XcmvtO!dcErkzEf42P~% z9gn6_(DaZ%2N6e4#6Dk$8CvamyI$W?ed9FL4oWmEcn2P`#>#jRWzfXdEBiuyW6vSv-eV z*jh$+l{PPk5m+lC4a{+8uRVCINQl}Z9S3BRGyntoX!NGP^%SvjAgF0*i9mKfvu(Mm zdCHz?Gq@X!s$-xVeI-Aqa(+vWGW(BQyJ*9uBG$c^ReIdeD3%arDVcHkblS}!*{Xa& zQtdSW`TGZ)ATd1K-DpDVvch-rcbK(-G(ZtO#p|QCuD%WWUKEZ=0t!UKo!C_I=dD31 zrZ*81@KCPWd@ex#&R0LNjPl(xl&j>n!@e|LCWMuh9M1R>L?S`^Lxy<$1!enIlw%_d zii5eFUBgLEaA+bTzq(L zzQ6M57~`|&%Ja`|Y^TO>SgBmd&qUizk}DK2#pviN0*(%!j#e;)S8`{VV)Cz)i$$U( zwHSJi=148~McvAD6r_rg6N@^YucD@&$+@61Q318h+fmO4@OkuJ3kEpFHU?zlitTdG zrHsLzP%L3ABRI#idc7q7Fe7NqC2qslT4*V~y26#8GJR3GmC()1D>>f45Afk7VpB2n zJf%z!sd$J-2!Ae7(@)chv&P=2i&7$Mf7ars*BOPbRJ1DH)tCPG4p!&uCr& zTc-k`2Z9;@jM=w|2`*IxZGJWDr(^m#v%CDSw!z_QJCDvrO;V#tWFo2ANWZw zb~OWL(;?Sx*XE{P%(^2^)J(zySQC!k*%ZW_jRZu(higD)H^vmJ1d&{Aj&@w;{&%8= z^KPrL!(+bV_d_1Gjgg-YE=+c|7owbW;P;Ep$qz&fC$_g5?)|2>?JJz$h)VUA-o<zK~1wf8`vU-TR^{;>icc$Yl1Ej0XgWS zPF81Ou&>@HYnBD$&B-HOus>lrj;~n#71g2`16!6jNN?mB_%1)HIL}*pk&6Dy&Cz-$ zzJSRz?vbFdmUR!$T8x(ONSexschDv4s!UvWwCOw-=Yb=mV82xS8=pRl30y3nJN^6Ai zP{I?Mp2Vd+{%BJN66@Q)F$KaxV!7OIPVtpP2}O(D#CeEkeq3)3pZ{(lCZS&YeVq{` zBHM_m{bQQiIb0QSmCaRASxc(a%%y&@VE(c%-y8JVYTarX`HcE&0cZjYjf_{gKYBcl zjKX}kfM>|)=|C^9S)e7Btj+;v|HV|LTUYqlnysHt;c0cegZjQHQHwlXf$4L7|7F58 z8`RTbD;~szxkw~pse>ZVigDX3Y4r9Ll^STxXGU2<9L&?byVJ2R5})t!PGk01w91&) z=yxz_TpvdR3LZ<4;*}uLo<<>$-l3ca79-be%Ln`g2BV7j_Z;KMz3K0`jH7dHS>Q-g zqhR;^Z3uc>r*=Zvy6S4!mOEqCcD?K*CrIA0b6+}=EYxTQgn*JxQU)cQa1ku!Tw=_utlI8eyb;|BFpw${bRGUE|Z`1 zOlXL?VFu2s9ZsLmF=+Wn?KF0~ToYU2aTX%)Ixj*|FO79V4acroRSBZ(r7%CqFDNf+ zyJd>yv&CG)SC9_XPCU*Fx%n;6ZT+ene)eh_tagMFU_+zxEL^U3Al>V?R14MeVY9p! zoIwgOU=r89ha&_fPbb2G+*SPTP2BJH34N$1ZLK$# z!0hV;u4m~70e_4K^?)#9`!vUO2^;~MZJ)jWn9M*JvR1j=ePZM9f27N zufP;Ec?Ux^P3vBJ#uenDyZH6g&lB-FU7^Q(+1B=S@aefSNt$><1ff^fo*8$QlSP29 zgiExXsCIZ<_49eaEkrMELWB*E<7a4f&}gqgt2VY*SJa|a*_cc`R0TT=jN^Pa(}#cn ziBTw|M#}sCSdLdfE8C-~EOR}v{D-}wMeO)xqpRq|B6IT97jD;X=3d&L>94h!cwX`0 zMq{N1P2yiC;oZ@la4>``GS8fTBukmMT61xjjI>1cMxoShu}!h8T{sper#<<{P+q!p zqdQP_v^h_^#EvU-l0ANum0zmNMdom2;c&F;o|>t0R{-`Y6D1(s-~-@rQ+Pf5=1L-; z<5v-HVl?^M*cM~>f^J62ScvQB;?SO6#3gUlx>^WtUtXoQU7TMq{BJtxE&Iu&jE7qY z+~#>_6CUF36S8ME&s05EQmo}vArtY1j`G>3=-CmIYcR2)eEpW)q9M(qu`ZCmfaD5@%cIfJZi+ z`TqbsK*PUhLdqhQKENGJ;PY$IOSrS=UX$i^VjK3SB(VI}2;cCx0i6T7<7b8&*WYJW zDzOBYq8li_3{PwT#G{4w0N*J;&Rt_O@U<+;$4EQ`$5du3#j;O7SZ^CsI_)u>!-%8+ z-oW-C`(+=^!Y5Mflu(@Ko{HOv-=Gsc>!689(4DWm%}~~y`EP5`AF$BNm$yazWG<8I z>Ywz^bhj9ev$REd+6__+%ZLI;wTw95%oer41kk5V-hLU2)B_g**z7-M5!uS3txM8* zTNRdR<-&NGwUPBbzQhQm{aU=pk|G%oHrlhjm=g0YYECk|==)s9XnemPHOVKrt-45@ z|2lphmAFGmR( zj-2OcSX~_nJmxYG=&aisZ`3M6=gXb9KMa<0R!Sd?V8|01#Adcc!PCCq@PK&S`0A$DtJ6LW-z@wr!OLvF06{yFqS?#E% z&bqiI6*i?haMom=+xm3St`uFQkKNjix11R38AV+9o(+YGJMmGd^dB~k0}i#MfGgB7 zcHpjFtpiXTpNWa3NEfhsbL16wyffeq#oRY?^~VX$y~`V?_~L%Mm3Zpg?GBs88E=pFALCCm``;!Pjy5LKve}nSHw1+s24Y9bf$6e zv(T1j3a~xQYEN)?jZX?`%b3s@Uw?!JyASUjx+$~1k3*!hr$`&Q!;zP}<27QyjMw!W zc0vR;OiDFHoDYjcvkr+L*W2_{&(~@E%-2Xo=73{wB))B*m94Hic@sYiO%yvTN|J)zQlId<1#u5Q+p#w<# zmJ2wG#rH2(MbOG(eHdQ*bfBhfnTF-G#%3ejBLo>4)VGz)Mwl(;Xo{(NyAZE{WK5wo zd!5^{dld|uIAKeF<>t*!izRE z%*U}|e;jmYUn;pb97^U@`63sWD?Rhrrvr<&<$)bdih(T)? zfZ6%5x8Kk=Bf<`|NkYc@5m8{sQ!9u+jpC1Q!-;P5C4u-M-eC)nfC1W&-1Fgxp_#`} z=ifNL0g7E48Lw{8s`t7ET&+lm(KV*JJSp)f!1V!>MRhCqA{M&`B$7(|cDbSrsWLqsuj*1 zG)XrOjM*%pLFOU?CgpL~{BZ=M%jLoqU2-w)gCA!vJXVMOI%SE18sy8A1;T&VfG;7{ z?PciC1?5jQGIE!IwJ!rCHh(xqMo0lK4F|^%+PXPvT?)oH1&gZn`BcL##D+2fD+kD> z)9V39XwJ04FXs4X-wh}T`I3k;Ijody7#tkxB!fa!DY~JO; zq*ZMu<52SYuM~*%sXv2|0x*S78llWzK=!-|504K^B>rD1<%byj`FfrFQtJx=P~_UQ zj^SSmp`SgDGOCVDNR2mlIt^j#qhYhJKm=!#WnT{N`$=Ma2dtb7x0x;Y4Or-1iZ%K$ zXZN2DQ?yg^%C%tMm*{D_;M!X^xi|l*6`vA%`YSuOfn{<3Zcp5WwJK-N(|zg_))2hB z=rPsFlP1^_BA?mGa|nD1X6Ed>fIgE|;8^>Pan4WBgYS?Ehf;So9loZiDLb#1_ozvG zjxuW=40uIqeR3%YW%?ElQ)Vj=szQhM(WjFdYFe^eud-Pr@)mo6UJ8sPdjtpR|AvYE z5a|`vBu6s>^e5Lxg_0lqf|hNY0wI`UX;m4hc)CLb5d_#0OJOU&=x&;&IaZpY?Sq@E zr<3;x$eDhWTYmqnf(Ll2mYp>LxGEs`_f8_f-S8uaIhr{!FCuTY&DMS3`Awjewx?iF9|#@6r}M)`~42WLCOAFZgxV~lS2 zcQcjF9%gD#E_CTd8g}{vPA)Ek!Hafp6k8s2DYr1R0u}IltVmR1>9ocMw+ZsEhj)FT z*jGP*YG3zbYj_PKe8${hY}As)FK|OO5i5Ef-S)iX;88c}6z!HU^X5VS&-3T>iG2t; zPm|At%n7@kqz_Gl)tHA{NA5^aZVBaGaH&BFgcrk6Y)@8@1j7{i{}zkgM2u|=_fP5z z%YLBJ^Wfjc5et$}@Ajp8nFSJ5{Z!E5!ZIb-D}Ixw3JUl20rh<{ywDXyhAaW8tKbHO zl1?Jx(A;hu_u0-7ZCGZ&SSY==K0q?TWP-g>Z5bhwr&^&m6v2$hci*3o0R~C^iGCAq zN*w~leoE+(u}*1p4b_Y7)O@LnY#2+)m$)G|{IGXb%M&r`$#}dd5~p7Y-}z)lVsQBz z_s-}FxnsN1Sxas8RlS4-LJ@-^f(TO^H)N#XlrY?Dw){%8 za_34?3F*XW0NCfMCjo7O&x>~2&kiMR2ke-L?M-1VMOJy<&OmWhO*-?KqtTe9emPXl z+b{(lDxwvWE>NiQpGxfLAzR`v>;yiR+P(7OOHXtZ({vL%MJgS1TOKGeFhvny!TBYZ zAr*dN>2(HAp3Gl1!oUB<;jc~wj@sTV3>~E|5W&sGizI1Mlcw@AzZvcGLA#G?)#H^i zs>_q&(esH#mCd&&Kg8@;{o?|MX{P3CBZ(X^%0?43QN+`mMG9c2(BnB_RqXaepjNLu67wMEkz~bQBLZ8o z7}MQv7;$t-Wh0FEdVOz>MtypH&gCD64BZ$|Nel%40t$>tI3dvOPq>I2mB2-K3=ZTu zoobMzvb*t;=nO?Ch)z#godF8t9&RpZ_P=;UxBFe{I9!1)EUyANa`WE(?ZsrWc>FPh zD@-gIZDOj%2{gZE%uNOND;RO8gYym#7HiY|jgG~9Uv-#{rBk`sww{Wb*=So?EQypP z4>+K?E3q%Nq8LAMz!Pt$E#(`xhymq|(9<5rtYTUxs72V>#jhWR?V|O>>?ZTX3FKc7 znQ@?kY#Wvq?izq?yHS;e+_O5ia61CzZ}n@lKzQgqOsD!5{zT50&36Inj4bGRc8gH^ z!CKRmU|y2BfnVD7iTx;ujm`E3-H*^{6v^h!`%H?WM`dAu9t^2}>aPYW#4Vs~^1?RQI~?e9YGHZrJ?OKD(Y6G1qNGl{Z%%U89g|N!qq%<`6vQ03T@z8cV&= zZtjwmSHL32voQ7`sYjxBTC!wnYjAzlAZr(ga(-kHQaVVH-;5)m$-0yDX=Ahuc77MYhz;9HR<{Sx8w-wm3qvPY(c|Kk z3A3nSJ%_5+eoT>+qP-f7j7W%=eKrby_HScK?Y?oz5Rg9$q0t7CyoD8oOc-`88c{%~ zvG@Qa)F>NQ51iv_Gx&34s%d+A9NGK4AAz3)+{_gO!Hqzpyn4s=8qh*m&bv3A1Gz~U zv*?BGTY+e+EMpZU=cd%;6F_8!>doVa{>NIT^_iT#I)V0vhc}j!kLa^g6!byT7B>|8 zP^y-{Z22FkYY3$E)K#t5L)pHM1pAz?cM)tQ$Sva<{uP}Bz4yDT`fj} z^CCM?aIZ&HHk+(@y|St;a&jH86<>4!ws>XEk~ z7%-7I7hOB5xPDw+Gf3D=Nx4Nqvd)K2&M8k?Jfi1bqPE#+KCd2RK8o8Dt2%XjtM0A; zJe!RE<656rtGCqbSWpP5y8TAP&p{s~N7BdKOmA#aJj!9!YgKgEmb>Ef5gP65hF6+!(({@#)tAd7`}=1V+ZGB&gD zM_7oSa~)pAzU(ejM#3M&2(ywaD(U-i4V;o(;}@X`$w15&ySe8;jH8c} zUHkQTNwAYFKv{ZI{IGEGgI6B@x)sX*0Id550Ic@Y*XKHz;Z2jp4i45_FJYPmoQzO7 zF5PQbMI);7XDf8#qhHUl-N;^klooK$48QZH96KT~EG;22skt~&Mm(Fu0G*uLMd!kK zxFq)oV{tKP=YjIF>~llW9AijQ;{;*bNC~V1Zq`iA0y267jxzXb?i8sawNfPKW-bje z9pT}x!C=>8)O^{~_atC&0?ODm(B%XdRG09uegoe@Xld`KlPPl`&klK2k=p42@@s2) z6+svO6I9Cj(t3jws(4FN((i1Mg@NJgVubm2Ji?88Jki)6$!gF)ZC;=v34_Yws2A{| z%tYU6q`?7egiE2yLDi$SfCnN5q7ON>0-!SJNJEu0f4CxN(0=Egs^f>L>Sk|bl*3?r zhuO|1Bjkie4c8owud&>2B+b1Q;L{a7K+S9%JKqF7lWlp=U;k}HH#q;!Kd4_KHdw8H zen-yS)N)}*32XNC?`SVk`L@-SHdjm1qov?<2UY8EGIhn%UO8I}`09sy>WPa{PYl_O zV8w6roK3+olXArpF;zls#Lt^|nGf^~m#->kiK+fB_VKe2kJlF(Be&)0u2$SlZN9A} zjLil(_V(eEN45FCia1#V<%u^+q5s1cOBJk*{wn~HMp)+?Q3B8yKC?!6wtjOS+3Me{G@6>qCzyp8&XUr5C zbnn@j^TSHOp^PreWTqWIRr5uUVQEZNXwO6}y>Ik8 ztJ|{x2a5}*CI+3ai#%5Bx#`)EUcv?{JW>Lerb>dI{aihlZ9^1avs$!Gtv=d`RFf(S zMcgrcN=U{e9Za+0zH9n5^?Mz?mD=2Z50%(>_`BtX(eNKm9!TTrb^8y?naVd5kR$L| z`COeheW_??=-CdQ|rms^*88{!1{vFx?^``1!Xf?NKe8a-miZe;M4#g-l={SUx_b z>l=l85*bsGUZ4$s3KPu|ffadIvC9l{`I`QK67caF$Mf*5`4n4?tqbWR&{OEM;uwz5 zA&BVkk0p8~+p78&sU9RQ=(w_u@S94BffWqc-Y=O_ng%=!CBn8ig)|{Mn8gJ<1XaB) zX`FB3)nZX)Mh~;eHYo4eSdl*@|CQydaCQC7^7}NC28Ipj3xvm2627|2sw^_%N{~Jh zG><)qS^H#4D4ls@&zr@YevR0o2k8qrlmm$s!%ngZ5oWl=Bs4FvhNGF@EvY#JG;>43 zA9&Z_?gJTkyg7Sca#@ukt%y=%z5&DtgZh;CnW274dg(djaZr0C<-jUnw4==^VUmFL z)cgzkN{Cd#DKMXg1V(zq=~2+adt9h{+!5h!8IdzRJ3$u*#&RV99cm8nf^kmYLV0-Y zS}XrpI5ZQ}L(R<8Fvwqw?H`23^kEj~XeX09aCBo)04j^Js9=G0CT(ZCJfv5`j$=8o z6*S}l9KbJdO#0KIE88zp#2s7k z%|f7lKW~d^ufSF6jGM_jd#wP_^{!FwId589#z6csyf((8tHQ)pqra&PBjug|?5JFf z5@Yz$GOQCn@rIBIoh&+s%OrNCIfvf4EWH(KE?ipb^X3Y!Qb#V)f%@s{i)xS@X*S<^` zdo)jjHQ(p~+M&{ig1V^VG(O)NB*DHV70XaTzJVWyRY@j4!Yh{p?Gshgl<_X1`YR>T zX-=jCZD&}9BCvt6Hl7CD@f7#G0A>8l!lZ}t%6g7qJd!=W2NyDRxSA(&EeMKdhav#vh9=j zj^uT9WF81P#ud9RDYV>+c;_VuZy?%)fmiR(Ta{&}Q;TH5qgBBH#-95I_gsrS=c6qe zxaB$`U)8{$?L#wile}wkFLbbxI~J-GZ7$$S+mU5xQ1s3Hvq@aRUN?d6`k# zQ_LnMi>PdL!Rke%1`(Q`Fkp3eu-;maVC(^$$i&>HZgDcZVvo)-5L-?Gp^0{Q2aENa zyaG#Q&Efb2*GTA*3wbJ|3R|#L7PgNtpWrm6j%hx|1?5Eb%zt23s`Jp$@_5K$Kx1oQ zECLC(O$~ZV9sOoi#*7@)S9l$E4|Hpu)cN+9ITeTx!Z^afF#+BVaNUqmjlW~B=I}D# zZlKO|?^8@LH|yGK7%RrK3}&Ym)c>D23(Z$335uYrP9=A19L-MT>bkx+>I~4QKoig0 zZV7-Z2Y&CBSfPI%_Hx2`V#fnN`0Yckrpq#YE`3mX0f;;*MISN07hvp1@?KJ7Es;VM zhc=r{S_OW(pO3j~7L32MpLzYuD$gdr^l6Zr*IM4;tpqK6rIbCVHg|9`Eq{2v`6+8a zfi=<^C^d`~;FOthuxav!a@IZMZ^vsj@+~3=ODx0yW!-DlUbSaYSixHb5J!)!9_7gs z1U+^mPOZzn?-u8%$oL(lA^ROxh<~(KsS8E+MPpdsPXv{qJpVywzGBJ^$bRoyY-*Dj zBU#uRvq`!?9oto1IYrS@=Y4^M`$Niv1l`Pi zjsF^@1Y^(=-sS8v$m6doYio>`u zTRUtK@__#+kyPZweTAp&_vO%mmr+(RMxUhcbm_9VS5zZ#l)||t6W?-nQor$z=lC^5^#KSs;#Ky6q}qRn{JOsvPE zxg$y3n`cF0!)uluf@x)r zOAW7oSRThnTwxNB*4Re=!Rtx*r9BBxy9a~0K*&YXk$^wQ^Cta>#^$y25fM7KvBPW< z26`vZVor5~d&`I%JMc@`sbHuoU(vZoP~{0(!61=7iXk;hc5zdGC{{yU>eHeR= z{kzRbQZiRA*9i9CS{-?lr`)UfBc5C}Z%+?Y!S_c7?*<6ryE(odC%pt$W~?eSU;J~W zx(Xlor*pG%ZKtF)!Y)G8UjCw`23-B3?9WooX_>uP#)j*g(4mNB|N21aH0nc(mvdZf zo7N5OX<(4a$+CREe_rwALu10hUp)id%wB0feHoG$MuDsS<%6)yHnI#m&+a4%ryp6W z&&%-aQwb0Jt`1L}r!z}-h1%k8Rfx|G>Q0lEQ0+Wh7^!7TI=BZ?><%U(JMBB7zv*mV zX%{}yz}?O;(vCzm!OdxRtX|dO!zaF}P<(9mh_;1=6TU6u*U9S`+9reKgE?>^fW8v) zW@+txV!>X48YE-Mk<7-5$K9cS+V;E%3z>X z@RI*(bCFYbgADJ#G=hDs6OwrO>SqZd$8ZoK=2W*R8nP5Fxm5!z-u3?3F50-^AZRK3 zSPp+W?`V{Xj0lG8?j7;B{1?R_wcuZ8UbQtv?-5+(-&L{z><`E2yT7A%+9*S!k(ZA& zHY{6W#VLgl$|8*d_q{;8@1oZ~NP@xJiocw2Y8d6z{8T!HtqywcbPb1@YQ~tSj5vbz z%t3+VY5vCA1J%NZAE5h>LM-)oRcqY%%4t@Klj0ybiM0Q)&ny(v4Co_pT#2ja^hT~`0 zq<^{d7Ql$k9CmI9N%QEz$gLK?tR#7SkA-`Tu1tQY=?1rNPs@#XOA~))AM~j}X!g@QNs4=FJ{9!d+2ulHZg-wU5^_9b>8@9kBy(Z*h<&1LAMlJ!<_qf1n=g$ITBU(OxQ}j7X;73XbvJce}G7VWR+s?3*e28cXMHD zx>0XW5C>fP#r7B{F@qxp_i2e2fExhbR+6 zd*av5I{#UUPjdkSO}r~H9WDE_lS}Q5=WSrBiSC0Zkj;n)@<;4?z}ff09wu%j)a07+ zOHQM-Ei~gaR5fUto#qZ|&+wahH+C#bO$bnu_Bn0BS1Md1C|(Q4#Ikqj2Kevt6$`y%)-LGi0-`#k<7?a;Hzy|WQPI`xLZ1W z6|A3Q9Xz-s$QBgZ;O1eJ1RMj8$H63C?KltHfk(6DE89%2;Nq@l$z2lR*DR}N>Omo8 zAJKVn`ATt$(*D(ABNb5@58(+mhtP(U{iV8g94o*e%r)o_)pF|J0bjpC;H&Pn z%oOUtiMAvdL`%5A@Mj)CJK zgO3H{yUL?;*9cPRhFLXlZ(KZ_FtEb{+wY`+MvZIwnV3)ioPfY*36Egkp`{+RnZMRp z9(D7#w;Yq=2q2hR4`1NUv-YrtA?tOO>PJ=k8s zM#`P**X#%WSb*1KE!7O!Jfk$N4K+vEH^;r~_g-iJ2W{y3`)7^G8CAb0ikgpmJ%54* zDlyjg*vbcLmlXDcataS_g-3)D_}wC=6!!simbbOL$nQBt?K|#qw8ikST*-~bW)3(Jyj_1L3 zJ0LwsZjli+6?Gv4k~Md?u?VYtPn>e$uxV9`=*xBPWv(6eY^TjvA+g|oZpnb7$O?S? zNHLHI(6Jwu6DER#9rF}PYAsdH?=L4^pZ2F99a}MG;M0KF?hJdQBe@}zNv}Iza8+nM z&pJmMpPmXB^YY&mjKcEjtw(?!QY>U38OH5L9;giq+Xq|#G zHsicoy|$SO5D)sQ1)OXJ_~Jd%h17Nvn8%@i)GXZ8R<|T408?HeV{nIj zcZvCCP$6KAkN2h-l2*wr?2i2S(xz3&Cm^PZB4nCU?V*dp0kf%7|5Z40oJ%qD*xxni zz!Bgnc7G_LXL;YKgI@_@?NIh{T%hyO6)mQ-&1*1z15szN-Gs8s@i?sAvZggK(g?R~ z$9wqwk`*LflB75@KXU%1f3pf%)FVfQwK1EcB8%^ane-Fe>_>4tP#YQ096ao<7b3>7 zzgV2ap!v^5?s{8IA-SgLdTA9nMQd4)cMt}(uyBpBivAUwag%`AT>OVA1stc-UiO7g z#(zkn125@o8^8E`S-+>+9Wzhq*WHPh+9J`4qd7(D2grR1mx%%X(FxSfP(7>eWqH&r z?a0{NO%WR7G>c66xe6IfX?7qoyT4c)q}dy*72saea0O8h zbcN5J1EKO_m2J>rb`^yaSI~*{M%s|CzIjoJzT^v~nX4o<&6slkaeU>=6eRho){4o#s#rJF zI-J7&BblCHmVMrv6BlL_4tcYZbCOTeRK-oUh2c&PQ936U@R@*ybrVU~zM16-F^@6W>sH%IRz;!2U=Y6E6YJ0y~X;~sAVKUDCJqUPchNVBmyfjynHdE`p;0thCO(P? z^7YY6gQ1@9*_|9?8RMaDcD9voS3`1|mm`=E!UEvo4kGFr(v8|R`>ELO`5u&fDZqA8 zo&xG>>>XQcpy1brO-3^yi%+}_<7=>&;m}J@pS5{6;M<60s8?!tb%d5PW*I>X*Twm9 zmik#IP=KojPuG|+F3OBfH6`NNCxc`zgsFlOAg*1ugaH058fOYxYuvnrL{uV8iw8^e z#to*wyA5L zj}tdxB~(f{A;IVS`07fol{rQ)c+Ek|dvR~+!)kEIh5C0pLQ8Ek3Prxiyqw+!NQzJt zc0|tBYyzkpZwwFxpfKs=h=fYw`_ z8U!{7$~hQ+{CMiG6C<;xDXsI+g_{WDZPx5zJNdkl-EgzKaw(k^MNG*_ZVQ$0krppr z&-YL}4Q9J*FL}xeqs`Yy@^t3*&J)d<5G&P=3rCZBTY%CN$XcOw20NZCqBXlZwsgK_ zRBRDkYM(XY3q<|uT8SHaO0KLnECAshChW)fc`5~4avzVwDAvL$ z5eVuvJ~V^_P)SlJi}^y1swmaQ(okTQHPp#lRIEWb;n|Z;*MPESQkf!>4~I@Vqv29> zHmYFwjjWGX*<73^TSgDgh=pFfclnMr*w=f~Z~v&|SQBC5U$@euNfW9V`94wLGYj<@ zj`F>qblJN``MCU2-Z2jIPd|d%)hRzLq+58kq1Y!(?>vCI4+u|9$a)BrCF_cWu#|f+ zx?-Ys>(0>C@emU(>Y%D!t!gge;8GYL5aSLmDx5u?OEy@XcAT9DOXBOcAFLaC34@42QW@kP*|zvsN*cbP-X_r#0s`gL%aV z!=(hp7h>)fXwgvt^CGuI0A*)?0d|{QkDlw0%k2ZUq<%$(CakgPC@}Dgp@ZV05d)H8 zM$rO0?u75w6KPU5?&Sx)?omJqN{LxN)My*#2#ut`TS;{WxX!R=p7vTvv+#p5GW0p= z1K>AQcHLroYX>Y((n#7U2^9dUEX1&F$V0f!lokjHn&_bj?;iM`oQBxr1o?fd+z&g!5Tu@4fBz}0s{51Un{&)?n^!Q@}iYN%ApLnil>iYF` z0r)V8&R&*%z&zuD5>0IO3#`R8I|i6-snZ@2xk}1{_3N`a=g`IskChzm{`!)+gy53Uqq8AQ9P6fY409OJ)bCNzd@?QPnO)8UgG^?70{1b|_0x;pkd z8~ZuSyYrUJ3rOJL9h!!+P*rubVJ5lHOxz+y1!kTG0?UHDU$d^lfAj+vsh^0bHr4~z z_;h_BVa7+pfXXWSL7wJ>Ioy~FC6$^mDw;ri!sYoYqX(XXvU600{R>fZY>oB^2kbh5 z+($W`j7$qc20-MvzJ-;+XolPdMez-$I5&zfC$k1fE0C6qw@lBA9^o%`h$=eUIwiZn z7Fh(i#R7s?l5+w2qD#c7m}UUgW5WP4iFWH~ezh{z^;d*ZMg%I2+(5zen{bwvl`%D| z;sw@Ds3BUWsu}jz9gS0Sn%~{FD7jugya|M`hjmKJ$<+a<0Gq?>hHEFJ(s`)AS| z&S)#QQB>4Ee%#8I<%~A{v#qM28meRh%Y1gp^r|N5{ZsTjckHjhZ!Q-SjiOPliyzj0 zh2|`w58V6#-9jbgRhD+I=|3eBsu|h+tC@cXR)Qu`%Wp&QGtYVuX;dCM&ntUhXd-gM zVz+(HMVpL)-myav5ReWAwbH*oymMa-*+_3%A%v3T>2mIg$}PmMRHMYcIb+oJKr*Uj zKV$E$5t{1GV?Fb3Y2&?}QT_oVx(52h5+G-Afv5z)K!bm2krF^eYs4|%>9E#E79a(_RKYC1h0xPwz6sRhbQ~&xGg8M-^GYXO&+^{G!ct!9{(TAf zUnmb!tykHp@y{?1;57gI^wRO`{6Qar*}Xc+mj7_`sST2v^Rj36SarSYE&Q7YPA>?U z*qGyG_)RwhP+Z>gLkJfbNJSgpT-baJ`ZPCkd7?GuE@;bvM~2#&O&XAiIz2u5XP@vMUN*|thvf0G%9Gi>U2~y>gM~hz z-a8POtp^pa4BAhlB>j`6?{n%|N7A6q?`xcOTz3~}d^ot0tWe9LHx_S3uT%fOlgJ2K zaxYmpTNAnMOc0h3bra4^4u9o=zac2R-2%XU`Hd2Ep&B8NM#)dxYU|oBsf(?R&fGQ+ zdrqo&I&I!sb5qJy+c~PzJrOzk8+*chXI+c-G+;Z*JxZ(+y_M>kI?Zm(oF zJ4AX@#KjlmPlhsTSCUvdY`_C0Ak1x8!qohgFl&wY9vTp8Pso>1C5uAa58qXX_Pk^= zo-5G2$<~CAM>p9!>VbfPIe=n5_7m&K%nlcqczHd`@a!}w*@1G9@=IB~(nbL5+FpVe zTqn<8`(0M@i;xJdkvl~+7Y0&b`w3#{H%U&^5xlqGn6U_@)v2RAW4Bi4_L;5#00(d= z)W%d=pFfE_01O?+PD1x{=X-ed8|~BE&zA%Ec{HPMrUajnbxx=y+@ZZ#W+*VSOl{XK z%VE2Ieyb}$EiLEtAWrixbZ?JC7D<(ew34(!$aVp$$b$Cf0Q41_+}@bQ%E^r|Ecm67 z@G;xVyO0Q==QH62WGvfeHQ=g3KLVysFd5i~&X|3^UWWZ@3f9TvOBoQvO|w7m6Sy0J z2T+1JetaLABYjNyd{KJi<^_gCFpPZvCQ|K}uK6#_^;$X=-CL=#vUeAibHOEy`yRd; zYx;!DAOAd7+=q92ZT#f)|zS_R?tYvDW9;H>fF?4mq? z)uqK!rgf86jrcU;2*lSXHkx|&HD_U4i@7(~lMNs+tz>fc!6p}0 zK-ULx;x#^=04o8H_*hSdsSam4b^1*6NA`C@C)#%ge7vK;d3g&4HiTDLZDKT+NY*XX z!GO}l1`c^xB-NEPEHi@_)lqsKLH6 zJZ%6l*#!be6OCj3DtoernbbgM^4T4`6TmMPK;^P7Hxweh(jki`5#D9czO@jK61x&z zTS!JM0E^<(wdH@Zhz5h(42p2j!tCvkm=+%&Hgj49WaFjOFa+>UQCH&EOe0|7 zmw)Q$Kvcu4*B2`HK1?LZ1w_CLRXX&mI>Stg6%zOO!N4g|5j>ZCOfFiaOxC>5ptYwf zGa&sa?Y3|`9ZUon&gfg^+9raPp$R7r?GW%a{bLXFm;Mw(UsO@fvsDI#I7RXIgLyK( zZUZh!&_q$vyHORu3Ian(3}#X zG;V!=K9rg=AWIhXCf6qNnD(D5hi$&js2u^N>K2BnfCPYCkuxPEUBMW{z^(z+?e8VmPmu{e=@gg_?~6YO-#|25Ny%|d$Na=8+9 zK+LxV7CmzyNaQPFBvpidR`DYHqBgIB1WOS00rVH@ia1a@J}MakKfqBA*B+^N^h+QV z%*tGDGb#8*J~k#klz4{c~V09%YYnLrr0I|{ki+lbyB-%cn@8J8;$G={1Mri zl?dI8X-YUAL=KS%fz6Ym<+YIhl>b6C)=>~KmTypn1S0b<`@v$0p4Tw1eh?ebR=}c} zxi56tvgRab1c+#Zv#_!ZI0#xz2Q$5-wUi`w{#@MF>FE1WacUh~Tz;s|TfJ!B#WTf3 z`E)xtmE>CvR1k~o+#TC$fn^89K!|8BwmW1DuEZQxS4EJb1>Aprf$|_~lP!05?}i)Y zZrd{lic`X?{p63g#*u${^Hq6+Njznop>|2FKMF>{;zvt zzSo|-@>_rYLRYWUX9_?-E^G?;%}AmV3g!asC!^M3wk8zmL5^XA;Q z>Gp96VPif&8sxbDbcP*$xuCG|F6~BvE|HvDu7R&BR8qU_5_LYDp>X3FbTEe>d#@h2LQZQ^WBIQOPf zWl@gny9Pn5K!{4NZ%4-7FI6<~W17|kQjX>_HZKiPU=S+=syO%^5n$5NPh3f^u%v<|1Z z7lYzG=UD6MljJIKD*F6p5qDXZI7oP?-#Hy>3Ial`@|$n*JEDYPgN2upP|9kDhu#b! z=`x00001M*nf9hq>9FK?uWZOdfBq;%fm< z9fzz$kFq5B_*$O|MSB!VqpC~aDxLe0479h9mL*&w*Ba}O`Ct>ru%rfgkIx_?%VNiif>kD*%8}U3 zByp^;i@BmMK`gGn+YbsS_3zgvg`ZB;q%QyG)`p{R0Qh-T^8U1YLye*1<+#o2APs(SQ4K2yyCW`l--NA9UC;>p_3_(EFeiIsm4tRS4GL2B`u>>$Pbl9KC5|0D) zO}Pg|K=>{HyefkFSr7LXhsFzrp(qGO74TzN7%l&rS6GY})ui&$u0siA%X8KJwdIf_UsygUldlRKb@JH8I zZiOSdp6&v`tz3^&{>{=MkaD_*7Kld=mYQZNkMA4w3O19-+c7_p$lOHJ9-h@zGZdkC z4LnoC;&UE7hKHiC8tHx7M*<;=idnkuKsK8PE}$pxgPXCb*d(PC@1jq#dd%-Iv^L=D zW4l`hQ0vqkrjvZ^E?j;CpnC33!GLM5E1`}w)oh(qq6J&one=`v1K!qX462FaLXakkj4sNJG ziDrNP?K8?Q2;F9{xGRYd&j=121d0BFt+hs4Xi3u~+rrnY&j(Dgya>jCkOtaPr?ors zn9+EU4VL{{UBuMtiyQ6-+%#6Y(gt4f_=f`b!Li(wTt};{0~%>Ke+(9^#k+wuWy*~Z zTH5Npaot+iPfl}U@LG>f^1AHOMg>91wwesuXU(nJdtHb= zRJ}HvWB9E!%$%s2r=gVJREc7tmczICVFTu`>6vqLwOiR=L|J1D)utKkLzMvkPEcXr zmFC7Lk@tflbenXaPd@iz4(E@ZFDh+9y2;eeghke(TmL*2ae~jCbP{z-GG`(5YI~jjD;(hL{zOft{RQYvEC(Hl<0000000B~+SRuIwT^{u%-#TBV zVHD}gAmqP4eAi|C{l<1`Y0ZZVj%~F^9cg#Z>3lADt<0kX2|@Mfc|l>;k20GqVd>3C zSwUS_Qpk}fYyu}JL!R#z+Z{(vYk%ZUA@$=;ZL^Y*=>37kma9D>s?1m76uEBd0=L+& z5Xc>+wNq=wxH_JP|KwJZH@pn*M z9#>f3<_dn}d+ONZnT&wQZr&l~1Y!OnWNnuZ>BcCOVK-H))cko!?iL|Mhn%dWlR8aB z(k>uSGDl?MsKu8Mm(&z>+JVxxYgGmQC=<+f{NXEs8TpeDEA1vjOW*+t3?M9%TgaSa zw=JVOzF98gdj=Y%HRc8=Maw*9Hr4#uZJOHtsWG4y)Ibs{3F7`0lX+b3l^)_PxY?}U z;8M{ULl+ejEusPfi(N+ZhIE4bS16AL(rL^3f#;@iUN-A2X$offJG^F}QnV8~F%K`*Yfl%e2TcuBFkIng3gGnyYYq-T3 zxzKwu7p1@a1#!b(GsfHV3-pz@dhKg+&+koq^(bT3OixD~^fv;rY}mOPd6D{+zJ1gH z|J@F6d$~ijpc{HRdWc=qGhfp@!^}X5>K>SAc&N0-duOK3Lm#r^aUbMYwdeZf!>r2*`tuI<$w6+$9 zn?uCITHKmpX2TQEh zUQZaFiLbu%T}Ovae^~)6(R+jEyU)X^2$XYg*}HaTygC7!2WA24HI6Ui17L3uD2z_M zzTZ6{0eO*?@ZPeeRcV}ndmZgq9dg;Sdk{7P;i&H`qN0KXf?_WBUaki@5yCtP;W>$L zy->;e_S)7he>1YLPy4a0F|IvGC0W{1eT=-*VMDjF0e|#Sl4O$;+Q@)VfC$%o)0`wF zR*BCF87?!%*x>ozTgBwhOf-@g_WCqW^J;~CA^l%xxrj6Mp8%F(Lg|USp|C8&Tw2BH zD`v5TX?eF8ryztMIw8NvCmv&|7tp&83}nb`LG*I~zeQ`P8-074WL)Y|@%NL*L`@vc_3iOM>&X)_DP$RHtG+Ev-x?{L zVcOB#0vFyR-8j7OzOPe<4QVp|v&gE1i_7zZmZH)}#^K^3)u+W&2SsYC5_~s2$OWw5 zP^`t=4~P}Yx+hq&vX};mo}=U=*lAIz!a2ba%<@`69O`Ea)j&iMZtx7o{3r!gY{1rG zE1aCe48g4o&B!a2X-CA{u(s>9XB#leg=kyz-l+^0WLgpgkJ{xInkOX8kl-Cw1q41r z=HvWS(-V;^aKSdlNLFDyh*=3&XtuP2VO?qq)jmoq<1D~PYFF3c0$Sj7rrjw99l0OB z?PX#dkVFJ@T7!S__#xUUMk~d4+`sS7yLdMGKXW&G(C#n98#`{9539ads|}>ahf2=* zgs+704Bh^ky;pT@oz_+d*@cYgNcLc7mr2uBDBwb{d^9Gs)&K7=td&2w=p+?K-&D6I ziFmrgXdrOaVKKafD4rfe_<&P57}n^BKY`MgL&#mwrdJv++o*`1sUt37{*&~FN{XG) zqt(XJfg)gCh9{2DAqRAxK5!(I%z|h3{YytolJH+#LH2?BpV8;?{9H3K&z?RYLRt)= zrtRuAsrh~@1c=E_s7=#@wf>@ulP*U>mZ9fP&{Wy>Wa%A&!R5Nq zXL9(`;2~LR$s7BVolCW`qP|Wbm6TTZ5L2O4IBEyw%W`C;$9{_gra$Pu(_OB_x;gC? z<3^r>B{hU}@eeLdOBZ;rc~-JuI|cRCENI$dO4yi52P;AU)SBw|Mr^&RdehZ;Ydu@O z8qsCPCXnN$aN)U_@(8iIT|~@`1VhoSRtMO5_YRT7pOwi(H3`8Mc1-7jQkK5P(@>HjqwMP*T~uQFjBCvnD&uH7puf@>acots4;J#B(k&A7fC z&vn?`&(Z#gu!}gYmzeD~J!V*wO(EDirBB;2Wo`)MmL2uV6p}cU$LIO>KZtnz>ri5J z(6M;y`U2niIElBNNpK)W8D9{8-xMFxh+Y$r^4#bJAOl_iskYHOxs=smQL2YpJ~h_L(HFcxE-q^8LoPe=;0pKHadzer74y(rXxydo!3%#J_t+!A9o+B`@O~- z@(wE3ynJVr2>0~hI1UQg#ExcE2`>dr%4|JG*nJ-?(qzTG2Z6Husl$&tDIeXpJy<22 z3Gvm{KjAlU#Xk-CTJH?M?V%`<rO;kooO#Ue9T5S9mxL-Jq_3MvJ*s|Y+kLel^SbtgLLgztI`VovemKKi zrU73PQCjzGH{u6Q){F-xTz&Xsv{Gshxf0;LUc;yh-dfdVm8{OibOYRlwr$D7D-UFr zUnq@f z%6%Gq%Xzy57Cc2>3$4#0r?mH}*)k7*y^e9hDVpcgu-ebzzBkV=bn7&=<3fuLbL2Oh z)0bO}_TpBOvF(%So9AUUO57>jst3%>x2XtL1FReu6E0>`( z%m#2pE{v&1(cUz+w56W5%u(z9`G5=1ySvEr4Q>)E*Tz(z3Gnp2$&GvR6kBT#5xSIn zvjw(2Ys(;zAlC8#(g?`;sisWUY)k%F%-s)67F~RN(g+5QHX=T>K@J=@tH#la)N+HC z|AB60T$p~Quymu00m2EI1k1H@&mDmrBupH5pMS&N79x4In5B)4F#8UdWStxA(8Va7 z@P8T4>viq=@TFFHk%KF0_VqHdh|RcRs7fK0+Y!L(Q7W)BE_j*Q4#9)$JwH+v`uRH zFYg1O{hX@QJ8%cpg9NP1a6h&$esX6R*;U-#jENN=fg>ghNC} z8V&JNOKInSsK>E}i!1pzIG5Jq0HbJw6w=d{h4U@;e ze7Kw_Z2+1z2M5$QvGAm;Qsg$q2-O}=@70(zwhQAd%c-(Nno$>7I#hYEzkAcIDH<+f zjaYfO^Ex+x%Ge2FT$uB_5Qu5A0sP|K;UgLNvPi!W8dJ*k&nwqFuUzuIbISG4E7v@) zIGk4+70X)aV6$py3E96jV6o3f`fg2F^^3#)RnI?z%~^Lqz;1Vj-sc=`511##bFKrm zqrWhV2>;O0>k0Cq=uF@3u$FQ+oV&XMwgUI==A0cD%?$lHkxfwh{};hLSYugk?2KTH zn1S-KN#7uD}`|KQ&pLOMu+wUx}E<`mT++`aR_P^f3vn5*g`C;8|S z_Y)a%C>XfxZH4KJ&Mwirqx2Jsi-crVh~<^B{2Y0ATJH7NZQj}KSZ>UvK%TmcwKVgR zq)o@l&>kI9*1 z!{Ia`X%AIfv;H7e=-A}U+}z49``E2oU^Z44<7r}LXXhu~8Vw%@=7tZ+>tH zA#Uz|o3}N15^GDESKO&O0T{8d6n_W!f0NH?oTWgsn!gJ)(9C<}axnT#^~^YKd+p=J zXhRt=7>qro#9M05*T=w^J|t;FAA`72+cSr$&@O%=0@nK{%JulLh>e%&>D&%X&7v=& zF;F{U zUTjOyQS83B90N7>TIVIw3*ZCG0>kn{9Ys@Jb^P)(XX@@72V5cfEKtSFNoMKRdPr;@ z;a>FJt6ivIg)`XZ++27~8*lz;gN6^cwL0iD_oh#75sZM1-Z`@_L<(Egh?m~4XmWUI z=t@E03Hjb!!Y(G}S0D}@I`J0k*#AMswLaO_x>VevG7&4gq0BOfo_mN;#*#L|*hwrx z%?v|zq=7XaKF{X=(fy4nWcVY#BCNpa8q?$@s*xzAfcIeYDza#g| zaH)#8=?qgz=-*Ti)%Uu4ZKtlzd_ zi2N8IeM7UxujCH(vE;7IRg94p>Th&m?8mQ`o5y^M=?a4d54-&(@3WSIzXLX>|1``#N8p>{zz->i8U@JJ`Qj<6x44iZ(7mqmANLY3R42*+!wK3^f^F#I zp}fmV_Srs6gZ^jaYP@s{KpeiVG7u)7jN2nU(B@rMP-lBDwsArB_QWqG$`D~uJ!J)o zJ*28qJY(rhl9BsS8dt8*V) z#(^xu;t8`dJu zp{E&^=l|236|Lwhcu8#iPaqm8m45Qq_LYwk=yrqap72@Wgla07QF2_A(Zu+1=~J|D zjl;)`+naNe%_L3ImyS%XgHHSh%{Q0BrxfjLR?lR@Tg_eyX;ObE!yQ5W6aFtdw zciFzd`RRes{hC$ zgmxgTq9Hh#9@YpR)(9Tf2p-l59@YpRWH>b7v~>gH6-Z^7#xARRX9uxIz}}c@yWKWy z6nm*bE&};kw828tyd&ihoe35AWCKR_Z7XRj^4o_#QQqw_YY1}MUn zHo`B4_JA_$M#8fzvoj|0N{+Nr`i1Q4QF@tQO>Q5h9yTjcvY@73wwfeiK9gK>)1L*$p~bYW1W8V?xRKe6?%v zvH;T*c!p9;uQZ&J(!swura>fiwB}9Vbklo<6415S|KIrJr(9FmYH3SyD#1b<6Sfr0 z$yK=2ERy(7CTY#On{o6Q^25^O_%E960{!+1g%7?SuT348ri;WW6lKRLV36b zXNI}r?3yQb`>jp|irRS3VqcuMvn!ZOg9oO$7A3Lf(#LS9kVm|`wYDDDlrMCac++ZE zrh~Fm#8A@Stk{28?#~7m2}cuwRqVCRM+@(M^>T-LsW9sFV-BLuA^n755S>4AQE(g6 z1kZr#X$Pr?hPSG-tKZPxQp&DTfYx!qz)E?D9y#sILnCjt=M{Obu%fxN-5yeVsaY5_ z99$i**Y>@QBHEkG%fz=F{9Hk8ZrZyqLrjuoX<3F<>7KbBsvN|aF9_E5?B4;5JQXkj z{F>}J9He^$1jSKw7;KJcDQE*|*?)vtJ()Kn-`n%roTk#m+s%Og6ZNbr}6dQcDyi*xj;@sL&R@rJ`48nIo{7Es8c zExwi64hapj8c6-Uj~t<2dj-$TmqbA>aE5=`@4N3max+Ps)XSy;7RY{OrOQGs8yp?l?PL=$8Q6i(uOQL)F~U!h<|-1X~3R29Tx@jmXX=0~_sb&sU^ zbRqnp-*S@Dzo0H-D)}!|Vow&(O>~w_t@RhG@u)UebrF{n-zK}x_ zK;b?zMz&7u|ey|zd{7*ch+aBJ28eh@RkGIwS z7PO%SEPQyRo>;uTrusCjW^+q!wmVN5SYfIpvx{Hm0ZM3YATR*RPKBDn@6f*lLRqg* z?Ebl^l}k;n|>i^^?$p`fyn&R42YLW>c|GMzGdu_H4Wj$?`#MTlj_a z03|`#^7n`pAS9)F)1Z$uB(f5U$P>vy&+v87-e)s{g~*8qe6=xKEMSnB{vjV&FgEvZ z$g_PGXpMXk%U_v(fy+^Kl4UAB5({HbQkFGI1+WWg0>dlrdX207=C^OQlY)%QlhG4; z1&5;;XqKfc;T5Cpel0E~1<*=1&v^BGw9~vk=#gFTw&H{mLYaJoZs^(JoKEyjGF-S> zJgqQHt6`;9y)`2_*k8NvdUV3!km%NuYQsi6*nFCF9uYO@mwgx=o`Wb&7L_cE~)WB z6bFu(keuJ!g0ecOf~U=WPAHe?WMOixMYyb&c?GNRbo z6$k0=kRG;^MC|D~wimqyUEJ}ZF=(v02WZ0%c((q+M5@8=p&xN@TKT;d&^;a@;a*6E zk?JIwb5y!9j9u`&!6pKtR@jSA^QVMr#j-?U(}HI(Es@aF$$CvZ$R z9|rCaYy7F;DB9xpLjC%IDjOqJzwIO2pQoDfnD{VEQaLp-PY;=5n@R)OaOE1H!Zx?M z3??{REMj(_44B!1auOjNu;UKi6@cGeDr*r%p934L-5Q2v7O4pao$lbuop z(I6zy85RedvzI!`pxZ|n=WgJ%fdPG;C?+ke|%kxThvXFKDw>27FhBGr>?YK z65A_aeS#`2t<|z=b0%piqzNx)UB+)HR)s1T`m+d=@K#8T5}H90yw;Xd|k%#i!hHo9fXRUByzAT zQn?|q`@6%(_bQnbK){nO85B)NiYJfj1( zXHLgnbUB-tdRo`|)WO!(`0=l(*3K=BNMq0TR5QztfGcS=q2;!B02T540hyKjK(*>& zNUByKA-Igz693hc7aC{80~36yeoc_UXhw_0j#}*7_?YD_5%Se~z@XKvE%s zd*CPE0?N{*wPZG(Xj1GK-}K=ZcJ6`K`A|(nFH6M$JgrS=)zZ;ub5mHTEPY?~j{sl< zl0VRSiFR4#W?H_cAO+8TtnH}Z2C5ZkQ{631>D``hyY>vx;MlO(LEEa_eQCM0fM6nW z=mpg`W_c9}nfk20Pv4V9zU_8sLxhO~gsNDcW}sl+y~n{)Rb;&P6ZWq4o_yO)^+Lo9 zZteke5xW);EjdJ~UphJq%+PxKy^H4hLLqn zarg-|?KZE?Rn}n|ZXAi()&$x8SB}FQ4Va2Ju1cj~G7M zm4j9Bpo?+JM3pcNDi4CkeiG#E=elI_=b4`q8iNvCCKb(aSL(9Td?b*2WbxJ;+kUiG zBzAGO+5uE$BF2FL)PU@ymuNTbb`3wa6g@%6D`Q_JM>fhnXHf(S_^V0T)a3QVVXkkH zkHU{@#n-b;J%p3Sj2M7_OfP$A41<>Yrg*b4=NEqyR-3ah89m)U3y5T9o{n{>|GEbx z^llW?i*J{CJvai*ZMB=qok~pU1^RnI4#NtE1!3gn0C5w~gIrFXx8zP1ad}PS&R$FB z=6$%nDy|Sj*Mc%V+ zVY!(q6EN`I`DmD{h#?4q^ngA`POb}0wyzcT+-id+PJ!#aY_iMl;x?{VrCiSEQ22rcpuenrP!MkURGU$Kk}fdhy&rUc$l71;V)4M z>sXd#Z-iGF2(pfY{#)&L-v0B2OkUH9z*MIq>Ml7hb@s=xfL6_J+;Uc_rAyNXG=P zF^kMp7ArvDh03Iire*j^_kK1oJ7GE-cpd}telS#~)m;j0u3mamnS&mSmkjR1n~E8( z6E!wjBUoo}nri#BqiJY~CRpN14uAIA+3fgx{2p~szp*!wbc!0#vn!{Lczly zeP{VJcd`pUWt7WY$Wfw`8+8H}bmW&n54!VKAf+Q~p=3h0G(UBNTZSpv z%;uKiDd)2G>Goy?IAR-fhn0oNKqeIC0C z+{n-&mlSiov$O;_X_Uz$Gb5HyJDZ?Vv zq-UC91Xj4)1h94<4dq+qjpUoR05%1`gr2jBTVBYTOg7D@LTXrOZ%}e@G}M^WRkI_F z&|51O*;LcZlE&+g_9dEN9MW5oX5t^>kZUcc`T6+d&07p~0 zzC!koTNeCe1HT-Enj7^4Iix2n=I*gz>!wPy<>7GXv3~KAAxLbhg{U}9xE|Sl^R=^} zlhI0BFc2IR-_^k)h$6S%j_ekkPHGMjA9g@zOY9uNp$XDSuuBK)Ih3~{B4liw`;nLCGfCUsRzl*d+e!k8xX*_-8YYT?PIxGMj;H%?QKdZU7UGA#hlo;kz zDY|><#=)j>Uh$`Pg@;;IDF4wsx1;5lh;qZT)7n)i8~MxXLbs5Rn#U;HNN5`M>P(Rp zSSObh5?5kBPq-MK_$ge;FRB?#&2a&7p1T7_bHwKhb*44XQlMBawp}(`tL+1Q*M>^( zf#3#X$5-nKyGV_`q)M~;9)w3mS(RDr@90hoJc>LHk19cs`k|9fTj1YY&?rPm^pmv1 zH;nx2K@-Ss;1YeA%`^3#0=bQZ67JsOO@g<*pf5JGe%oQP!P0~}P@Sr|k|2F?u0E!L zsXHOFZLX=mgijOLOKdW+v_m`OVTl2u04w|pVJ%fZ?l$g_>B<@)y8KG*KugZs^#$R{ z9(MK!0Dd7)^x_LDpFcu)DjAfX&^{h?B`rAH^K2vp?ScMpz*=YGU*sneTL*<42r49LMJBKqigS7@oDlj4|HMMf8eTG#A|Q+ zs*WxcL;&&wVO^kfoMd#Fu@4AZ;ax7G68nb%e$+QNDe-mPbZ!_ZvH{=ZgAxJ ziMDB(2d_Mco@{L>No$~q|11__#YU>i2x$#Nl@TG}HJ0O>1z`R-0mB##+F^x3B=Z(} zH{2Ch68qe3ckIQ}wLEBc>FA?9Ej*K{_sM6)U4)f-qFiO*5hUjFqu}>W=hQCf{w7kP< z)83@<8a8QF#IyZ4 z=KHKz2O?B~b%0Yge%DO|MY285_4%{%G4vO`K&`i?$USo>ZwYAI1Zq!HQCl{u+z82$1BN&*ZPLAj@neYl34? z8G)!ejyrgJszBCX!Wo*AReI8qZeCG>!}gSYq&s3{c%|B_&tYr0%(Dtn*@sq2l*Rv@ z1U8=`2Bl;Uk5?WSJJ%B;0X}o7++t9SHA)3#p?5)HAjl)KMc?n6MmGD2YQF+#II|w$ z6{nZ{xR%;h847eDP;Ej)m``;j6Pmb~0!!z1Mmz?VZ|5EFX^DVL#tZp@WJhLS4{W91 z@n zk3S8ic*;}{Hg3l;i6!^2qXRObJ+NKWRlaK2=K5mwHUH1F<{8tzHYY?7i*Iq56IZm8 z_*xcG!iUt~Oe%7BzFoOX;*%RmX z_0d(;Qydfkn`B=oIc)rA^M@qqo3c7Fj=(IimU~$YFwfG7 zy6-)bn4^*ug^;2zied6F!E4FBLRqM3mMD$4eUO}%1P%W-(y^^)HId2Vj)Ua_@0Qd+ z)N7PwA4@@ss7r)mwslh>F2ng*uP_^WPFav%*Mm#Hg0P}JNV;~C2PB8T%x>^k zJI6QC;ZHG{$lGs%ia=#96eku!q|68bW-n2}X>zg|>VleF2)Je%y?X9@Tc6LS+2?yz zhtufpo_V_ZUy8_QY<-EWK@p7BB>Z>94_aXeMZ)nh=toU6Ng1lOeHD{Tj2-^v0((Q<52LdteaX!9m-c&dEVs~At(GJP6lr(G{xBMiB=E4N(y@1F0 zhcfQq=$+*-wWEmqn!sy01o*_Zk>Q%dwLB$5j-G*HY*6l}5H#74G?+3gguqa9HZRm+ zjEi*DbGWGc3$aIX)p?9X;6U|0QI)jT5`rE{G^?Lx^P(Qi=ZgK-n-AnXp$^{(P_t zCUC@z;47gKh4}`~tXmCP{r=!hziC!UUS9s;yXdVh_l~5X08V>4UD)Zg`kDph%xCUJ zn3#Hp`(U8_=Mz_tfwlWYHlZ`UFK8#vI96pAxvaYH44ZWG(S zQo%4N9942QMXQkO+yNqlUu{j}P_E&dj@$|?!Pyrs#J-8y(Z~- zi_Dby7fMO|@R*u7s<0O0M@zX?FzUFtl9d*@>Q$*n7nvTVv1%mfJOR^!|23p+lyoo@ zh{qr~zp*3$34dQKXz4E5qyM>~+SXmKvs-+<{kY56>odVH60=Eyv9)UA-{0{) zA(Lo~{@Rn3%EwpRbA+C9CE%*dq3z9cU`?)I+S&^~!27G~IZg zi|ewk=cct60h&dy9^J=56VllxK3)u>)>r|5x+4_WOA0lnZDw#Q`yl~lcd<;3g9rFSryo3%s~As6H% zys6QFSiqFiaz42@P#`7qA_f*VO)um{L zHK>1#7DB@7c=0SLxMR{kc|gh4k;))ARK zJWO!@W$@O&Dyn2KSt@Tx%z7m%gU~@0BV;7t+N5l>6vHpdk9zHzDOl2oxy;bV6ax+! zpGKK5NMRgZ2BO6X-+UMbKj+kT`>uCr)_Dc=@T`gC{zM5v;#<@)5- z=BzZ0cwe`83|%HZ`%IgJqGm56|=i9n0CyKLxEDOr35Nr2X(ywvEJ3%ZKt{p2%`p`mtbA zo1DCtE9DP%HqUZ@f(n6&Lh587*i}5GCnx*reS>;T1njo|+)~6xWV{3ha4SDC37)Sd_s*Vb;Br%YCjFlDr3NS9CG16`=<|a!s z%k=S$$myE18%o4$|1~%yWMK1(-P$O4P~sa0>^L^(@<&&A;u2Ll)4`gSq6wR98crv+ zU;vLw97AM`ws4t!sX0+-vE(VY12K0Q4vljAjw_%R|Jqa-7JArFO!88Y^Ae|v1D z8GWs)2bi&$x{GJlMvxw#v&2U-go5nT7ny{mqZMNb_#3Tr%jjCc!l&mJvGY*N*YWej zNx^D@ZYd}ktm>e+GUV{Kl`DLQmPL~^#9wW>#iaW(s~tRuJ$;fBM(#=w<3R_H=*5T$ z7HpWRkud0}zvhLluLA*gv_eF1fMm1TdYq%Zs0MTsBPEk~)V?!1t)5@h%fI`wz5UR- zP03MCsXsb3MFov}E^=vdh9%|KVP0s*>P?tB_}3`1H_x9Zy{@ZhbBWK!`wuNWY?BPfu}FM5s){&9Lak zkh8EUHAK25r*2Ku!;sP^R>57R@^FXT{}VNqe%X9z z3DP_PGj==ljyV1eYCcUUcoBy#J#%-KC~K(qI8L@fv#%{4Y9gYF6IttTR4}dVgfu6X zV^!88KvQ9u0$E!Sa;k@Er>t}gecC*4b6^XH*?iLV0-m#p-D7{9!}6M_r9EREi|HT8 z%mub!4}Q79!kpRgY>_Q8(Jmr50Fv#>!K=nH(ht7m_=q2JU{ssQH+{-bPner+}tbOAmAA5~szu-N-}I9Ew_ zi^+p3of|)x{kHRwof$n{1sxP%LC~5ZOdz*^c0Z{ohUx6`%B0;06H}ay z4rc~gsBM@?MucyoY2LNL+iD!8mR@3-_?>)7-lBW45>v5ku@~WXbG2?4^>6-%$N#Ih zo1l$nVsW7hzJE+Tv~J7ai^gAfj*M&FHM4>%WfJ-?r2RTAv(28-QL>lK`2_X3tnbgr zBnuvP{GcDM;+hX6pnY1jf~x*Pv%0;_f_d1{&vbFK98opOoI4~DvxxJSpA&ZsRM>A)?zvh($_~-g#P`Xj5+8Fh|PP|#PkY=Bw_Hex@xPC zpUngPAVuS2j{A%f+h`E|z02bjeOEisZOJJahnCm8+ z3O0gcJ+z$U5V=1RA3nR$U!2XhfVC@)^hdRgL!$*+N&m<&wY1#&)$35K?XfWBmyiM0 zn{q9*nJ2bl39Z=sRi`**?Y=YIJ-WiR27u(1Na%Hj3&%!Ept4y?A;9%yNX>p5&5c8} zqT@!Ljtn&mEsp984#Uyy*OIGjyA44C%NcB{|J4tDNcM!54^mYO=yus|XqtJCd7U zsrp0!9S|?ojy_g8ALBx2S^}E9A?>yREY0gsM8lc#RFKqMVinxg_3Ie-k8amcQ^AtvX&VU)c1-fOsO+tVEL87}bEsBB2}E~v!DzBlM^0Gr-4Mfn3~m!-KvITh z+mWho8<4~MCP(GKlOn!AyifdPjrb;x%)`rJP-o&{CYw??W0xAfmQW*=o<^G}hL?0Z18ak<#gJ9%fafoas*Q_FWE9cwY=T7REy=P(CtKAED_RdFxhjA?s1O` zeG&;fR$Hiru4eLGZ|qPWj=eaHmEmzU5QV&bPs&_WqYJd3U1lZ|CI9INn(A~#eO)5% zGC(%C*=McrjH6U}Rn_zO^n{Ei{l@ljD$Ai`h_{(uLR5X^gw+VHHyxGM~YGoyZI0!j7P>C@Z#PT8O+S!+suZBEA}k z_d(g;%9!4w8z(0VQ}(JWD{dIaGTW%9@fZpAi!KJ zxv87wtegW=Jt7{oick07qpCUuOk`mgxXdNAx909+Ta^AvdUBAGrev=hX;oHt-yi#?x8rI$%7xkEQLv|kR;pBGN3r&SmlEv!W`29byFJa*QO0SZQ1 zTsdZ`yIF1N?9P2hP5e<|9W3jdZNlG~h-bI~z%J8uRA|vLjkHROv_PR#V8%lh4nXfL zXt?Jhk(@)dnBB;R_ShH>Qh@)qjH2AJN!@a;S|kKzE<0Msuh`=(z@+NxNNB} zy2C+MBm156dl#Oi+$5Nxjjm7W9o=Hy+a{Q-Wqq1dxE+y}*zAj{OiWE>io2#rYE)2zj@%?<|7=4;N@pjaq=IVK z6G3?Spf~xrU4^)dgwusiI2aIq4#Eq<)+?Z<4Qex~fZfZiOQMugCGE@P311c9g{W-^ zGP87Q;9$meM4n98e8U8J2+dU+#Ijyd*7sX8Y|p@({r=_-aFzB<9~xd={x5jV-_uR$ zz|tap&N?P5%nZE*N{-jwztYqITtZE7uW%) zos)oXSqh34I_Y^QcQ_SO13b$NXkEcZ^`%Y&EKoOC7MUz@)H;iY0Phr6gTi;)$P?~=}N z8Cam9upT!vB4~ak&|EkY)IV3Ap*pvQA(`R@bW~$({c{}~M&JOQF1@UZ3LX%35MwHRI23{*VA$NQwq!!*QEDV_+l#QE+FiO=Y8 zE>K|K%O^FwwDe(4oj5U|U0VAl`$}_$Pm4m|E^kK= z*xdebN8N-_`oJaBeRD})=8KJ1=R3znZ0!-2wYCXQ{KQ1~y6UNWUOe{y6sL3^z0fY=2BPQj%IbpsL{>wHCRHnZotBff(Xr50JB z4G_x&gsP>?Piw|wzyw)ht{z0jv2bP<{*>EOr$PWvK(N1%s-eaBv6}m6P;xIJY#xEh z@tS{!praSHg5ecQ-huMJi)z6erIv2$xS}joFjr~|f+wveehKxpG8`pu(+;XPRj)=WqVKVIZrkA>&aQfp`oE-)gsBbPedqH~53J zc2_RsjYZ9kVL1Zt@CS6YlY_s)$|YFaH~{Rf zIB2M?;){N9rh<{ho+BVf>wspNrOF`>ZMT9z+E_ zo%(|yn6{-l<3sgCea1&~T3V03N6Vm>p92YdgUOVEzaO?wb_BORX{$qEW{OL$YtxqN zG$DgNg~U!1yZWjPzFJLz64$aYRuz-}FJTbG=S|vaHCfZ|SnWUBNGgl7HI0OLJ z!Mmr8qTh^V%5uWAe`$w3*`SxL0owJ5i6h4XOHU}8<*xBnZ_`se6p3CFQ@?d~E=vHMLA8v< zlRfrmjCA^F%GjY+$^h$3%QP5Adhqx6YA>n(Qj1u2W}dH1GEnBDC~vy7$20K;z7 zc`H`ttx-lmK;utTu=JBN+yK3Bqz2=&Cb=amS~+YwM9c&>?$t3|(u&54aV7{0`e{0y zxAT_B?SYOptYr2i-&UNZ7qyv`=gnnc3f?3SfT{Y;}cpc>S% zl_^@<(yU+{XEkNnRl>+WGqyW?G}gs+2x*S-*?d_y5oLxVURSbz>)B^F>A;EU+Le%9+g#6D>y?m z3)vsY4|MdiqFy4IH~0)(-Er3ZVZGcEta5wyB?=?Cn*8(gZfs)mKd8EHS$t zf8|bVHRm4#fFLRiS8Y&0GRDPG9pbeO9=FI&Jj8C34&y%_nDD=Cav27zpL&34`raQpDb*EJQN zWZ6jhBRu1uxuh}SUVe4(k1%grx(L$labDqRX^lOTEQrmL4|=Sct>UjUWbBsj)J_Z_ z1K^5467)g`>$BF>(M^7w6lsy1&dW`Fdl8!a?_g8+~&E^q}r z1m(w}8!T(eF7-Dgm$uA=ID_4)sZ)*+6&RUi7mHZL(Zh14W>p9tHt*@8B5up!l&k&F zxA}^A=V@RD6@M_8^F2YA(D+_&e|}mlSh4^eybbz`3MRnWa*ZQzkJGq%Fq87QtwG~#6hIR@+mckuD z-dlMBhL4CzfkPp6NXMO&LM$}cKe5(u$xYfVUl);#t+yYI~`?3D7$i`OlLUPayjL=$r}|4p|t_6z2@?+1YLb*IxxEqTMSs zhA35?$WFpp9D&}ThzzIwC}v&0g!vF)>=gSbkwfiPg>B>uh%}3L`w`fntwuh=YU&Z= z!$-(GYd&r7kow%%S$PGqR%@WU&3fIOcxB2`>?6wowf`DuDvI*8(w?e_>zMI}p$r+@4>N2?|j z_AdWvj&I`^j!_oCuU6{#u%g!G21KQIBb`!F%tFzYkifxv3ESVS04@4vO37y^BpGZM zujUqc1k68+A-H?r%rl;n8d84ymo^kR1f7pcMK8g{&W=ckoeOD`bgd|K9O785kYXMFm_mJ>DahOa{ z{_ovVJ9&+AQ~hSvBOsCQ4L|0r?SAuMFqJbKDbD4I5(b-YS>r{*7VeM*1|)<&6eVM|DNLVzQXm zQPPze?9U$BflV@qiKHmwSVD?h8$)jkgR@gmg$6i-(eyCtb*W8N$cN@|i+z~_?u3v) z-u!ArJ`*!qG0!j7emR@V$1r70#VRpTIPrnyzOU7q{uul zSZ=bp+4Eq$RuG*sQxaRBt;upp2$!gXmv*y!Cl&F7AdYrerzJ6sH9Aw;H?ypVeuJuk zG{YBB62N|_#=h{LA4%iv`3r)yeW2ty5B_&T-d7-eBm{Q0&>3^bQV+NqA3*vWRn>iX zWhms%tzBuHtSr4Ji!|`({|8vYyUB)VnLkQPy-6Zo4ZoVO_-^nB>{WNQ6}PC;(|=K= zrv9T#P5nleoBE9{H}x7?Z|XF(-_&Vozo^pFe^I5T{(!>U5#84o2IAjXA|cIUIR(-N zI}9+t@H4>nmstN0o-%m@`*L+&Hj^NQbaNo?jon;sg-K--a`f)KV8|g&su36393H6q z((@(Qql@jWI}st>p?A7de+iD`3jic0?hH3H^86MBe=o)n)Z4%~W?l&(zmS#h+Il=S z#gyowS9_w;qRk|`a6csCRY_-%_n?d9F&OR>@b{*$@!U%U(5Zxrv42(&*UUaiuLpJS zDpm@n)2%ZiNuZ0U_0;b?}@ZGvI$pp8h>6g z+m&w%(IhymBLepL3BbZRI<29`2!Pb>UG!AS$l)&+^wnCFMIF`GI{ z5!U*-AUA)Q3-k;(F&qK+hzwv-9}`d^=HsMcnUUe;s{WIGP>Hn)scton2jyLLi71@| zwW2|%3C6}uy;vfE3W8jYl7iUHIVb=T?Y<)=mY~AJvj&h8_M-IR=BT}L~S+exjVaM_42C z79zS9&#Q*fdVdly3y1htX}|Ht6$cy%fRH;de1k57o|gbaXOZ)J8=cg65aN&*W<>#f z3yAAtw=M&Rg2GVZtry;i;^qX3>yVsYir^q}SgN^YaE2)(!k{DJOvhZ1AkioL>0oRmBJS z9MmcA_W_1IHWRbt6+2`aD#Jc}Fsoxf zh`S@apYswiXDqRqp$O6>?6er=jzE5_1l75neeq~Z&C9!KYvtW}bW{>zOMqA0U`E6V~W(VkCuLTir(P_>D;v<%f++n zDqm4f)Tj7?4&BHMcOFKJKQ}bafmNxGXyEstbBe$xOr6rE0h3!<3OSNd6Cw>EkDt7o zu$B%c0DrX*>_mZ5OeUvw=Z_m>%F$rFrP2I{*O&0Ei5A8lqo#;cVznGcDj~# z>PWA_Nu~JIX*7SeJ6DSviAkXE8n=f8=boq3xWWIs_#6FY@1%lscDKQu(2)uRTrc0| zuzDQo)c0H>r5{*=$6NDQPMjO{oULJ=y)WclZPs=__@vO@+X?p<($YC zC-;NqMixTHL_QV_!?Vyq&&uVUvIQZPYH`)%g#fn|Zl z)J!?b_|3_f0jofy0YPv`0$LTz)(?3}5UD1MggG%;ZP%p^MTrak)oksdwItwxCX$6u zvk%~2M{c3o8;nxNN~{Ad5_`!dSf1+kGb8SK5SBplO_jevXIk^U z9c2+u%6pA~-`g`s3xBNp)uHFH(f)k;CsgIpn~Z-GEnC6x={7>+1UII#c=ta)P9!?)~AE4M-aW|jMd!-ifAxbp2y4b zJ%z%L=KLIA1-~izEKacMT>*}hrq7eCn z6}v9XV~pah0*84qcIMdaO!_70i9W1w(mGQbDI-$hO|gTpe#XBr3Rd5lvp1ZgHk-Uc zwU$VwkpIlful}qt3o4sbItWVq`}}-6En=Z!T5b6%RJo%mOgfPE_w*0blfQIkJngN?1FVdF#Ei?E&@oH0-DE6^0*6AbuCqMXi`L?FC-c z89Q*Vz%dJhG>D(ZgiVu8f$z(Edz;vmW)5XVZIbGUMi|I(Mbeb6P=a^pz`R9^9U5grh+t4d|2y{*?;eGIqD`cRXr#=NNk}bcoV%mcf#WEcdL5p1lisjRu(3-lq*3{ zgwF?uqJ-}@*kY0jR#sjp*&eJK!{A8YDx8>j&__I}=G!t~-A(JPqN= z`0U&+HM~|2^EKyR8vV_$#d^_FZTJyW$6PFD;ts@t^9!$hWjy-P9XTnkHTj!6Unwse zPf1BgCxmSh|AS3FTHxO^W#b>OAhQ8YH*I<_iGv-p7{)-ow11IpKy0Kl*U#^o+iE=l zF)#l)UeB79y-P(oYYFmR0Zin%464+i)+dbOSOE|Jfq8%?j90c4Y@`xn92v53MD`ws zaB_{)W;)QSG!(YHeHwoXk#) zDbUBurU1qJUEiMXX4xzbtVFMlbkc3QRvbnlTDsa+=F$kigz*h!*xc-7SUCqUz-&fTGOE*Vn*pAGr7tG(@q}Yvb+`T$zI+@V_U`5nhd8BULzawYI`qU?E%rpT(iOB0^7KNrCcXQ z3IdJEKRr#oVgs#x!LhMA+fRloUCQ|U4=X||R=mx=Ot3BPQa#IJOWnHAL>$BabjA2; zK^UD6h~bIE>ArF0F{zo^q!7+NUc+O*Bww6vbLWMIDJx5 z{Qetb>N)x4|m2G;|~%_;j#{J!a8In(++V_)x<{Mn~rN&W%%AXfe`W2Fde7c%0Ft5}V$qfKFt zll$LuzlZ+Bsb+T|swIWOiYrFe7v|{f2*=g6!u_;9p2)I4@HjJ;aJa=p067)G#r@Zn z0hesdl%EZkxQ`m;eF5VFu<$*dVh}nKP9*X=Z%;k)7GI$51;3qM<2`5lZML%FRc&%xZH>ZSLgb~O|#<< zo6oW+-YQGFdujKLOViQSdg*EBXJ(D?H{AY$KoI&is|#zsMjwrZ&w^TH;IZw1C&Df-Kgqmdm?QRB_HC4^&JD08%K6p1I`o!lOTCijY~<)(nJrMUsO_R?-f?U;Mr2)W-4w5!VaK`iCAimf{+a(YaP#qQUGhl-t8JN$N>|>z;_fXl|J*HMwJ;fX;r8HV-&{8cf&ZtT7OC%kHQ|%@V2ugo4KgxUYg|Vo_g4vY4_WH+NEe?+Bzr_#dcINZqWIJmo+ z<_kA6a>DfbML!7kGXG9lK_Ok;;i&AA@cmVn${XAhE4c?RZ;f_$ffr-_8Poxj9)FLo}MuTey>K|AN zR;^5e6{K1deV0wDasN%!)m9Mv(K#6J|^%KY- zZvC!WwNv-X@8z;8Cl7Uy=w`MIJstmQ0&yD7*P828s^hP=)2#6>-NfsR=sC{*3lwUD z@Or_7TJ#%%VVTqz%wUEI+{`~=nw)RM6ieOC{Bg|Izggzlf4IANc=WgVJnuO;BcA$8 z^2u7!Cg2#MYPnRuL`%Q;I$KF1-`;- z!N9zD6ik}=?v!-bapYu>yS7I8Ze1j)nS26uR@L#eNgGKS4gh@An*O&ke#n>SloiH| zN0|7zqM93X2fg6uSLmsEP#A26r^G|i~{Ijx_W`oX9gj#Utujp6%&X+ZT ziI~;-SG8KyoC&M-P7gLb&i}a;EYjtP#NGIlr}e+O0JPV?k+)iO#)X*@hV^6rQGGT{ z+b4Z_^4WZXY30X+ByU7zqqB8KUGc9v)&U*qE|!t#(m-?$;U{TS(JKLGh8?C6=fMlR zY8r8=H9f17yQRLc1TJwWik2sUe^k$&MCiAQU`K4h8B0TU{o)cKneCrDP5OjmvC(i$ z;$yjQ*s8Xj)A15WKu-KwG|??DG&i-_%WCHGK3NwSV~U`!*%<&^Y$B};DSh0DAH2xU z3Basm`?Mn;5jbm`Qke)?pN-2xC6)h(OTJm>ELWhfL0*Er1$qkf73eF_SD>#!UV^oG zbqGyww-=8}Y@Px^|IHnM*lTV?;-zKvmdwdd1HyG77KLzktya!rSMFIntliU?u4eLa zYO${~pJCgdsp)8scj}02HXKDi;mQ=-igT|;tWV)&uzLCpY7FSgS9bx4X9GMpeA4yG z=|Q6Xy(@~RTckLn=K%MrvD;jR%D8~~HW!Tec8Lmv_ULh}39sS%){p#vJ zqCrG^ETEFjlH{U1Of?0CB11+c42RdryK{#$5pMZM{=VrXD~#5^(6zP?>!44&VfO2jTm^?r0%^SOo>%&O%ONXoY>%|UMU60 zlgE}~oPDDCBQbu^6vOfGvdUSZv<((@SU5cgm;v>;>t_x$mvmn=EQidZ;wGQDraq;C zlV7B&B!kFF@>ER=qq!bl6#-DaO>1+Pu{ z_~d$~QeCtLh2kS-srSj1H6>=2E{z|DrSy8+&plk>hk4nlO$)Wr?=)biv?~T~JoMun z@g1#QV!W``=zsFG3wll@Ns z3C4*ZDJ{AVbyfCcNy=+K(>H`3b=|SfzXBJ2`UlVKWtRhC)iUT>5-Q#lm z4B4?o8=##8Bkfx7{l<<}u3`f!f3}!5+!K{M7~wZr{}-3VlWIPb*J6@#9Ev{>^>}3~ z&GzVdk!MxvS^OnpqQ)NI@+p!M#eR% zD&#g8w$VJ8rqC>N0YYtzU_`2RwcO``XQ62_^JU)Qf!8`E%(oZy2CJUMwFIL(TtrxUjwf!C#!>I}4$5SbKWJEKcCM z1iK)g)Tj$s1*z+Q<1KI^C4dFShgkh-QUb&$x=F&nW$qpPuM1Ir!=vnQ!Jn^%pMa@D zjBMGtl#n_J?FrE@i{s0&WF0X1)=K3ZCYWeuz`V*92n$z7{S;CN?s6scIHhOtXI$Ea z)Z=zwIm8cR!SQO#5+&h*mU;MG1f0Rq@W-@hs_i^a3UpIHssD6AhUtx>td*31)5iD0NwK$^ZCz-#3@?oJtOy32h0oOXEg| z2ta~6n*`$D))jvy)8nDk7?gJ3fH}vjJC3FrzmV)YjzAakwDRKjQbQ0|1Kla*^mgCu zzJcm5S^NOrbd3u60h!6ICa{z^VP8tl8(FMHy3Hff8ndmU{mFx$>ck`@jj}u7FF`~T z7Oc$#ST?(P_oB|`nwAGyqn$_W`|V*qZnTC0V{>fpLQz5i%Pq-%BXN3#=thP4TSQwZ z7b=d>&DkJhL%^zhSM6Zl5~*{-go6X7sV8DA36?ovh*esP)t^g(mGW{9va{g-O`Zr_YUMhyx^i#|)@{j5X) zS22krrDY$4$#z^r*578s&&d_(K4q!)eVafUeX0a7aZ%@w+B>AUCS9zVKqr*LaF{c; z)hrVEP5I>!^u$(e;XeCy< z1;3e!%%~_fl?y`mhY*36xuc8eqIYK`F3_QKnMrsV^hyj`1(ms$$wly0^JM<|FZK7+ zoztjryc~gg>giKSrICAR?u)Lo*S`Y09*4osD|n4*%+IL82jM@s0JL+#jwp|6L;6xm zA{M~@>z)UaA7Qtc)|6_a@ix)E#yTBfg~BEj5!srL}G*vj$_8t|Lc-=ya|~& z)3FN@EP!J${^s-6V;P*1B&E3kN<$V5RKJgHoUzd6PE&m^`9r2VonMw|gAb}p-Uu8u zhw?LVfWl^eO3j5_p^l?c=$|;wX;#kB|9Y-S-W=+Gh2FMJRm44&3XEH>)dm0V?o45m zD9%)$s+|`uqOJFrk=BgAEu?roVJG7gpfnCf&tkB@_;GN5E5H8b$v^7NvNI5W4S)B? z#>jFohN82L%xWHfoR<-XR#PI-?qc!@Q@i~PtQe0F=|AEl;s4rKXU|`_KuKk0jXUK` zy^+aLTR#6K;Ef<6;<;zdi%a4)RRz+}BbM6?T<*_nu54K>h6_GT9VR%rQ82p#u~q9? zu=~0B3vl>!FePT}@xzK_WS|il?x7o=P~}*bIsEvJNkl<$?O(U)iu|M*9YsV*iTlNs zYB#uhG2Nd--~iKu4_PaMUQwQepqQ2U7_Tkw-@0i`SUwV*kN5rx3IwbLmN$~f$pAb4 zAO-rCg#r|aT~#t~1@e!X5L!uZhMf|1WHO2gkdmcX&-8!p@*maPc8Z$sr5xrzc8PnE zzR!%-6EtS}XS2!i8b;mv|F}3e3yh@cu*XmL&u;7nq`GFfY zYT{7il{v@|J~RX!Mv4HoOOsKPfQRT4X6ruF!x+rl7eZ!?k_*)2v!*{a&4w5HgZG!w zCqSz(;wq2!Z_i;F`%a6sGfz$Ty(}J+7(qyeB+KK zzK;vj5v*>D6JS*RVbGT;n}pSA$5nO=M>+eW(X;w09ONk7cE>ca`YgvTe0pS+u*u>g zF>gPki7vV-xZ~E7+GBb|CrEG%Tsi@eojHPy-3bq$LKQY|{e5h3#mjE=o1#(j0Y)A=|VpVe35uH1aDE)`uogb6d<1FVM-( zG{v0-+^vih3$5m&>2>liR`=^EKTXtM0e&k;POuiAW;z7iWG-u{^?&_+{x zaa@R$fn+H`iQo0tOW3po4Uad<5(j)aOxoV`VB_yB$%nm=09aV8IMHvI>H!oEMM6c>dkz18ilIYVz$HY~4J2UdP+Y1(j0 z7Y=a%^TVOOhzJURCIDB zbmuI<(xV!s)%~1B#~mzP*6tN(`3s@TA(>m7CdIX#n@gz)-E92f+rRL}d&405LfG+DE5TCW&exw&1udHikHEO|LH#RP$O7A2t$6aC0*Y-k=Rb zO8zJ3jyjd-%Fse)sNG>dD!iEeQfrm#+(^A@BnWV&p7Mp8C|Z3DXd%gUXkT}cX>9hZ zrD;+9!51Z$HO>U^ttZk^&Hifq|9H-#*Dho!pELl79=q)A9f!44_6d4Mi$uJtB;t>& z1PSmL1aKetDng)rodniOp;0>|{Wk1E;eMcuOo0XY^^donq@GSPR6O}qy)!4z$=tt5)LO3% z)iPXmY0KuA3?G9dt_V^vAHH)>t+%dZGHb-r)9@nChT1YAmYn9f-nnLEgMK%?StR=ZR@LEd zm7H0Pn51$R0z|zIpJYe@Spg8_`F42vL|^>0o6`4r3ge!z!2ZCu`=-4{7;&j3ygH6Q zV2?AeZFZChB>Yj%l`8^7jS(jxRyeNi7wgw;si7-iHq!ii=XYl`Bf`wI&__htg~S_R z8B*)bIqJ0T-)a(C)6F9sFRUplt)6`T#bB69Po6sc-9IVm8ro=QT)BND0sqf0)ALN%Zpi_7Sx>6*e(g_@e#^1uWkeZ=dh~kDU#JFl zm$fh(kDG?OHYXPN+eM@a7KBZ|K?upPEjY0s$Uz@t#yZx2z6kplu zj1R-9OiXRd;GrOK2<3~xSRDBPz7xEWrXRQ+y=SD(VSy3)$xCr+*JBYEnn}}{fUZt) z8x`ag+E|R?Ey7fDo8|y^b&-5@wqyVH%~px}_N$pleh{00mK}1Zhcy!)c~(H9A;&-z z;(Q5+@}c=3)KpJ{>);wjV*j6**S0jV$1pmic<(DSC?|_GGDo8&fvHyOU?~&*l0cqtbTb_(aB8s`1pUi#bYmYJ+8mE)0mFjMwp1;E^_HREj8Zl z76G?r@<8y1So8QFgTaK(baX|NG1}wV+0l^=N){N2#*A_GKoj%}Cnr#}a$+z-9G_(hRu zFgMDBYUNUg##sx~!|wLw4#MR~&IIwu6DAy*IeX||=5Bkx|HkJ%K!!EO^(UA-+V|tG zx;o9BBe*{awtKUsNxWH`=33wC4iL)mt02RpquC8AP)w4R4yQ7}eY4{Dom#uBT(ii+ z!#OHE7pDekX%`MzRRPS5nYL^FAFV^XF=Ju1e|F_Qe7DM*v1AuOxN4E7WLjq)N1&cdS4w@9Ky)0Tu=L)X~@)L0PbZ;hdA z-?6Ok=^#E-TL-5)w|)S+fO$}L5DFhLjd8s}^f*iq;9}F24X@x}OGGH1*8Q3Z%RlHK ztAzEi1x22@OFtBv-T9NOY6XazviHH&l3fV;v4lnzwU1I0&p0l!l!58xGD)LwE+-x3 zz#ddNVIxfB;{stq%$>aV^={5!CV@HpNZ;+fPz+r;ias@c#Pc*N?U~+)9;<2r`>ZPd z=qh)QJ!mk;Z+EzkWN6AvjIbqIk@+2ts{rGAK2DrVy{ZklPimKWuI;r-WV^V z-qV2cWSIFOQ5RGM)9qTQEmI69FD6pF6-^eIL72%Rav`dVg>q1{eecur$*x?QUB*(r0hgv}4g3*2}eHmAprC6YS5y_U$R_+ccL*nhCLIyyUQ2xji1CsI-Qnb?A@woMgPLkqRo_3 zu(1b+R!2Pfl_Yj%kR7Xa=Xsmu47(9?@rFrK(bd+lh025l<{&rMCuLZkxcsGRPUHwy zIg$x&!_s76DtJ{a(fI2YBACEEA@85pUoTs9G%I*{>B?FY1_W~FF}ZU8U-apN`I`M7 z22YV#cUN7N@sGFwHDg`RmX~r?QVrcN z#`ChdJLAG4agwzwDpbOGB+xoQ?uLjRtoy@|*oV110a|PZS$M!JNu}AKK+Lu3`MIg7 zNQ+I`uz}ZgT=Wr;6`ckpa_6rZz2Sr+RMl^z7W#jP;BsLc==zwm{-rJZUGIvX=;5pC6E)X!HU~yfAW1$>bAWzUf!>Mi4KcIDL?_%}{k31EeSNBXN#k=s?arZ*#qh&b( ze~s$z6~j1LMl0S$=iZ5vDva%yDIjV$!^;(5D9^oJKp0n=OKi+w>hz#tO^!(LWPCx_ z4I_adQCHaV4JPo)y>OdJdDm5uAvIS27eRuk0o?zM#CwKyzgea;b_xjb*# z&iSAv;7jCvgn5#+3tCBN68MKbPR=f>PBAj9K$vBkWVWEmKaOiCYR#_y$CWZ2aWl7D zFgjg+`F2&=IiL?Rx4PIqp57x^OlcGrI94j|*cbITU#>Nubx#KuXnNUrY0docz|3|> zL{S;RwmS4Db6YYzc zZppa(_3fu|R^l*=1v&)kWRel&-nJS1XwBr5#R^t{-^IZ-Nz&RwmL^RI$~~h@=sh#s z4AG1otu6cTFvAnr!cl2XVC86igmvQ6=IjA41ZPy?c~-)Gc$V@!6TcIL!pEq25`~>Z z)}OfFjmKfH!!FZw)`|;}XJv#~XWgciATy2!;yE*1dDMK%LIWL~P>4Fj6%N|&?MiVe zA7e={k5t*m-i6$4}JFlZNT~#|CmquDA2r{EP3V?D)mAU=ZD)6hd=- zyL21?_A&eQZhsaA8UQuS&%nuxr8T%oSd%XllyJ6D5A_H#&)RnUT_khRc~tzoVJAg}6Flm{dfmMZkC zc`y7pylLSRm_85Hn7Wz-1VfI1uqMcmp8%eOwQJf$je>f}RpSgvxSofS^D@=I% zA0#I8;`JIIt4b4aiK#(@Ga#}Xh|b5BtOXkS`|w@C7R!3gOYWuBV4~QBpJ8Q<%Bt2H zLNLY+6RQfgQ)NGty(0F?yd|MxcT( z=P9t_CsE00gy-8yg+aBOB!g&)Lm1FX5Ok^*`Uyf@I0yRautzOG zV>k@}=o8d)V7O=n5fG79!?DVPNQVHnL<`tF#xAGUuV&=zct<6BmTJGt{uK%P;mdZ> zHS%Rx2Npg1D;MKz;c;{2)g#^DVNr-0<+C~Y=cJGcH<8b@<{1p(ImJHvql2# z8xG#c{;ctZSjF8`S=nB~|7tATAVXTO!cX3^I)*EI`XvskmO;4my$y{-y!Rk2uF{5#hD>OFRfb#BbIWi?5fAJ+doX7N=Yk$dl;=!&E28 zp6m|6o?$vI!RBcF^-EDgRIFTRwJUfI8UQfZZzuok{VD~b3tJC$}*19i3Veeve0ot;NC`4+} z-fY=yaTHpM+Ic&>OJUCgcOP}x*Wk=)R5nH1w_@GuqW)&@?MZtWntJO9G5zt{7R=vg zHTAk$CP^E;(N|&O|AE4K0Ou;8yd!6=I(eCxx|=&E`(|8+L*RS3S73H~Gs8bBL|&BN zHIW2xNEteOkO0b*1oNOB+p4Mx?FGb5_p$-HT-c{yySM-Ac3Qix)6Ihf3`5xOne`A2 zbICixLM%X}pN+dME6mscsEe9d1dMEUz_m5!Kr&w(~wS%NO|2cq<5&LM&|}@`G7i z5`H6T))UMAyPF`36p!p*>cO8t&v+wJ`H&+6N%wz|Rw}m|y-(-gBc&BW3@QK|UxGz^ zH-co_X@mj0d7~h?aU`IQQ}ZWkS*2ALIIp3@>nuu#Ncc`#Ab!e`O8p17%8lp z$mmVAhhp`>jl7y@F@xAfc+#iT4)XRJPbKOk4Sr>%t5}r?+3UU#^i-p<$|v!^nERYT zzruK~Y-|Mk2oQeV|APFxr}hXQ^RbARaK*&ipH+++z47io2ed3s+o~p~`}zC9M&3NF zcQFe@6p0!k8gArM!f_+p;Fz5$tyVQCZUST$BsNcf7r2SxHu+IIlSPPobxKIhl5x`8 zficAIPsPm*(_u*Lu;kD5+f^U&7yH`eh6QI(R0JHP zFNmXk=~O88%S1z?^RAxd=L?Ch4*6Rsr)x4+9KklZFTKu>8J#^9fv9=G4u?kf1&%&r zZkEeoo5!bo@AHP9Q_fxNK+sm{E0kN8wAP&Qg8najl^5882CSG6bJ(mpLo|oACnMZj zq!ZyNn8nl;z@B~l0!zTltTeiyKi{A&fr6J_QRsl>0C{*qiuT%|B>H($NXCq3NLmSr zCo56~7*)-vm_zM30HnWb5YYJvl1)MHvi{PN0$3ccmBzI8u{X8JiH;`D=zQP6e8*n=D zHPi0w0&ctGJdNHhGM9&x=}kEQ&|rKsO19*Y*{GmB?*#u=_n|M#Yq_ibq`n-k8`Wvk z#`au;?!z1q#>YR`+S@V<0ZxceMie@ELY=S}bDf-Lj8Gn@ld&E3cuZON*!x-pogMr? zb!d}*P$<}t9t9WYiK&@jZg>R$AIhqR32mj4LDY|xdPXwblX{C?LJR}N3Q|Kwfx9!! zte2=~hb5YHf??fT9j5?DK)1ghEHfFmP!^jG+#wLM@vj|HCp&Q5y960MS;mNmD^YJ0 zstjV*Y%qpbd!06~Dv2Ly8j8`Kv)&Z^h`JNt0_}U>76SZX_*B%it0~FB&bTlnmj!AZ zx_eLQQolGo7n$by!v|_Lc*R4`@97m)#+63cQS}zW|T7BEN3YRupDfn z5=?6YK8HBLc)a|EmoSKmMeE3b|bFOPx;^~w(_f~g!lxPnHZooIY& zXRl6vmWvl&_Vp&n*$6UxnQ~h7r0cNuu?_N&KtUel`6qkWA4HXuO3XCVBywdty7w{%w+V;7!$oCe=Gz^>AW zEfb>s(V*i~(_`~ZCk}{UJYJv_vxKtOMHm18Jn#a+000004F!`DFZvfliL;D|DzvE; zQuJ-toj0sXftt(E3Rc38WUU5s`p`D2UB`^wGr9EZdSI2_hQHz7*71oQgu`cvLk4^7DI9b|@8?L&eZw&-h+WbZ zz8XbVv162*7PeX+1yRhQf$fp}q9fv8*7=bgL1GKKExzSKDDEwv0-%5@+HXj>7M9G> zsm9-23D^I}JIaZ5R{pW7OP&~p-AwX|xT4SmuP6|dVS1mEH^dQq;h{y5EOp}+WE#tS8$NT7hOwk5{~|VCQxlr$<%G>T0o()7GA`M8S_}iSvCa?GY$5l9 z22w!cuDNS8ZYt*Q&>T#ju4Lawz0mt}mBsoA7mdOvKhymY)kXyBcFb=TPyC=qEO#ic zHmx7bI%wKN$Uyo2Vl63F!^;2@-3N2Abo_w!S~#{}+*{1AyIb&9JO zn&76(aqWaAXO_{e-CneIUd~t_u>4~ODf{R71nMpE6_-6UdGTv(={2BRjY3>4`nI7z zbdY6vA%R+K9(h{5yyVrosY)j4Pi@wJRcf+qvzx_ZLVI5 zDrgP!WtkKzxQCqrLw#FPfxc5ES!pshKoMVYGswaCx;3JZw<$bY(%mT(>PA7Wi9*Pw zo?LJH4M{h6JLW>SKE{#1o{H>e(BA5!N(`uFEMzl@M?G8@dEcN>=Vg)6hPHUOfWs_p zAGemONrP9HP;#;q$~NDR|NNw;9x=ml<^Xp$)+(RAeb?G7K#*u}8~FD9P2y>j#)9Ha z^|x_lUpp7t$x05qT2iED}o~Lt~cZ zYms^5w>4$^QQDdHqQVg!o9^P2k-16wngKQ!@hk1Gk1;i%Y$X1rNUfg));#;G9p_1o ztvgQpJ{}JT41;M0yj>sRLOxz#z5ajH(PPdhyB7@CL4Cki4~LrxwOA(urip55#f*1*{IBLowqDs`^u}YO$01lI z-R|~r7Ij+~|Jv%LVEGstPQj}7dr;qObG$Uv zJEG(wS|*xe``J0@5xgY-A(*l2xtpP7 zR*OwG*d(B$o11;6*yD91PBueXINr$;EwSnLtK_@m`Py+*&0w68(W=tE_8>uNN%wI? zR*1AbJ0~&L&G;hZdfb=)0DSsybf&BQw9~!!rQj|Gd;`*0)e!ZSa>nx6>M|d*#Nue+ zv?F=>$9|NhaXpWd`HZp}=9B)E+{I@|6RHEprm2$y{KKJs#cc)b0J*7c(88gL^D8-~ zBAmtP`}%w_=){oTRbp)RYMhToU#X1QgRATAy}0E(dJ1C-eF^tm5*3R@w^pR@()wm= zRMezV8s&P`R~|Now_Cg@={N+g0c&AS0-N>^#5*sVhY`LvDsOB!n)X8*_Br5L@%CI$ zu)*<)56GJ(rUhqW*g5W$T_Z8N_^pwUAWP#SWqq%`MYxBdWF<7R=`2-|5;zzRm%Oxy zeqHSe<|cFl@cZQI!eZn=pg!p$^|YQVvGX+)-dZ)ia4DN^^i7WWT^bi*G;*OW;XCKe zF@0sAvFW|3;D1&R%^WCt>R@ZxlK$i#R^~F|@#aD!3_Fmu7b_XCx8P$VY$kM&>3d=Q zG?@$39GVK~Bf%k|Dgq|)f@+YfO(+@pR8t(4@T{>z%))q@Crm6s7ZWxm4G4Eb%4FAd zgD1&`&H|uqM^11NuYy|WAY#UOQMEZ}m?+?Z3O;uBxM&=Ma1B`S7vH$pq3r^Y3$Eix z8YS^E9C79Mx6+I&_8KNi#%g4x!!c@2(FRLdM00g)=pavj!=Y@qV~5CDQ*=2SEA6-` zcNJr|GBaukfq5I^k+$n;Tre)}Y4`p_ho%0TBN-|GD7eYD>gf|cnlS%RpEDqSbx9Q| z8^;R#guux*a3=i#%*VCktS+jK?ay7H)M0OLMS(Xx)n0siZx=pxL+ix>LbDAzn4{8J zOak{KnsbHI{bPH%GXKSRO&%S9oxWnbyr^heWFoL#z7+dPBl=H3v*=4yS8AtBfRHnf z(be`+EXA}h)Dj}}5a5a2SfI;Dkv*oKrrc;S7I=1t7d9gR*gDt1fH-mpsLklG%e;c3MWZ*IhE;@_eIsLA2iOW&#L_DT5q?-7% z(V~tpgb8kaj{&!A;2`*cILuMFX9m)fEc*P4va!H;xi-7!i1<`MJ>FQ>cn%Al6|E0y zzPCV2xCSStpwUF8pZy8spG_!-?5!BHXq@jPemy35=%&#*^JO3F^W$iNIu=QaZ59J~ zVnJ-%T`Zf37ODk00576q2+h82GM$cI^TCy3sjdBZImZN)=5Q0AfFdZQNy7B$RM*12#D3qXK6 zm8(m~mRaN+J0XXQ>zr|5X4@J0y0+?3e z07=EukPbKAPxQm%^&}DHwaLBrV;JUlK(8;MFFL&V=wVMYPU0*bBtVS6`uH*jziNuT zna;mtsiLP<-fG2c`;j1<2sTj1#4wX0r0Dk%l$c}i@}`ll@Y|_a~Zb#9R8cN=6tg>`d8Y;**o|aE(7y&Nb~40(0cY z4o-pfb$PqzaTElxuh=YL@LqnT>)R%C)lbH}w{1-H_cAf!3|9wR>Vift`&D4R{mMMy!>^Ab%!-{N z*5aVYJbq1u)gI7P!HTHM^oE-@Rz#g2gC;_A!EYi*Z3_O?wt!q(`&Lkb%EVyxSklqGuYhNtd=Yjd-!H-%u{4 zGCAe=q}@LdXRrslJ}{3S#+R{C!oxQ#&VexjA=X_c}5%i0?#fMNV-_kPcbc$#t+*&g!Vg8pQHF65onOnW;7 z$gcud$>rf@hq3awRck&qdyk{4&V18*_2K%JQX_VB=J&F&sltU5uyWRJBc(q*ZWtI* z|A+Y4kDGKa)$&My!t#xPwlHq?T#4hvR$5gy43+lGw+)<=#SuWy^%>8%gw-K3_SS@T8 z8Cmb4I?;Ns6Dx5Bt9-pfGnvP>ehw}4?DJ6KeDHT71L6S1l#X&XS!6qe=PMa&c8I|` zs;aH<`HZUaM`CQ2@!b{f(rw!B()}1@!tY@IDlmE=rCq`306r|BKCFrM+@hmh1C zc{WPeBl;%+?q}B`zYqU`04e zPZkdWzfRZ(w@h?uU=p|K0|7A7mko17ELqOwc6ILbzB_ZGLh<(_|4VBk64g_;x6Txk z`r{j^a)p*ZuOJGsMRFvhRj_v*8+eObV8CFU+lF%(RG%LyZo_&TJ-Me{8G!JlnWUa9 zU=MnVIr?#;q1M2#g9Nmw+*hJ8fcqo9sk~5%%s%iI0Me|}1xY;#q$#JbTVhZ(5b+i| z-RPg29`YL10^ym5IR%%gMzi=uSn+pHtah9vwC`;EpVM*uO+$N-<=RE#dq0LspJ@tC zA;Wj6vq#nZ+7ctzPS9VFgLswJMudqHN!4GRr1W3GK{c8RmjEtyl5D=*;Gf-!Ur55* zXqB5V>UIH*kODW=8>4Nitm}n-v_x354$e3NDtN3qKJRM%#c4QcZqoi~yLzlqr?LWl(^!?+);@G0jq!=c-V3n2pVJRi<}F z{2KOUHZ|Nwk5M*zyh+ZnX-!In#04_ZBdqGx_IEjVAmW_DY8Axga(xpOIyZ zFE3XsI1M=Y9^y$1+?*0x9qVeZ6y!hL`^y5xb=(iy)dKt2PW0S&!TcXaJAv(H0%1Nj zGgRgIcFp-*_T~UA6Xz_Ilri#iqu1L?kzt%BeB}3syZtO^F#z@WqMm4qeG@>k(a0G( z!t^!MQ|V~GW;ld1W|99nIlwSo+&BU8QG?QbBb$SWbL3UBcGkVK4%`S?_fO{l)^&*X za+}#Rvz;cfTBOpUbP;z+0t^$>AG+D-j>JGWle$K}-v(ZG@Fysp!rPze)feueql1oC z@c$JNeiTTJOsb9FNq9!DP-eK5dM6xyHYTF<&rxUBv1J11wMEmec{5K|*RloVRPpJL zlF{M_;xCwGFW$UM^D=DSCPmQCq*Qo3tLq$QgPpD*!~TgZar~RLaSea1Q6^dbPWFZx z+JLq-5+#G;Q8#e4xkI4Tl;X_|O!9euvxP`nTUSRr7=7n4?e``^W4 z%ybe5n)7G4Ac@QuEVi&rsG*;AESYbx#G~*K9!Ps$kZH=AvSS1Kw`OG0N@FuH)3-%G zk(W&Xov~a@otSyokT^%x4*NxTK&)R|3lKtJcv#@$#8PLt-q}!Qm)Dt_xM~l=pBe-} z6(0~LzR;$DXSqKaOw-xJWkndoI+6AIHEyfkH+FqYM$JiZ&Mv`kD-b4}&P3kZdfEn$ z_R@gs_J2t9-nqU~;h^-VdK3Icok3J&2LpK>t4?d;*0E@}9%B+X@)!=t@tlSAnb=@i z?^oMVg7^~@gVxUS_cUNQdhwU^BpM#_;q@En2xk#IhXP^pv5UVKzE;P_E{t9>+XRFf zAjVf2^C+CDg%E~EY-i7xJ+?O843n3O@7#kNXifq{v=T=PMhg2Hi@25Mp|=Ed-;j|w zKVtf;^NM5XaWc4qLRzwL z`ngTjCY%Hkt|)k`QUEo5=H{NQKz5D6cQpy4{=Upbj~yHZXsv7DiONJkDc<7Qji0rY z*8naiZHjna_>8keOWIDV#NGPVH;L#k57ya^M9mMQIFYctH@>CAw2P4)A;H_r-ZGgW zelXPSmxXD+VNzp!_wzk8M@XifRm+G{A2OhtuKPg3B7=W$2y2ZH9tJ!6+X1dGP2rhP zjmmG}1S8r{*>#0<|4m9(?WE~} zhIWduZMLc*L$1x@Y!Y>I`)L~huOg;DI}k37*Rue-oy>Tj=+idr>+5bHK|e_z%6IzGpU~Gx7NZs&Q41 zFT}Zmaz>Zy`|4)VK2Z+%wUBw|fUz=gWn;n=4t`9Ds=e1-D%=ux_U0 z<$0BN=QR@TIcC-5O2(+@fIw5Na^b%9lU#8?xCA-?!#wEX`{2{K+oO4-2U5mg(H85G zOmyoYtr>ctZ&e~IaCYT1lNW|QPRq~L$ltwRolxy*T~l~zB5z3ipw5(o*FH+*_f2v@!1HX%vQB(h(oNX;hd}&4y z3zY8NcT+;0%zIN&xbyNb>JzfX30>CX?wMFr!yKvn?kO}b1Ray}pWni5 zWg8aa79_oFy7AzLU|?O;TGWa=?O+Ip2)H2|z`?82G_KqdM78hZY}#}SuoW%4X9L8w z(42T~lBWB`@yYD@N%gUnO;}b<-)CY&cze1iEX*JNi)45|C{uzRb|IfH3*-FbhmQld z-omP$RedUh8eO{2p|{sK^6QU$)K1`R&96QzYQ9^|Idv%@RXP*div3UdL#8)OjqjY> zBleXm!T-dYn+C)lpVtFHR#4u)NKHj65y-$Ql?hN6mkO(KB>#B9o`(NY*=oE4(S~!38b1O+ur_RB2lFv-8Eq z(`AS8UL|L*${K54(1S4tMx<9uE?&*Q^ zO&@$*j8u1SPDtZZcHSJ)bFde^KQ`AHrv*$@?dnY* z3;vV&QFFW7S22WuBj`lRzBb}uV-2wN(t9@()i!F#_oyYO|L%e4pA-xyo*cxEt)}Wjpr!x$ zJzsv?ej(v@)on$(lqFmxH5b$m)~;=!wfY>fjULSf4P7Xa+bvTD?U4yjiv#$>nQ*j`OXf=kUInWssO@`Zr8l>`w- z96bVG?v483y$RThhdcz)g0qn-xMRz8GY#goeV$0k<81NBc!{k^JIV!fFT&?wB5nC3 z@>p!b?{pi)n9Ai|D3v5m-tpFU-oQqU$3J1HVPT^LYo^374(!&@?5LPty$en}#!)IG z3$yI+tjnQ3FmdMek^xqZ0gjD~{;2~m^=?{z#OODF8$It>fZo{aZOBCm>+V3*dA(bu z+AaUM<3@&hmF^eU=_Q&)Cd(~R3ZG$-9=+4Aq4c&q#XPx2xmupj8vUa;gw-5@T?`~X z4#QFxEv<#snXZj*^w)>hLL;ic>o-nuX&ntVsVwC9j_PR)>| zH)5U@N>Q|WL8}NnQWq?%FA*_FNOE0y3E!27PsTR?pHw&8JU2IhS{2sOCXD5kwqmaC z3-bpZNK(0S_=_Fd?4!-PMY-TSX$Gu(-!4rwxEayMYF7A$9jhoX#WC9OL3r} zfgj`jXhE$>le7;I0mNO$GX=^RO<~4SdeA5*>G2PDLOD5QB3Wbu zhvQJ%{~6H5m$wl#RbI#ovW(qQ=8r&N@3H%ilT~G{4%|OW-xx1$Sqk zacyQDE*3DqLz$i$y&;Jl07GqI#0SGOBYmIVZU`XW<=tF&C>{RhPW=~c$ z>!g2HJ=L}LZ~#qcIV;5|X21$gTgYHxn*fwIQKSqLm(P?9QO{!H4S$L58=obu<9&my z4_%hNQy|{U1qZ6s6gC(Kd#VIs5+os(1`h8pLr|vOX34X-0GK4bECmdI3%o7bUw(J@ z%7p204p4~qidN5Z*25lG$kD?~U63~M1ycVv#2_p-Ai5-LA(Gl7uV+l*wo+_iM9Xx* zm4Yfe*N2?c;3E-rTS8nHob7zKd@A?HIoU?Mj;3gV_0`w9v41MdK3b@WLucjbMW$a z3U}V|V@p>m53T4BtoNs&-hHWsH+_-VHH!aobAu$LXq2Yk zY9e1n-a*+!vO?iuQFfd0G*>$jy%JX;qHEPbyBB;Rt&1 zI6Um!k(PCo)(OhCNu)jYNEGjgp|WOK$6t{#px$6r27VuKnCANzHvO+g zqB)oiZ+P#e$==7|QZRrb)L;<`ViM<|(3${wX2Hod=695Vo4SOZab!3*-`g%_w`Snx z6?eKLi5e6E7={JW=BbfFZP(}Vf5}1OfqH2DlO;|xYY{`KYawI`T{WbJ(o>BLKb7Xk zy94v;(mH0hZ~<)(5S>fC+v+1!h-yOREoQeu84TK=IhHHwd=wi- zc#d(?n(Xo}^$1gQr6Q}h{-dcC9qzWIksc`-cwdT^&XoKsMZQ7gN5e2xYshpkAB)#IPyIipM%{Hz05T5yIJo% z#}k%^EIfT+ec-Y>%t-TsJ*EO^rAm2)%-fO9pX-wECG2;Nu-M8o;EMi2`NQg+?6k7E zTrOo3P94DGhC8Ax6|zC))&yROPCY^N+CkN7N+5D;*9|G`gGzMrZi99_C`V++##}N( zkq?^~VA6Jg;mDW=I^k@X>)QFGi5sChARbSn~>(SjUg?hko-h%0AwGc&)(t5=y1hs_cBDnhtzT&Wn5ds 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