# FIGHT R5 Web 설정 계약 v1

작성: 2026-10-31 KST. 갱신: 2026-11-01 KST. 상태: 아이서티 R5 기본 적용 완료, 사용자 영상 표시 문제 조사 보류.
Owner: `app/fight_r5/config.py`, `web.py`, `runtime.py` 및 `app/fight_runtime.py`.
근거: [결정·호환 경계](../profiles/ltb1021/topics/runtime-lifecycle/decision/20261031T103126+0900-decision-fight-r5-runtime-contract.md), [검증·배포/복귀 준비](../profiles/ltb1021/topics/runtime-lifecycle/run/20261031T103126+0900-run-fight-r5-runtime-integration.md).

2026-11-01: 아이서티 기존28채널과 신규 Web 등록 template에 R5를 반영했다. 다른 설치본의 공용 fallback은 legacy로 유지한다. [실제 반영·복귀 기록](../profiles/ltb1021/topics/runtime-lifecycle/run/20261101T102749+0900-run-icerti-r5-deployment.md)과 [Web 무패치 재사용 절차](../deployment/SCENARIO_DEMO_COMPATIBILITY.ko.md)를 따른다. 사용자가 영상 표시 문제를 보고했으나 별도 해결을 요청했으므로 Web 수용 완료는 아니다.

## 적용 경계

- 기존 Start의 camera/인증/6개 Scenario/ROI 구조와 Redis key·schemaVersion은 변경하지 않는다.
- AI 소유 설정 `fight.engine="r5"`인 채널에서만 이 계약을 적용한다. 생략은 기존 `legacy` 엔진이다.
- Web의 새 설정 블록은 설정 버전이지 엔진 전환 명령이 아니다. legacy 채널에 새 블록을 보내면 `FIGHT_R5_NOT_ENABLED`로 변경을 거부한다.
- 현장 확인 기준은 기존 `parameters` 지원 Start다. 다중 ROI 신규 계약으로 자동 전환하지 않는다. 현재 새 블록은 legacy Start 경로 대상이며 multi_roi 요청에서 사용하지 않는다.
- 기존 Web은 새 블록 없이 사용할 수 있도록 구현했다. 소스 반영 자체가 자동으로 엔진을 전환하는 것은 아니며, 아이서티는 별도 승인된 저장 설정/template 전환을 수행했다. 실제 표시 문제의 원인은 아직 조사하지 않았다.

## Web 요청

다음은 전체 Start의 `scenarios` 안에 들어갈 FIGHT 항목만 발췌한 예다. 나머지5개 Scenario와 기존 camera/입력/시간 정보는 그대로 보내야 한다.

```json
{
  "scenarioCode": "FIGHT",
  "enabled": true,
  "rois": [],
  "parameters": {
    "person_confidence": 0.25,
    "pose_confidence": 0.10,
    "cooldown_seconds": 10,
    "fight_r5": {
      "schema_version": 1,
      "window_seconds": 1.0,
      "min_samples": 2,
      "contact_min_samples": 3
    }
  }
}
```

`rois: []`는 기존 전체화면 의미다. ROI 선택·이름·On/Off는 기존 Start 계약을 따른다.

### 공통 설정

| 필드 | 유효 요청 | R5 적용 | 의미 |
| --- | --- | --- | --- |
| person_confidence | 유한 숫자0~1 | 최소0.25로 보정 | 사람 입력 하한. 싸움 확률 아님 |
| pose_confidence | 유한 숫자0~1 | 최소0.10으로 보정 | 유효 관절 하한. 최소 관절 개수 아님 |
| cooldown_seconds | 유한 숫자0~300 | 기존 공통 Event admission에 적용 | 알람 허용 최소 간격. 현재 판정·빨간 표시를 끄는 값 아님 |

