master
John Lauer Add bounded obstacle planning and live calculations using the Adom activity palette e936bc3 1mo ago
import sys,subprocess,json
from pathlib import Path
import shutil
F=shutil.which('ffmpeg');root=Path(__file__).parent
HEADER='''[Script Info]
ScriptType: v4.00+
PlayResX: 1920
PlayResY: 1080
WrapStyle: 2
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Main,Liberation Sans,34,&H00FFFFFF,&H00FFFFFF,&H00130F0A,&H00130F0A,0,0,0,0,100,100,0,0,1,1,0,2,30,30,22,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
'''
def stamp(t):
 cs=round(t*100);return f'{cs//360000}:{cs//6000%60:02d}:{cs//100%60:02d}.{cs%100:02d}'
def render(name,source,start,end,cues,side,title,hold=4,keep=None,action_seconds=None,frame_ranges=None,capture_note="Idle waits trimmed"):
 duration=(action_seconds if action_seconds is not None else (sum(b-a for a,b in keep) if keep else end-start)/3)+hold
 events=[]
 def text(t0,t1,s):events.append(f'Dialogue: 0,{stamp(t0)},{stamp(t1)},Main,,0,0,0,,{s}')
 text(0,duration,r'{\an7\pos(34,16)\fs38\b1\c&H00E5CF65&}CODEX ASTRA IN ADOM HYDROGEN'+r'{\c&H00FFFFFF&}  /  '+title)
 text(0,duration,r'{\an7\pos(1490,125)\fs48\b1\c&H00E5CF65&}3x SPEED')
 text(0,duration,r'{\an7\pos(1490,197)\fs28}Native Fusion capture\N'+capture_note)
 for y,heading,body in side:
  text(0,duration,fr'{{\an7\pos(1490,{y})\fs32\b1\c&H00E5CF65&}}'+heading)
  text(0,duration,fr'{{\an7\pos(1490,{y+46})\fs27}}'+body)
 for a,b,s in cues:text(a,b,r'{\an2\pos(960,1058)\fs34}'+s)
 text(duration-hold-.2,duration,r'{\an7\pos(1490,885)\fs23\c&H00B9B9B9&}Final frame held for reading')
 ass=root/(name+'.ass');ass.write_text(HEADER+'\n'.join(events)+'\n')
 out=root/(name+'.mp4')
 selection=("select='"+'+'.join(f'between(t,{a},{b})' for a,b in keep)+"',setpts=N/(30*TB)" if keep else f'trim=start={start}:end={end}')
 if frame_ranges is not None:
  selection="select='"+'+'.join(f'gte(n,{a})*lt(n,{b})' for a,b in frame_ranges)+"',setpts=N/(30*TB)"
 vf=f'{selection},setpts=(PTS-STARTPTS)/3,fps=30,scale=1440:900:force_original_aspect_ratio=decrease,pad=1440:900:(ow-iw)/2:(oh-ih)/2:color=0x0a0f13,pad=1920:1080:0:70:color=0x0a0f13,tpad=stop_mode=clone:stop_duration={hold},ass={ass}'
 subprocess.run([F,'-hide_banner','-loglevel','warning','-i',str(source),'-vf',vf,'-an','-c:v','libx264','-preset','fast','-crf','20','-pix_fmt','yuv420p','-movflags','+faststart','-y',str(out)],check=True)
 (root/(name+'.edit.json')).write_text(json.dumps({'source':str(source),'trimStartSeconds':start,'trimEndSeconds':end,'speed':3,'holdSeconds':hold,'durationSeconds':duration,'captions':cues,'keptSourceRanges':keep,'keptFrameRangesEndExclusive':frame_ranges},indent=2))
 print(out,flush=True)

if __name__=='__main__':
 import argparse
 p=argparse.ArgumentParser();p.add_argument('take',type=Path);a=p.parse_args()
 source=next(a.take.glob('window-*.mp4'))
 # select + N/(30*TB) removes gaps correctly only for verified constant-30fps input.
 frames=json.loads(subprocess.check_output(['ffprobe','-v','error','-select_streams','v:0','-show_entries','frame=best_effort_timestamp_time','-of','json',str(source)]))['frames']
 pts=[float(f['best_effort_timestamp_time']) for f in frames]
 assert all(abs((b-a)-1/30)<.00001 for a,b in zip(pts,pts[1:])), 'VFR source needs trim/concat'
 keep=json.loads((a.take/'action-ranges.json').read_text())
 final=json.loads((a.take/'final.json').read_text())
 assert final['segmentCount']==120 and final['viaCount']==48 and len(final['pads'])==72 and not final['remainingNets'] and final['drc']['clean']
 selected=[i for i,t in enumerate(pts) if any(lo<=t<=hi for lo,hi in keep)]
 frame_ranges=[]
 for i in selected:
  if frame_ranges and i==frame_ranges[-1][1]:frame_ranges[-1][1]=i+1
  else:frame_ranges.append([i,i+1])
 retained=len(selected)
 action=retained/90;duration=action+5
 cues=[
  (0,4,'Codex Astra calculates the 24-net crossover in Adom Hydrogen.'),
  (4,8,'Output column j = (7 x i) mod 24. Rows and columns use 2.54 mm pitch.'),
  (8,12,r'Top copper carries rows; bottom copper carries columns.\NCrossings stay on separate layers. No Freerouting.'),
  (12,16,r'fusion_routing_state reads nets, pads and revision.\Nfusion_route_net dryRun checks explicit planned geometry.'),
  (16,20,r'fusion_route_net adds each trace and via through Adom Bridge.\NexpectedRevision guards edits; copper is read back after each call.'),
  (20,action,r'Each net branches to a third pad and uses two through vias.\NFusion recalculates connectivity as the copper appears.'),
  (action,duration,r'fusion_routing_drc: 24 nets connected.\NNative Autodesk DRC: 0 errors / 0 warnings / 0 airwires.')]
 # Keep captions legible if the actual action is shorter than the template.
 if action<23:
  factor=action/24
  cues=[(x*factor,y*factor,t) for x,y,t in cues[:-2]]+[(20*factor,action,cues[-2][2]),(action,duration,cues[-1][2])]
 render('fusion-crossbar-24-3x',source,0,pts[-1]+1/30,cues,
  [(305,'THE BOARD',r'24 nets / 72 pads\N120 trace segments\N48 through vias'),
   (495,'THE CALCULATION',r'j = (7 x i) mod 24\N0.30 mm trace width\NExplicit Astra waypoints'),
   (685,'RECORD + VERIFY',r'Native AB window capture\NFusion copper read-back\NAutodesk DRC result')],
  '24-NET CROSSOVER',hold=5,keep=keep,action_seconds=action,frame_ranges=frame_ranges)