Download _Metadata/executed_collect_reference_videos.py from MyNiuuu/gamescene: direct link, hf CLI and curl.
- Browser
- Download file 21.1 kB
-
https://huggingface.co/MyNiuuu/gamescene/resolve/main/_Metadata/executed_collect_reference_videos.py
- Command line
-
hf download hf://MyNiuuu/gamescene/_Metadata/executed_collect_reference_videos.py
-
curl -L -o executed_collect_reference_videos.py https://huggingface.co/MyNiuuu/gamescene/resolve/main/_Metadata/executed_collect_reference_videos.py
21.1 kB
| """Collect existing source-scene videos as verified physical copies; never move originals.""" | |
| from __future__ import annotations | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| from datetime import datetime, timezone | |
| import argparse | |
| import csv | |
| import hashlib | |
| import html | |
| import json | |
| import os | |
| from pathlib import Path | |
| import re | |
| import shutil | |
| import threading | |
| import time | |
| import urllib.parse | |
| HERE = Path(__file__).resolve().parent | |
| REPO = HERE.parents[1] | |
| DEST = Path('Z:/ryli/UE_ReferenceVideos_All_20260925') | |
| LOCK = threading.Lock() | |
| PROGRESS = {} | |
| def utc(): | |
| return datetime.now(timezone.utc).isoformat() | |
| def read(p): | |
| return json.loads(Path(p).read_text('utf-8-sig')) | |
| def save(p, data): | |
| p = Path(p) | |
| p.parent.mkdir(parents=True, exist_ok=True) | |
| tmp = p.with_name(p.name + '.writing') | |
| tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') | |
| os.replace(tmp, p) | |
| def digest(p): | |
| h = hashlib.sha256() | |
| with Path(p).open('rb') as f: | |
| for chunk in iter(lambda: f.read(8 * 1024 * 1024), b''): | |
| h.update(chunk) | |
| return h.hexdigest() | |
| def safe_title(s): | |
| return re.sub(r'[<>:"/\\|?*\x00-\x1f]', '_', s).strip(' .') | |
| def plan(): | |
| records, groups, seen = [], {}, {} | |
| sources = [('20260921', Path('Z:/ryli/UE_RecordedRouteVideos_20260921/batch_manifest.json')), | |
| ('20260924', Path('Z:/ryli/UE_RecordedRouteVideos_20260924/batch_manifest_v4.json'))] | |
| for batch, manifest in sources: | |
| for scene in read(manifest)['scenes']: | |
| folder = Path(scene['output_dir']) | |
| receipt_path = folder / 'video_receipt.json' | |
| rec = read(receipt_path) | |
| assert rec['status'] == 'complete' and rec.get('partial_diagnostic') is False | |
| video = folder / 'route_native.mp4' | |
| stat = video.stat() | |
| assert stat.st_size == rec['video_bytes'] | |
| assert rec['trajectory_sha256'] == scene['trajectory_sha256'] | |
| identity = rec['video_sha256'] | |
| if identity in seen: | |
| seen[identity]['also_listed_in'].append(str(manifest)) | |
| continue | |
| group_id = scene['scene_id'] | |
| if group_id not in groups: | |
| groups[group_id] = {'number': len(groups)+1, 'title': scene['title'], 'map_package': scene['map_package']} | |
| group = groups[group_id] | |
| capture_date = rec['completed_utc'][:10].replace('-', '') | |
| relative = f"Scenes/{group['number']:02}_{safe_title(group['title'])}/reference_{capture_date}.mp4" | |
| assert relative not in {r['relative_path'] for r in records} | |
| row = {'kind': 'continuous_scene_reference', 'scene_id': group_id, 'scene_number': group['number'], | |
| 'title': scene['title'], 'batch': batch, 'capture_date': capture_date, | |
| 'source_video': str(video), 'relative_path': relative, 'destination_video': str(DEST / relative), | |
| 'source_receipt': str(receipt_path), 'source_manifest': str(manifest), 'also_listed_in': [], | |
| 'expected_sha256': identity, 'bytes': stat.st_size, 'source_mtime_ns': stat.st_mtime_ns, | |
| 'duration_seconds': rec['encoded_duration_seconds'], 'frames': rec['frame_count'], | |
| 'fps': rec['fps'], 'width': rec['width'], 'height': rec['height'], | |
| 'trajectory_sha256': rec['trajectory_sha256'], 'map_package': rec['map_package'], | |
| 'note_zh': '完整原路线,保留停留、快速移动、遮挡和空域;未宣称全部区域或逐帧参考质量通过。', | |
| 'source_quality_certified': False} | |
| records.append(row) | |
| seen[identity] = row | |
| archive = Path('Z:/ryli/UE_RecordedRouteVideos_20260920/Overview') | |
| rec = read(archive / 'video_receipt.json') | |
| p = archive / 'route_native.mp4' | |
| stat = p.stat() | |
| assert stat.st_size == rec['video_bytes'] and rec['status'] == 'complete' | |
| relative = 'Historical/Asset_Display/餐厅资产陈列_20260920.mp4' | |
| records.append({'kind': 'historical_asset_display', 'scene_id': rec['scene_id'], 'title': '餐厅资产陈列(历史)', | |
| 'source_video': str(p), 'relative_path': relative, 'destination_video': str(DEST / relative), | |
| 'source_receipt': str(archive / 'video_receipt.json'), 'source_manifest': None, 'also_listed_in': [], | |
| 'expected_sha256': rec['video_sha256'], 'bytes': stat.st_size, 'source_mtime_ns': stat.st_mtime_ns, | |
| 'duration_seconds': rec['encoded_duration_seconds'], 'frames': rec['frame_count'], | |
| 'fps': rec['fps'], 'width': rec['width'], 'height': rec['height'], | |
| 'note_zh': '单体资产陈列地图,不计入19个完整游戏场景。', 'source_quality_certified': False}) | |
| cathedral = Path('Z:/ryli/UE_InteractiveReferences_20260917/scenes/City__04_OldCathedral__L_Showcase') | |
| for sub, name in [('captures/20260917T093116_569589_ab419328', '教堂_稀疏采样_20260917'), ('reference_01', '教堂_旧地图稀疏采样_reference01')]: | |
| folder = cathedral / sub | |
| p = folder / 'walkthrough.mp4' | |
| timing = read(folder / 'video_timing.json') | |
| assert timing['hold_between_recorded_frames'] is True | |
| stat = p.stat() | |
| relative = f'Historical/Sparse_Frame_Videos/{name}.mp4' | |
| records.append({'kind': 'historical_sparse_frame_video', 'scene_id': 'City__04_OldCathedral__L_Showcase', | |
| 'title': name, 'source_video': str(p), 'relative_path': relative, 'destination_video': str(DEST / relative), | |
| 'source_receipt': str(folder / 'video_timing.json'), 'source_manifest': None, 'also_listed_in': [], | |
| 'expected_sha256': None, 'bytes': stat.st_size, 'source_mtime_ns': stat.st_mtime_ns, | |
| 'duration_seconds': None, 'source_route_duration_seconds': timing['source_duration_seconds'], | |
| 'frames': None, 'native_unique_frames': timing['native_unique_frame_count'], | |
| 'fps': timing['output_fps'], 'width': None, 'height': None, | |
| 'note_zh': '1596张原生图以保持画面的方式编码,不能当作连续逐帧原生录像。reference01的源地图版本与当前网络地图不同。', | |
| 'source_quality_certified': False}) | |
| assert len(groups) == 19 and sum(r['kind'] == 'continuous_scene_reference' for r in records) == 22 | |
| return {'schema': 'ue-reference-video-consolidation/1', 'planned_utc': utc(), 'destination': str(DEST), | |
| 'status': 'planned', 'scene_count': len(groups), 'continuous_scene_video_count': 22, | |
| 'historical_video_count': 3, 'total_video_count': len(records), | |
| 'scene_video_bytes': sum(r['bytes'] for r in records if r['kind'] == 'continuous_scene_reference'), | |
| 'total_bytes': sum(r['bytes'] for r in records), | |
| 'scene_duration_seconds': sum(r['duration_seconds'] for r in records if r['kind'] == 'continuous_scene_reference'), | |
| 'rows': records, 'source_batches': [str(p) for _, p in sources], | |
| 'excluded': [ | |
| {'path': 'Z:/ryli/dense_references_20260916', 'status': 'discard', 'reason_zh': '用户已明确弃用整批,不重新纳入或解包;原归档保留。'}, | |
| {'path': 'Z:/ryli/GeneratedGames_MatchedRoutes_20260924', 'reason_zh': '生成游戏对照录像,不是原场景参考;其中reference副本与原片重复。'}, | |
| {'reason_zh': '诊断短预览、失败尝试、录制缓存副本及生成游戏展示不重复收录。'}], | |
| 'copy_policy': 'physical_byte_identical_copies_originals_unchanged', | |
| 'quality_scope_zh': '整理和完整性校验;不升级历史质量结论,不剪辑或重新编码。'} | |
| def progress(row, phase, n): | |
| with LOCK: | |
| PROGRESS[row['relative_path']] = {'phase': phase, 'bytes': n, 'total': row['bytes'], 'updated_utc': utc()} | |
| data = {'status': 'copying_and_verifying', 'updated_utc': utc(), 'pid': os.getpid(), 'files': dict(PROGRESS)} | |
| save(HERE / 'PROGRESS.json', data) | |
| def copy_one(row): | |
| start = time.monotonic() | |
| source = Path(row['source_video']) | |
| target = DEST / row['relative_path'] | |
| assert target.resolve().is_relative_to(DEST.resolve()) | |
| target.parent.mkdir(parents=True, exist_ok=True) | |
| assert source.stat().st_size == row['bytes'] and source.stat().st_mtime_ns == row['source_mtime_ns'] | |
| record_file = HERE / 'verified' / (hashlib.sha256(row['relative_path'].encode()).hexdigest()[:16] + '.json') | |
| if target.exists(): | |
| previous = read(record_file) | |
| assert previous['source_video'] == str(source) | |
| assert target.stat().st_size == row['bytes'] | |
| actual = digest(target) | |
| assert actual == previous['sha256'] | |
| if row['expected_sha256']: | |
| assert actual == row['expected_sha256'] | |
| progress(row, 'verified_existing_copy', row['bytes']) | |
| return previous | |
| temp = target.with_name(target.name + f'.copying_{os.getpid()}') | |
| assert not temp.exists(), f'Preserve incomplete attempt: {temp}' | |
| h = hashlib.sha256() | |
| copied = 0 | |
| last = time.monotonic() | |
| progress(row, 'copying', 0) | |
| with source.open('rb') as inp, temp.open('xb') as out: | |
| for chunk in iter(lambda: inp.read(8 * 1024 * 1024), b''): | |
| out.write(chunk) | |
| h.update(chunk) | |
| copied += len(chunk) | |
| if time.monotonic() - last > 4: | |
| progress(row, 'copying', copied) | |
| last = time.monotonic() | |
| out.flush() | |
| os.fsync(out.fileno()) | |
| source_hash = h.hexdigest() | |
| assert copied == row['bytes'] | |
| assert source.stat().st_size == row['bytes'] and source.stat().st_mtime_ns == row['source_mtime_ns'] | |
| if row['expected_sha256']: | |
| assert source_hash == row['expected_sha256'], f'Original hash changed: {source}' | |
| progress(row, 'verifying_destination_bytes', copied) | |
| target_hash = digest(temp) | |
| assert target_hash == source_hash | |
| assert not target.exists() | |
| # On Windows rename refuses an existing destination. No source file is moved or deleted. | |
| os.rename(temp, target) | |
| result = {'source_video': str(source), 'destination_video': str(target), 'relative_path': row['relative_path'], | |
| 'bytes': copied, 'sha256': source_hash, 'source_receipt_hash_matched': bool(row['expected_sha256']), | |
| 'source_and_destination_full_bytes_verified': True, 'source_unchanged_during_copy': True, | |
| 'verified_utc': utc(), 'elapsed_seconds': round(time.monotonic()-start, 2)} | |
| save(record_file, result) | |
| progress(row, 'verified', copied) | |
| print(json.dumps({'copied': row['title'], 'bytes': copied, 'seconds': result['elapsed_seconds']}, ensure_ascii=False), flush=True) | |
| return result | |
| def publish(data, receipts): | |
| result_by_path = {r['relative_path']: r for r in receipts} | |
| for row in data['rows']: | |
| row['copy_verification'] = result_by_path[row['relative_path']] | |
| assert Path(row['destination_video']).stat().st_size == row['bytes'] | |
| data.update(status='complete', completed_utc=utc(), source_and_destination_all_full_hashes_verified=True, | |
| new_full_media_decode_performed=False, note_zh='完整原生视频沿用原批次解码及审阅;本次重新逐字节核对源与副本。') | |
| save(DEST / 'CATALOG.json', data) | |
| save(HERE / 'FINAL_CATALOG.json', data) | |
| with (DEST / 'CATALOG.csv').open('w', encoding='utf-8-sig', newline='') as f: | |
| fields = ['kind', 'title', 'relative_path', 'destination_video', 'source_video', 'duration_seconds', 'frames', 'fps', 'width', 'height', 'bytes', 'sha256', 'note_zh'] | |
| writer = csv.DictWriter(f, fieldnames=fields) | |
| writer.writeheader() | |
| for row in data['rows']: | |
| writer.writerow({k: (row['copy_verification']['sha256'] if k == 'sha256' else row.get(k)) for k in fields}) | |
| meta = DEST / '_Metadata' | |
| meta.mkdir(exist_ok=True) | |
| for n, row in enumerate(data['rows'], 1): | |
| shutil.copy2(row['source_receipt'], meta / f'{n:02}_original_{Path(row["source_receipt"]).name}') | |
| for n, path in enumerate(data['source_batches'], 1): | |
| shutil.copy2(path, meta / f'batch_{n}_manifest.json') | |
| shutil.copy2(Path('Z:/ryli/dense_references_20260916/DATASET_STATUS.json'), meta / 'DISCARD_DATASET_STATUS.json') | |
| save(meta / 'COPY_VERIFICATION.json', receipts) | |
| docs = [Path('Z:/ryli/UE_RecordedRouteVideos_20260920/DELIVERY_zh-CN.md'), | |
| Path('Z:/ryli/UE_RecordedRouteVideos_20260921/METHOD_zh-CN.md'), | |
| Path('Z:/ryli/UE_RecordedRouteVideos_20260924/DELIVERY_zh-CN.md')] | |
| for p in docs: | |
| shutil.copy2(p, meta / (p.parent.name + '_' + p.name)) | |
| scene_rows = [r for r in data['rows'] if r['kind'] == 'continuous_scene_reference'] | |
| mins = data['scene_duration_seconds'] / 60 | |
| lines = ['# Unreal 原场景参考视频汇总', '', f"共19个真实场景、22条不同完整路线,约{mins:.2f}分钟。另有3条历史视频,分别为1条资产陈列和2条稀疏原生图保持影片。", '', | |
| '所有视频均为实际文件副本;已逐字节计算源文件与目标文件摘要,确认一致。没有剪辑、改画面或重新编码。原目录保持不变。', '', | |
| '- `Scenes`:正式连续原场景路线视频,每景一个目录;仓库、办公室和学校保留两次不同采样。', | |
| '- `Historical`:资产陈列和旧教堂稀疏采样,不计入22条连续场景影片。', | |
| '- `index.html`:场景清单、搜索和直接播放。', | |
| '- `CATALOG.csv` / `CATALOG.json`:绝对路径、来源、时长、大小及文件摘要。', | |
| '- `_Metadata`:原回执、批次清单、复制验证与历史使用限制。', '', | |
| '完整录像包含原路线的遮挡、空域和快速移动,不代表全地图覆盖或逐帧质量合格。学校9月24日采用已完成原生帧恢复编码,原引擎退出码未知的限制沿用原回执。', '', | |
| '9月16日整批discard结果仍在 `Z:\\ryli\\dense_references_20260916`,按此前决定保持弃用;未解包、未重新混入参考输入。诊断短片、生成游戏视频与重复副本不计入本目录场景参考。', '', | |
| '| 场景 | 录像日期 | 秒数 | 相对路径 |', '| --- | --- | ---: | --- |'] | |
| for r in scene_rows: | |
| lines.append(f"| {r['title']} | {r['capture_date']} | {r['duration_seconds']:.3f} | [{r['relative_path']}]({urllib.parse.quote(r['relative_path'])}) |") | |
| (DEST / 'README_zh-CN.md').write_text('\n'.join(lines)+'\n', encoding='utf-8') | |
| table = [] | |
| for r in data['rows']: | |
| kind = '连续场景' if r['kind'] == 'continuous_scene_reference' else '历史资料' | |
| duration = f"{r['duration_seconds']:.2f} 秒" if r.get('duration_seconds') is not None else '约398.49秒原路线;稀疏帧保持' | |
| path = urllib.parse.quote(r['relative_path'], safe='/') | |
| table.append(f'<tr data-kind="{kind}"><td>{html.escape(r["title"])}</td><td>{kind}</td><td>{r.get("capture_date", "历史")}</td><td>{duration}</td><td><a href="{path}" target="_blank">打开视频</a> <button data-src="{path}" data-title="{html.escape(r["title"],quote=True)}">页内播放</button><small>{html.escape(r["relative_path"])}</small></td></tr>') | |
| page = '''<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Unreal 原场景参考视频汇总</title><style> | |
| body{font:16px/1.8 "Microsoft YaHei",sans-serif;margin:0;background:#eef2f6;color:#203249}main{max-width:1200px;margin:32px auto;padding:28px;background:white;border-radius:10px}h1{margin-top:0}.summary{background:#e7f1f7;padding:14px 20px;border-left:4px solid #397493}a{color:#176290}input,select,button{font:inherit;padding:7px;margin:8px 5px 8px 0}table{width:100%;border-collapse:collapse;font-size:14px}th,td{border-bottom:1px solid #ccd7e1;padding:10px;text-align:left}th{background:#eaf0f5}small{display:block;font-size:11px;overflow-wrap:anywhere;color:#617185}video{width:100%;max-height:70vh;background:#111}#viewer{margin:24px 0}#viewer[hidden]{display:none}footer{font-size:13px;color:#536577;margin-top:25px}@media(max-width:750px){main{margin:0;padding:14px}table{font-size:11px}th,td{padding:5px}h1{font-size:24px}} | |
| </style><main><h1>Unreal 原场景参考视频汇总</h1>''' | |
| page += f'<p class="summary"><b>19 个真实场景 · 22 条完整路线 · {mins:.2f} 分钟</b><br>仓库、办公室、学校各保留两条采样。另列3条历史资料。全部为字节一致的实际视频副本,原文件保留。</p>' | |
| page += '<p>这里只汇总原场景采样;未混入生成游戏演示。录像完整性与画面可用性分别判断,遮挡和空域仍需按用途筛选。</p><p><a href="README_zh-CN.md">使用说明</a> · <a href="CATALOG.csv">下载清单</a> · <a href="CATALOG.json">来源与校验记录</a></p><input id="search" placeholder="搜索场景或日期"><select id="kind"><option>全部</option><option>连续场景</option><option>历史资料</option></select><p id="count"></p><section id="viewer" hidden><h2 id="now"></h2><video id="player" controls preload="none"></video></section><table><thead><tr><th>场景</th><th>类别</th><th>日期</th><th>时长</th><th>视频文件</th></tr></thead><tbody>' | |
| page += ''.join(table) | |
| page += '</tbody></table><footer>9月16日整批弃用采样未重新纳入,位置:Z:\\ryli\\dense_references_20260916。历史资产陈列、稀疏图像影片不计入连续场景数量。复制校验不代表已经通过全部参考质量验收。</footer></main><script>function filter(){const q=document.getElementById("search").value.toLowerCase(),k=document.getElementById("kind").value;let n=0;document.querySelectorAll("tbody tr").forEach(r=>{r.hidden=!r.textContent.toLowerCase().includes(q)||(k!=="全部"&&r.dataset.kind!==k);if(!r.hidden)n++;});document.getElementById("count").textContent="当前显示 "+n+" 条视频";}document.getElementById("search").oninput=filter;document.getElementById("kind").onchange=filter;document.querySelectorAll("button[data-src]").forEach(b=>b.onclick=()=>{const v=document.getElementById("player");v.pause();v.src=b.dataset.src;document.getElementById("now").textContent=b.dataset.title;document.getElementById("viewer").hidden=false;v.load();document.getElementById("viewer").scrollIntoView({behavior:"smooth"});});filter();</script></html>' | |
| (DEST / 'index.html').write_text(page, encoding='utf-8') | |
| local = REPO / 'UNREAL_REFERENCE_VIDEOS_zh-CN.md' | |
| local.write_text('# Unreal 原场景参考视频汇总\n\n'+f'全部汇总目录:`{DEST}`\n\n共19个真实场景、22条完整参考路线,约{mins:.2f}分钟。另有3条历史资料,单独分类。\n\n[打开汇总网页](Z:/ryli/UE_ReferenceVideos_All_20260925/index.html)\n\n`Scenes` 中每个场景一个文件夹,均为实际视频副本;同一场景不同路线全部保留。原视频未移动或修改。源与目标完整内容摘要已核对。\n\n原9月16日discard数据保持弃用,不混入。完整清单、来源、时长和校验值见汇总目录的CATALOG.csv与CATALOG.json。\n', encoding='utf-8') | |
| save(HERE / 'PROGRESS.json', {'status': 'complete', 'completed_utc': utc(), 'pid': os.getpid(), 'files': PROGRESS}) | |
| print(json.dumps({'status': 'complete', 'destination': str(DEST), 'scene_count': 19, 'scene_videos': 22, 'historical_videos': 3, 'bytes': data['total_bytes'], 'minutes': mins}, ensure_ascii=False), flush=True) | |
| if __name__ == '__main__': | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument('--execute', action='store_true') | |
| args = parser.parse_args() | |
| HERE.mkdir(parents=True, exist_ok=True) | |
| if not args.execute: | |
| data = plan() | |
| save(HERE / 'PLAN.json', data) | |
| print(json.dumps({k: v for k, v in data.items() if k not in ['rows', 'excluded']}, ensure_ascii=False)) | |
| else: | |
| data = read(HERE / 'PLAN.json') | |
| assert data['destination'] == str(DEST) | |
| assert shutil.disk_usage(DEST.parent).free > data['total_bytes'] + 1024**3 | |
| if DEST.exists(): | |
| previous = read(DEST / '_Metadata/CONSOLIDATION_PLAN.json') | |
| assert previous == data, 'Existing output belongs to a different inventory' | |
| else: | |
| DEST.mkdir() | |
| save(DEST / '_Metadata/CONSOLIDATION_PLAN.json', data) | |
| shutil.copy2(Path(__file__), DEST / '_Metadata/executed_collect_reference_videos.py') | |
| receipts = [] | |
| with ThreadPoolExecutor(max_workers=2) as pool: | |
| futures = [pool.submit(copy_one, row) for row in data['rows']] | |
| for result in as_completed(futures): | |
| receipts.append(result.result()) | |
| publish(data, receipts) | |