- 예: 사람0.15/관절0.05 요청은0.25/0.10이 된다.0.4/0.2 요청은 그대로다.
- 생략하면 저장값을 유지한다. 최초는 R5 score 하한과 기존 공통 cooldown 기본값을 사용한다. 소스 기본 cooldown은10초지만 서버 환경 설정이 우선할 수 있어 읽어 확인해야 한다.
- 새 `fight_r5` 블록이 없는 구형 요청의 null/문자/범위 밖 공통값은 무시하고 저장값을 유지한다. 보정 성공으로 표현하지 않는다.
- 구형 optional `parameters` 자체가 null/문자/배열인 경우도 기존 fail-soft 원칙대로 무시한다. 명시된 `fight_r5` 블록 내부 오류와는 다르다.
- 새 블록을 명시한 요청에서 공통값이 잘못되면 변경 전체를 거부한다. NaN/Infinity는 기존 JSON 검증에서 거부한다. bool은 숫자가 아니다.
- 이 하한은 FIGHT에만 적용한다. 다른 시나리오·공유 Pose 후보·Tracking 설정을 덮어쓰지 않는다. 기존 raw keypoints와 별도의 공유 Pose 표시 설정도 임의 변경하지 않는다.

### R5 전용 블록

| 필드 | 자료형·범위 | 기본값 | 의미 |
| --- | --- | ---: | --- |
| schema_version | JSON 정수1, 블록이 있으면 필수 | 1 | 설정 계약 버전 |
| window_seconds | 유한 숫자0.3~3.0 | 1.0 | 같은 대상·관계의 최근 실제 동작 관측창 |
| min_samples | JSON 정수1~16 | 2 | 일반 규칙의 유효 관측 수. 동작 종류/펀치 횟수 아님 |
| contact_min_samples | JSON 정수1~16 | 3 | 근접 계열 규칙의 추가 유효 관측 수 |

- 두 관측 수는 각각 `floor(window_seconds * 10) + 1` 이하여야 한다.10FPS 관측을 전제로 한 입력 검증이며 실제 누락 상황에서 해당 수를 반드시 확보한다는 뜻은 아니다.
- 관측 수만 채워서는 발동하지 않는다. 현재의 방향·거리·동작·억제 조건도 통과해야 한다.
- 블록의 일부 필드만 보내면 저장된 R5 설정 위에 덮어쓴다. 블록이 없으면 전체 유지한다.
- 초기화는 `"fight_r5": {"schema_version": 1, "reset_to_defaults": true}`다. 다른 조절 필드와 함께 보내지 않는다. 사람/관절 score와 cooldown은 초기화 대상이 아니다.
- null/배열/알 수 없는 필드/미지원 버전/잘못된 조합은 오류다. 잘못된 새 설정을 무시하고 성공한 것처럼 실행하지 않는다.
- R5 억제 거리0.12·속도1.0, 공간 진입/해제·최대1초 후보 유지, 손/발 규칙 상수·seed·표시1초는 이번 공개 조작 항목이 아니다.
- 기본값의 샘플 검토 결과만 확보했다. 허용 범위 안의 새 값도 정확도·오탐 억제 성능이 보장되는 것은 아니다.

### 구형 설정

R5 모드에서 `pair_distance_ratio`, `pair_close_hold_seconds`, `arm_motion_ratio`, `required_bursts`는 무시한다. 값이 잘못돼도 새 규칙에 대신 대입하지 않는다. `required_bursts=2`를 `min_samples=2`로 번역하지 않는다. 그 밖의 알 수 없는 바깥쪽 optional parameter는 기존 fail-soft 원칙을 따르고 로그에 남긴다. 새 `fight_r5` 블록 내부는 위의 엄격 검증을 적용한다.

## 저장·확인·오류

