#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Structural evidence and diagram QA gate. Humans must still verify real physics.""" import argparse,json,re,sys,xml.etree.ElementTree as ET from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def run(strict=True): d=json.loads((ROOT/'story.json').read_text(encoding='utf-8')) errors=[];warnings=[] cl=d.get('claims',[]) if not cl:errors.append('缺少科学主张及对应证据 claims') ids=set() for c in cl: key=c.get('id') if not key or key in ids:errors.append('科学主张 ID 不唯一或为空') ids.add(key) if not c.get('statement'):errors.append(f'{key}: 主张内容不能为空') if not c.get('sources') or any(not re.match(r'^https?://',x) for x in c.get('sources',[])): errors.append(f'{key}: 至少一条可核实的 HTTP(S) 来源') if not c.get('diagram_truth'):errors.append(f'{key}: 必须列明示意图不容颠倒的科学关系') if c.get('source_checked') is not True: warnings.append(f'{key}: 来源尚未由制作者人工/浏览器核对;不得标记已审稿') covered=set() scenes=d.get('scenes',[]) if len(scenes)<3:errors.append('科普视频至少 3 幕:现象、原理、总结') for i,sc in enumerate(scenes): sceneid=sc.get('id',f'#{i}') links=sc.get('claim_ids',[]) if not links:errors.append(f'{sceneid}: 缺少 claim_ids') for link in links: if link not in ids:errors.append(f'{sceneid}: 未知科学主张 {link}') covered.add(link) art=sc.get('diagram_svg') or sc.get('image') if not art: errors.append(f'{sceneid}: 缺少 diagram_svg/image,不能仅凭标题渲染') elif sc.get('diagram_svg'): loc=(ROOT/sc['diagram_svg']).resolve() if not loc.exists():errors.append(f'{sceneid}: SVG 不存在:{sc["diagram_svg"]}') else: txt=loc.read_text(encoding='utf-8') if 'data-placeholder="true"' in txt: errors.append(f'{sceneid}: SVG 还是占位画面,禁止发布') try: ET.fromstring(txt) except ET.ParseError: errors.append(f'{sceneid}: SVG 语法损坏') if not sc.get('visual_truth'): errors.append(f'{sceneid}: 缺少物理关系/方向/颜色/空间约束 visual_truth') if len(sc.get('heading',''))>22:warnings.append(f'{sceneid}: 标题过长,检查竖屏适配') if len(sc.get('subtitle',''))>36:warnings.append(f'{sceneid}: 说明文字过长,易与字幕重叠') for key in ids-covered: warnings.append(f'{key}: 尚未在分镜中展示该科学主张') v=d.get('voices',{}) for role,cfg in v.items(): if not isinstance(cfg,dict) or not cfg.get('speaker'):errors.append(f'{role}: 声音配置不完整') report={'science_gate_pass':not errors and (not strict or not warnings), 'hard_errors':errors,'human_review_pending':warnings, 'scientific_truth_manually_verified':False, 'claims':len(cl),'scenes':len(scenes), 'note':'本脚本仅检查证据链和示意图规格,不能自动证明光路或公式正确。'} (ROOT/'output').mkdir(exist_ok=True) (ROOT/'output/science-audit.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8') print(json.dumps(report,ensure_ascii=False,indent=2)) return 0 if report['science_gate_pass'] else 2 if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('--allow-unreviewed',action='store_true',help='Only structural drafting; never count as science approval');a=p.parse_args() sys.exit(run(strict=not a.allow_unreviewed))