"""Finalize only verified generated outputs, leaving caches outside the download."""
import csv
import hashlib
import json
from pathlib import Path

root=Path(__file__).resolve().parent.parent
dest=root/'download'
specs=json.loads((root/'specs.json').read_text())
expected=('B_Space_Groups_Review.mp4','B_Motion_Evidence_Review.mp4','B_Association_Difference_Review.mp4')
kinds=('groups','motion','association')
inventory=[];summaries={};baseline=0;frames=0
for clip,spec in specs.items():
    d=dest/clip/'data'
    summary=json.loads((d/'engine_summary.json').read_text())
    audit=json.loads((d/'technical_audit.json').read_text());assert audit['status']=='PASS'
    for name,digest in summary['files'].items():assert hashlib.sha256((d/name).read_bytes()).hexdigest()==digest
    if summary['baseline_comparison']=='EXACT_MATCH':baseline+=1
    for name,kind in zip(expected,kinds):
        report=json.loads((d/(kind+'_video_validation.json')).read_text())
        assert report['status']=='PASS' and report['frames']==summary['display_observations']
        assert hashlib.sha256((dest/clip/name).read_bytes()).hexdigest()==report['sha256']
        frames+=report['frames']
        inventory.append(dict(input=clip,video=name,kind=kind,frames=report['frames'],fps=10,
            source_file=spec['source_file'],source_sha256=spec['sha256'],sha256=report['sha256']))
    summaries[clip]=summary
assert len(inventory)==45 and baseline==6
assert all(s['settings']==next(iter(summaries.values()))['settings'] for s in summaries.values())
with (dest/'video_list.csv').open('w',newline='',encoding='utf-8-sig') as f:
    w=csv.DictWriter(f,fieldnames=inventory[0]);w.writeheader();w.writerows(inventory)
(dest/'validation/summary.json').write_text(json.dumps(summaries,indent=2))
(dest/'conditions.json').write_text(json.dumps(dict(settings=next(iter(summaries.values()))['settings'],
    baseline_algorithm_revision=1,display_joint_score=.10,display_bbox_score=.25,
    groups='Frame-local connected components of original eligible proximity pairs; display only',
    group_polygon='Convex hull of member bbox corners; 18 percent overlay; overlapping hulls hatched, not merged',
    partial_stage='Valid arm with motion>=.07 or hand distance<=1.0; partial is not a detected action',
    instant_stage='Original per-arm strike or dynamic contact predicate',
    accumulated_stage='Original pair rule and matching current per-arm predicate',
    association_costs='actual_orientation means current slot ordering cost; for B_RESET_SAME_T_PAIR it is a reference, not an accepted match',
    identity='T is cached tracker ID; q is frame-local query; S is a short pair state; none proves physical identity',
    no_new_inference=True,no_new_tracking=True,no_operating_events=True),ensure_ascii=False,indent=2))