- 적용 후보를 검증한 뒤 기존 저장/controlled restart 경로를 사용한다. 검증·저장 실패는 기존 실행을 보존해야 한다.
- 요청 원문과 무시/보정 사유를 실행 설정에 섞지 않는다. 실효 설정이 같고 다른 Start 입력도 같으면 불필요한 새 실행을 만들지 않는다. 입력 URI/시간 anchor/ROI 등의 실제 변경은 별개다.
- AI의 기존 `GET /api/v1/streams/{stream_id}/settings`에서 `configured.fight`와 `effective.fight`, `restart_pending`을 확인한다. 이는 기존 AI 관리 인터페이스이며 새 Web 공개 접근권한을 제공하는 것은 아니다.
- `engine: "r5"`, `r5` 블록과 보정된 `person_score/joint_score`가 실효값이다. 요청값·보정 여부·무시 이유는 Start 처리 로그 `fight_r5_parameter`, `fight_r5_parameter_ignored`, `fight_r5_effective`로 확인한다.
- Start의 기존 비동기 처리/operation 조회 계약을 유지한다. 접수만으로 적용 완료라고 표시하지 않는다. `FIGHT_R5_*` 오류는 기존 operation의 error 경로로 전달한다.
- 대표 오류: `FIGHT_R5_NOT_ENABLED`, `FIGHT_R5_SCHEMA_VERSION_REQUIRED`, `FIGHT_R5_SCHEMA_VERSION_UNSUPPORTED`, `FIGHT_R5_FIELDS_INVALID`, `FIGHT_R5_WINDOW_INVALID`, `FIGHT_R5_SAMPLE_COUNT_INVALID`, `FIGHT_R5_SCORE_INVALID`, `FIGHT_R5_RESET_INVALID`.

## Redis와 Event

- 기존 `objects[].bbox/color/labelText/trackId/scenarioResults/eventIds`를 유지한다. class는 기존 person이며 새로운 가짜 공간 객체를 추가하지 않는다.
- 현재 조건에 기여한 실제 사람과 현재 연결이 확인되는1초 유지 대상은 기존 빨강 `#FF3030`이다. `scenarioResults[].scenario="FIGHT"`, 표시 state는 `FIGHT`다. 실시간 노랑은 없다.
- 빨강은 현재 조건 또는 표시 유지다. Web은 빨강이나 state만으로 별도 알람을 생성하지 않고 기존 Event 수신 경로를 사용한다.
- 기여 객체 표시는 가해자/피해자 분류가 아니다. 미관측·연결 불확실에는 옛 bbox를 남기지 않는다. 확정되지 않은 다른 그룹/방관자까지 일괄 빨강으로 만들지 않는다.
- 원래 Tracking ID가 없으면 null을 유지한다. Event의 대표 objectRef는 현재 실제 입력 객체를 가리키며, 대표 trackId도 null일 수 있다. 이는 싸움 당사자 단독 판정이 아니라 기존 객체 참조 필드의 호환 용도다.
- 한 ROI의 같은 episode에서는 알람을 중복 발급하지 않는다. 내부2초 cooldown·관측된 quiet2초 재무장·관측 부족5초 만료는 기존 R5 계산을 유지한다.
- Web cooldown은 별도의 최종 Event admission이다. 거부된 내부 Event를 나중에 예약 재전송하지 않는다. 이후 재무장된 새 episode를 기다린다. 내부 판정/빨간 표시와 실제 알람 수는 다를 수 있다.
- 썸네일·클립 생성과 전달은 기존 경로다. `eventType=FIGHT`, 기존 URL 이름과 기존 score 의미를 유지하며 규칙 점수를 싸움 확률로 만들지 않는다.
- 기존 디버깅 영상의 빨강/노랑과 이번 실시간 빨강 통일은 별개다. 실제 브라우저 소비 코드와 렌더링은 이번 격리 검증에 포함되지 않았다.

## Web 연계 체크리스트

1. AI 담당자가 R5 지원 소스와 대상 채널의 engine 선택을 확인한 뒤에만 새 블록을 사용한다.
2. 구형 요청으로도 Start가 가능하고 옛 동작 설정이 무시되는지 확인한다.
3. 사람0.15/관절0.05 요청의 실효값이0.25/0.10인지 확인한다.
4. 새 설정 변경·생략 유지·명시적 초기화·동일 실효값 재전송·잘못된 버전 거부를 확인한다.
5. 실제 bbox 빨강과 표시 유지, 미관측 해제, Event·썸네일·클립 열기를 확인한다.
6. 자연 장면·장시간 라이브 품질 평가는 별도 보류다. 이 체크리스트 실행과 운영 전환도 별도 승인이 필요하다.
