"""기존 실행기로 세 대표 컷의 PNG/MP4를 만든다. 전 컷 제작은 하지 않는다.""" import argparse import json from pathlib import Path import subprocess from PIL import Image, ImageChops ROOT = Path(__file__).resolve().parents[3] BASE = Path('projects/water-hydrogen-bond-surface-tension/stages/08-scenes/r2') BLENDER = 'C:/Program Files/Blender Foundation/Blender 5.2/blender.exe' PYTHON = 'C:/Users/baram/anaconda3/python.exe' COMPOSITOR = Path('C:/git/ShortVideoGenerator/projects/water-hydrogen-bond-surface-tension/renderer/node_modules/@remotion/compositor-win32-x64-msvc') SOURCE = (BASE/'generated-source/preview.blend').as_posix() ARTIFACT = (BASE/'artifact.json').as_posix() PROJECT = 'projects/water-hydrogen-bond-surface-tension/project.json' PLAN = [('c07','BondPair','PairA',1017,1219,1219),('c16','Interface','WaterCutawayBody',2806,3050,3050),('c18','DropletStretch','Droplet',3172,3372,3372)] def run(args, records): result=subprocess.run(args,cwd=ROOT,text=True,encoding='utf-8',capture_output=True) records.append(dict(command=args,exit_code=result.returncode)) (ROOT/BASE/'cli-commands.json').write_text(json.dumps(records,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') if result.returncode: print(result.stdout,result.stderr,flush=True) raise RuntimeError('실행 실패; 명령 기록과 기존 시도 보존') parsed=json.loads(result.stdout) records[-1]['source_sha256']=parsed['scene_file']['sha256'] records[-1]['scene_name']=parsed.get('scene_name') if 'media' in parsed: records[-1]['media']=parsed['media'] records[-1]['evidence_path']=(Path(parsed['media']['path']).parent/'evidence.json').as_posix() (ROOT/BASE/'cli-commands.json').write_text(json.dumps(records,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') print('EXIT_0',args[5],parsed.get('scene_name'),flush=True) return parsed if __name__=='__main__': parser=argparse.ArgumentParser() parser.add_argument('--kind',choices=['image','video'],required=True) args=parser.parse_args() record_path=ROOT/BASE/'cli-commands.json' records=json.loads(record_path.read_text(encoding='utf-8')) if record_path.exists() else [] for record in records: record.pop('stdout',None) record.pop('stderr',None) artifact=json.loads((ROOT/ARTIFACT).read_text(encoding='utf-8')) for cut,scene,obj,start,end,boundary in PLAN: assert not any(s['id']==cut+'-'+args.kind for s in artifact['payload']['preview']['samples']), '이미 반영한 sample을 같은 버전에서 재생성하지 않는다' common=[PYTHON,'-B','-X','utf8','tools/blender/run.py'] inspect=run(common+['inspect','--blender',BLENDER,'--source',SOURCE,'--scene',scene,'--require-object',obj],records) artifact['payload']['preview']['scene_file']=inspect['scene_file'] (ROOT/ARTIFACT).write_text(json.dumps(artifact,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') options=['--frame',str(boundary)] if args.kind=='image' else ['--start',str(start),'--end',str(end)] rendered=run(common+['render','--mode','preview','--project',PROJECT,'--artifact',ARTIFACT,'--output-revision','2','--blender',BLENDER,'--source',SOURCE,'--scene',scene,'--require-object',obj,'--cut-id',cut,'--ffmpeg',str(COMPOSITOR/'ffmpeg.exe'),'--ffprobe',str(COMPOSITOR/'ffprobe.exe')]+options,records) # 실행기의 실제 증거만 미디어 메타데이터로 사용한다. evidence=rendered if args.kind=='video': frames=[ROOT/Path(evidence['media']['path']).parent/f'frame-{f:08}.png' for f in (start,start+(end-start)//2,end-1)] pixels=[Image.open(f).convert('RGB') for f in frames] if not (ImageChops.difference(pixels[0],pixels[1]).getbbox() and ImageChops.difference(pixels[0],pixels[2]).getbbox()): (ROOT/Path(evidence['media']['path']).parent/'visual-failure.json').write_text(json.dumps(dict(error='실제 렌더 프레임이 정지함; 미디어 반영 금지',logical_frames=[start,start+(end-start)//2,end-1]),ensure_ascii=False,indent=2)+'\n',encoding='utf-8') raise RuntimeError('실제 렌더 프레임이 정지함; 미디어 반영 금지') artifact['payload']['preview']['samples'].append(dict(id=cut+'-'+args.kind,cut_ids=[cut],media=evidence['media'])) (ROOT/ARTIFACT).write_text(json.dumps(artifact,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') print('READY',cut,args.kind,evidence['media']['path'],flush=True)