readme='''Contextra B 진단 검토 / 15입력 × 3종 = 45본

입력 폴더마다 같은 Frame·PTS의 정상 속도10FPS 영상3개가 있습니다.
숫자는 원본 파일의1-based Frame이며 Runtime frameSeq와 다릅니다.
표시와 계산 모두 bbox>=0.25, 관절>=0.10입니다. 낮은 참고 관절은 표시하지 않습니다.

1. B_Space_Groups_Review.mp4
기존 B 유효 몸통과 근접 관계를 연결한2인 이상 그룹입니다.
P-Q와 Q-R이 가까우면 P-R이 멀어도 같은 그룹입니다.
반투명 영역은 bbox 외곽의 표시용 볼록 다각형이며 실제 위험 heatmap/판정 ROI가 아닙니다.
색과 G번호는 현재 Frame 구분용입니다. 다른 Frame의 같은 색이 동일 그룹임을 보장하지 않습니다.
그룹 인원 표시는 유효 검출객체 수이며 실제 인원이나 중복 제거 성공을 뜻하지 않습니다.
도형 안에 보이는 다른 사람이 자동으로 구성원에 포함되는 것은 아닙니다. 연결선·구성원 목록을 함께 확인하세요.
가는 선은 근접 관계, 빗금은 다각형 겹침입니다. 겹침만으로 그룹을 합치지 않습니다.
그룹은 새 표시용 해석이며 원래 B 판정·연결·상태에 되먹이지 않습니다.

2. B_Motion_Evidence_Review.mp4
작은 얇은 링: 부분 조건. 움직임 일부 또는 손의 근접만 만족해도 표시될 수 있습니다.
중간 굵은 링: 원래 타격형/동적 접촉의 순간 조건 충족.
큰 굵은 링: 원래 반복·지속시간과 현재 해당 팔 조건까지 충족.
몸통 기준선·중심점은 상대 움직임/거리의 계산 참고입니다. 발차기 규칙은 없습니다.
사람별 skeleton 색은 객체 구분, 쌍별 선 색과S번호는 쌍 구분입니다.
한 사람이 여러 쌍에 속해도 skeleton은 한 색을 유지하고 여러 쌍의 선·표식으로 구분합니다.
현재 규칙에 기여한 모든 쌍을 표시하며 실제 싸움 당사자·가해자/피해자를 확정하지 않습니다.
현재 조건·새 시험 이벤트·기존0.4초 표시 유지를 구분합니다.
유지 중 현재 대응이 끊기면 이전 skeleton을 재사용하지 않고 ROI 상태만 남깁니다.
표시 전 최대3초의 실제 과거 cache를 처리하므로 새 Event0건이어도 현재 규칙이 충족될 수 있습니다.

3. B_Association_Difference_Review.mp4
분홍 bbox와 굵은 skeleton은 기존T와 B 공간 연결 방식이 다른 객체입니다.
T 변경/순서 교환에도 B가 이어 간 경우와, 같은T쌍인데 B가 새로 시작한 경우를 구분합니다.
이전→현재 선은 실제 B 연결 유지에만 그립니다. B가 끊은 경우는 과거 참조점만 표시합니다.
현재순서/반대순서 비용은 거리 참고값입니다. 같은T를 찾아 비교한 새S에서는 채택된 연결이 아닙니다.
T가 없거나 새 진입·근접 이탈·관측 공백이면 비교 불가/별도 상태이며 오류로 세지 않습니다.
기존T를 정답으로 두지 않습니다. B 오류·Tracker 오류·실제 동일인 여부는 사용자 판단입니다.
q는 Frame별 출력 칸, S는 짧은 쌍 상태이며 T/S 숫자를 직접 비교하지 않습니다.
L/R은 모델의 신체 좌우 관절 구분이며 화면의 왼쪽/오른쪽이라는 뜻은 아닙니다.

공통 해석·보호
화면의 상세 영역은 최대3항목이며 전체 대상 표시는 유지하고 모든 근거를data에 보존합니다.
입력 부족·관절 미확보·후보 없음·조건 미충족은 구분하며 정상 행동 확정으로 쓰지 않습니다.
입력 bbox가 있어도 유효 몸통이 없으면 몸통 근거 부족입니다. 유효 몸통이1개면2인 그룹을 만들지 않습니다.
유효 몸통이2개 이상인데 근접 쌍이0이면 근접 기준 미충족입니다. cache 후보 없음과 score 미달 상세는 data의rejected와 기존 cache 근거로 구분합니다.
cache 보존 전에 걸러진 후보나 누락된 관절을 복구하지 않았습니다.
기존6입력 B 결과를 관측별로 대조했고 나머지9입력은 같은 엔진으로 CPU 평가했습니다.
본 작업은 새 모델/Pose/Tracking 추론, 운영 설정·채널 제어, Web Event 저장·전송을 하지 않았습니다.
운영 전후 비교에서 camera27의run과 runtime 설정 파일 지문 변화가 관측됐습니다. 변경 주체는 미확인이며 자동 복구·원인 단정은 하지 않았습니다.
camera24의 수집된 설정 항목·Pose 모델/모델 설정·운영 컨테이너는 같고 서비스 ready 및6running/22stopped 상태를 확인했습니다. 실시간FPS 영향은 별도 측정하지 않았습니다.
05·10·14도 포함했지만 모든 사람의 탐지/추적이나 영상별 필수 수용을 보장하지 않습니다.
개인별 연결 없는 공간 판정과 엔진 개편은 이번 영상 검토 뒤 별도 협의합니다.

피드백 예시
- 입력 / 영상 종류:
- 이해하기 쉬운 점 / 이상한 점:
- 공간 묶임 / 동작 강조 / 연결 차이에 대한 관찰:
- 추가 질문:
'''
(dest/'00_README_ko.txt').write_text(readme,encoding='utf-8')
files={str(p.relative_to(dest)):hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(dest.rglob('*')) if p.is_file() and p.name!='SHA256SUMS.json'}
assert sum(name.endswith('.mp4') for name in files)==45
assert not any('partial' in name or 'preview' in name for name in files)
(dest/'SHA256SUMS.json').write_text(json.dumps(files,indent=2))
print(json.dumps(dict(files=len(files),videos=45,video_frames=frames,baseline_exact=baseline,
    sha256=hashlib.sha256((dest/'SHA256SUMS.json').read_bytes()).hexdigest())))
