import pathlib,json,csv,hashlib,sys,shutil
R=pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0,str(R.parent/'2026-10-29_contextra-self-filming-pose-cache_r01/helpers'))
import control as c
c.ROOT=R;c.REMOTE='/storage/contextra/experiments/action-model-eval/2026/'+R.name
specs=json.loads((R/'specs.json').read_text());assert len(json.loads((R/'catalog.json').read_text())['completed'])==20
c.PREFIX='contextra-user20-replay-1102-'
c.scp(R/'helpers/verify_tracking.py','si@58.233.207.31:'+c.REMOTE+'/helpers/')
for key,s in specs.items():
 c.trial('verify_tracking',key)
 c.scp('si@58.233.207.31:'+s['cache']+'/reports/tracker_replay.json',R/'clips'/key/'reports/tracker_replay.json')
meta=R/'metadata';meta.mkdir();(meta/'observation_maps').mkdir()
entries=[];total=0;rgb=0;files=0
for key,s in specs.items():
 root=R/'clips'/key
 rows=json.loads((root/'inputs/pose.json').read_text())
 assert len(rows)==s['expected_samples']
 assert json.loads((root/'reports/pose_summary.json').read_text())['tracking']['frame_rate']==30
 assert c.digest(pathlib.Path(s['local_source']))==s['sha256']
 for rel,h in json.loads((root/'reports/cache_hashes.json').read_text())['files'].items():assert c.digest(root/rel)==h;files+=1
 with (root/'recording_frame_map.csv').open(encoding='utf-8-sig') as f:mp={int(x['clip_frame_1based']):x for x in csv.DictReader(f)}
 with (meta/'observation_maps'/f'{key}.csv').open('x',encoding='utf-8-sig',newline='') as f:
  w=csv.writer(f);w.writerow(['clip_id','sample','clip_frame','clip_pts','recording_frame','recording_pts'])
  for x in rows:
   m=mp[x['source_frame']];assert abs(float(m['clip_pts_seconds'])-x['seconds'])<.000001
   w.writerow([key,x['sample'],x['source_frame'],x['seconds'],m['recording_frame_1based'],m['recording_pts_seconds']])
 count=len(list((root/'inputs/frames').glob('*.png')));assert count==len(rows);rgb+=count;total+=count
 entries.append(dict(id=key,observations=count,local=str(root),remote=s['cache'],rgb_local=True,rgb_remote=True,case=s['source_clip'],engine_expectation='not assigned'))
c.save(meta/'catalog.json',dict(entries=entries,observations=total,tracker_frame_rate=30,observation_fps=10,local_complete=True))
c.save(meta/'locations.json',dict(local=str(R),remote=c.REMOTE,rgb_on_both_servers=True,weights_copied_to_test=False))
before=json.loads((R/'reports/production_before.json').read_text());after=json.loads((R/'reports/production_after_resume.json').read_text())
c.save(R/'reports/final_validation.json',dict(clips=20,observations=total,rgb_files=rgb,verified_cache_files=files,all_sha256_match=True,source_preserved=True,config_unchanged=before['config_sha256']==after['config_sha256'],containers_unchanged=before['containers']==after['containers'],remaining=c.exclusive_gpu()))
(R/'00_README_ko.md').write_text('''# 20클립 Pose cache 완결본

metadata/catalog.json과locations.json에서 시작한다. RGB PNG까지 test와아이서티 양쪽에 실파일로 보관했다.
클립별 inputs/pose.json, pose_candidates_prefilter.jsonl, frames/, sampling.csv와reports를 제공한다.
관측10FPS·Tracker frame_rate30이며30FPS 추론이 아니다. 클립별독립ID이고 q는영구ID가 아니다.
parser0.1/NMS0.85/파생person0.15, 원시17관절score·100개decoded후보를 보존했다. valid_joints는과거0.3집계이며 새기준에는원시score를 사용한다.
운영사람/관절score·ROI·On/Off를 반영하지 않았다. 전체화면이다. 없는관절을 생성하지 않았다.
metadata/observation_maps는 sample→클립Frame/PTS→기존60초녹화Frame/PTS를 연결한다. sample/10은실제시간이아니다.
camera11의두번째구간은사용자쓰러짐, camera8/9는지정구간이며라벨을Event GT로승격하지않았다.
첫 검증에서예전Tracker10 기준불일치가발견되어새run의reference만승인값30으로맞췄다. 첫클립재추론없음.
기존호환보고서의RGB remote 문구는원형설명이며 이번RGB는test에도완전히있다.
CPU OpenCV 전처리를 재사용했으며 라이브GPU resize·연속이력과수치동일성을보장하지않는다.
FIGHT/FALL 판정·튜닝·운영설정변경·새영상제작은하지않았다. 이전cache와원본은보존한다.
''',encoding='utf-8')
c.save(R/'checkpoint.json',dict(stage='COMPLETE_WITH_LOCAL_RGB',completed=list(specs),observations=total,engine_evaluation=False))
c.scp(meta,'si@58.233.207.31:'+c.REMOTE+'/')
for n in ('00_README_ko.md','catalog.json','checkpoint.json'):c.scp(R/n,'si@58.233.207.31:'+c.REMOTE+'/')
c.scp(R/'reports/final_validation.json','si@58.233.207.31:'+c.REMOTE+'/')
print(json.dumps(dict(clips=20,observations=total,rgb=rgb,verified_files=files)),flush=True)
