#!/usr/bin/env python3 # -*- coding: utf-8 -*- """科普视频成片完整技术审查(比 ffprobe 文件头检查更严格)。 完整解码所有帧并检查音频存在。该脚本不判断科学事实是否正确。 """ from __future__ import annotations import argparse,json,subprocess,sys from pathlib import Path def run(path:Path,expected_width=1080,expected_height=1920,expected_fps=30): if not path.is_file():raise FileNotFoundError(path) obj=json.loads(subprocess.check_output(['ffprobe','-v','error','-show_format','-show_streams','-of','json',str(path)],text=True)) video=next((x for x in obj['streams'] if x.get('codec_type')=='video'),None) audio=next((x for x in obj['streams'] if x.get('codec_type')=='audio'),None) assert video and audio,'Missing audio or video' actual_rate=video.get('avg_frame_rate',video.get('r_frame_rate','0/1')) numerator,denominator=map(float,actual_rate.split('/')) assert denominator and abs(numerator/denominator-expected_fps)<.12 assert video.get('width')==expected_width and video.get('height')==expected_height assert video.get('codec_name')=='h264' and audio.get('codec_name')=='aac' # Prevent writing garbage to stdout or disk: decode both streams into null sink. proc=subprocess.run(['ffmpeg','-hide_banner','-nostdin','-v','error','-xerror','-i',str(path),'-f','null','-'],capture_output=True,text=True) if proc.returncode:raise RuntimeError('Full decode failed: '+proc.stderr[-1500:]) return {'file':str(path),'duration':round(float(obj['format']['duration']),3),'fps':actual_rate,'video_codec':video['codec_name'],'audio_codec':audio['codec_name'],'whole_movie_decode_ok':True} if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('video');p.add_argument('--width',type=int,default=1080);p.add_argument('--height',type=int,default=1920);p.add_argument('--fps',type=int,default=30);a=p.parse_args() try: report=run(Path(a.video),a.width,a.height,a.fps) print(json.dumps(report,ensure_ascii=False,indent=2)) except Exception as e: print(json.dumps({'qa_pass':False,'error':str(e)},ensure_ascii=False));sys.exit(2)