--- app/fall_runtime.py.before +++ app/fall_runtime.py @@ -17,6 +17,7 @@ class FallSettings: policy: FallConfig geometry: IntrusionConfig + engine: str = 'legacy' scenario_name = 'FALL' @classmethod @@ -25,21 +26,29 @@ raise ValueError('Invalid FALL settings.') names = {f.name for f in fields(FallConfig)} roi_names = {'roi', 'roi_id', 'roi_name', 'debug_roi'} - if set(value) - names - roi_names: + if set(value) - names - roi_names - {'engine','r2'}: raise ValueError('Invalid FALL settings fields.') + engine=value.get('engine','legacy') + if engine not in ('legacy','r2'):raise ValueError('Invalid FALL engine.') values = dict(DEMO_FALL_DEFAULTS, **{k: v for k, v in value.items() if k in names}) # Site default applies only to absent values; an explicit per-camera Off wins. default = os.environ.get('CONTEXTRA_FALL_SKIP_DOWN_HOLD_DEFAULT', 'true') if default not in ('true', 'false'): raise ValueError('Invalid FALL site default.') values.setdefault('test_skip_down_hold', default == 'true') + if engine=='r2': + from app.fall_r2.config import block,score_floor + if 'r2' in value:block(value['r2']) + values=dict(enabled=value.get('enabled',False),person_score=score_floor(value.get('person_score',.25),.25), + joint_score=score_floor(value.get('joint_score',.10),.10)) + elif 'r2' in value:raise ValueError('R2 settings require the AI-owned r2 engine selection.') policy = FallConfig.parse(values) roi = value.get('roi') geometry = IntrusionConfig(enabled=policy.enabled, confidence=policy.person_score, roi=((0., 0.), (1., 0.), (1., 1.), (0., 1.)) if roi is None or roi == [] else roi, roi_id=value.get('roi_id', 'primary'), roi_name=value.get('roi_name', 'Fall ROI'), debug_roi=value.get('debug_roi', True), point_x=.5, point_y=1.) - return cls(policy, geometry) + return cls(policy, geometry, engine) @property def enabled(self): @@ -66,8 +75,10 @@ if k in ('enabled', 'confidence', 'roi', 'roi_id', 'roi_name', 'debug_roi', 'point_x', 'point_y')} def settings_payload(self): - return dict(self.policy.payload(), **{k: v for k, v in self.geometry.payload().items() + value=dict(self.policy.payload(), **{k: v for k, v in self.geometry.payload().items() if k in ('roi', 'roi_id', 'roi_name', 'debug_roi')}) + if self.engine=='r2':value.update(engine='r2',r2={'schema_version':1}) + return value @dataclass(frozen=True) @@ -92,6 +103,11 @@ class FallScenario: def __init__(self, config, *, wall_ns=time.time_ns, event_id_factory=lambda: str(uuid.uuid4()), direct_down=None, direct_down_cooldown_seconds=None): + if config.engine=='r2': + from app.fall_r2.runtime import R2Runtime + self.config=config;self._events={} + self.engine=R2Runtime(config,event_id_factory,wall_ns) + return # AI-private: never add these options to Settings/Metadata/Event payloads. if direct_down is None: value = os.environ.get('CONTEXTRA_FALL_DEMO_DIRECT_DOWN', 'true') @@ -109,6 +125,7 @@ self._events = {} def evaluate(self, frame, poses, track_ids, *, gap_before=False): + if self.config.engine=='r2':return self.engine.evaluate(frame,poses,track_ids,gap_before=gap_before) points = tuple(((p.bbox_xyxy[0]+p.bbox_xyxy[2])/2, p.bbox_xyxy[3]) for p in poses) selected = tuple(i for i, point in enumerate(points) if inside(point, self.config.roi)) result = self.engine.evaluate(frame, tuple(poses[i] for i in selected), @@ -138,6 +155,9 @@ self.engine.clear() self._events.clear() + def notification_status(self): + return self.engine.notification_status() if self.config.engine=='r2' else None + def project_fall(spec, scenarios): request = next(s for s in scenarios if s['scenarioCode'] == 'FALL') --- app/scenario_parameters.py.before +++ app/scenario_parameters.py @@ -122,6 +122,11 @@ validation; in both cases the original saved/default config is preserved. """ code = request['scenarioCode'] + if code == 'FALL': + from app.fall_r2.web import apply_parameters + handled = apply_parameters(spec, request) + if handled is not None: + return handled if code == 'FIGHT': from app.fight_r5.web import apply_parameters handled = apply_parameters(spec, request) --- app/secure_control.py.before +++ app/secure_control.py @@ -28,8 +28,9 @@ def command_error(error): import re from app.fight_r5.config import R5ConfigError + from app.fall_r2.config import R2ConfigError value = str(error) - if isinstance(error, (ControlError, R5ConfigError)) and re.fullmatch('[A-Z][A-Z0-9_]{0,63}', value): + if isinstance(error, (ControlError, R5ConfigError, R2ConfigError)) and re.fullmatch('[A-Z][A-Z0-9_]{0,63}', value): return value return 'RUNTIME_COMMAND_REJECTED' --- app/stream_settings.py.before +++ app/stream_settings.py @@ -128,7 +128,9 @@ if type(value) is not dict: raise ValueError('Invalid FALL settings patch.') current = FallSettings.parse(spec.get('fall') or {}).settings_payload() - configured = FallSettings.parse(dict(current, **value)).settings_payload() + merged=dict(current, **value) + if value.get('engine')=='legacy':merged.pop('r2',None) + configured = FallSettings.parse(merged).settings_payload() if configured != current: candidate[field] = configured continue --- app/det_result_path.py.before +++ app/det_result_path.py @@ -326,6 +326,7 @@ visual=self.visual.status() if self.visual else None, artifacts=self.artifacts.status() if self.artifacts else None, fight_notification=self.fight.notification_status() if self.fight else None, + fall_notification=self.fall.notification_status() if self.fall else None, sample_error=self._samples.error if self._samples else None, performance=self.ingress.metrics.snapshot() if hasattr(self.ingress, 'metrics') else None, fire_metrics=dict(self.fire.metrics) if self.fire is not None else None, --- app/multi_provider_results.py.before +++ app/multi_provider_results.py @@ -799,6 +799,7 @@ for key, stages in self._timings.items()}, artifacts=self.artifacts.status() if self.artifacts else None, fight_notification=self.fight.notification_status() if self.fight else None, + fall_notification=self.fall.notification_status() if self.fall else None, visual=self.visual.status() if self.visual else None, performance=self.ingress.metrics.snapshot(), collector_frames=self._collector_frame_count(),