master
John Lauer Preserve BQ independent routing, narration and native copper-retention evidence a4b7711 1mo ago
from pathlib import Path
import json,subprocess,os,shutil
from PIL import Image,ImageDraw,ImageFont
P=Path(__file__).resolve().parent;T=P/'take-02';E=P/'edit';E.mkdir(exist_ok=True)
raw=next(T.glob('window*.mp4'));old=P.parent/'video/fusion-bq25792-astra-3x.mp4'
font='/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf'
def panel(name,title,lines,footer):
 im=Image.new('RGBA',(1920,1080),(14,21,31,255));im.paste((0,0,0,0),(0,120,1320,1000));d=ImageDraw.Draw(im)
 d.text((38,28),title,font=ImageFont.truetype(font,40),fill='white')
 y=160
 for line in lines:
  d.text((1350,y),line,font=ImageFont.truetype(font,25),fill='#c8e0ed');y+=44
 d.text((35,1020),footer,font=ImageFont.truetype(font,24),fill='#bcd4df');im.save(E/name)
stages=[
(9.5,13,14,'TOP COPPER | LAYER 1',['Native PGND fill','About 76% copper','','POLYGON + RATSNEST','ISOLATE 0.1 mm','Larger rules still apply'],"Now the objective changes: retain copper for ablation. Fusion fills the top layer around the routed nets. The requested isolation is point one millimeter, with larger design rules still applying."),
(17.5,21,14,'INNER COPPER | LAYER 2',['Native PGND fill','About 89% copper','','Explicit layer selection','Solid native polygons','Exported fill contours'],"The first inner layer retains about eighty-nine percent copper. These are native filled polygons, measured from the exported contours, combined with the existing pads and routes."),
(25.5,29,14,'INNER COPPER | LAYER 15',['Native PGND fill','About 84% copper','','Signal clearances retained','No external autorouter','Ground fill around routes'],"The second inner layer reaches about eighty-four percent. Ground occupies available space while other-net clearances remain. This is a copper-retention experiment, with electrical qualification still to follow."),
(32.5,37,14,'BOTTOM COPPER | LAYER 16',['Native PGND fill','About 82% copper','','612 trace segments','60 vias','Four copper pours'],"The bottom layer reaches about eighty-two percent. Across four layers, the original independently calculated routes remain: six hundred twelve segments and sixty vias, now surrounded by four pours."),
(40.5,44,10,'FLOATING COPPER | ORPHANS OFF',['Native comparison','Disconnected fill removed','','CHANGE ORPHANS OFF','RATSNEST','Zoomed top layer'],"Here orphan retention is off. Fusion removes disconnected pieces of the pour. The close-up makes the empty spaces easier to compare."),
(47.5,52,17,'FLOATING COPPER | ORPHANS ON',['User-authorized retention','Disconnected fill retained','','CHANGE ORPHANS ON','RATSNEST','','Never bridge isolation','or prohibited keepouts'],"Turning orphan retention on keeps additional disconnected copper, as requested for this manufacturing experiment. These islands are physically floating; a polygon net label does not make them electrically grounded. Isolation barriers still take priority."),
(65,70,24,'MEASURED RESULT | ENGINEERING LIMITS',['About 83% copper retained','About 83% less removal area','Compared with trace-only','','0 airwires / 0 warnings','0 new DRC errors','1 inherited error remains','','Area estimate, not laser time','0.1 mm is a process floor'],"The estimate is about eighty-three percent retained copper, and eighty-three percent less area to remove than the trace-only board, excluding drill holes. Native checks show no airwires, no warnings, and no new errors. One inherited error remains. Point one millimeter is a process limit, not a high-voltage safety rating.")]
manifest=[];clips=[]
# Preserve previous real routing footage, stopping before its obsolete trace-only end card.
first=E/'00-routing.mp4'
if not first.exists():subprocess.run(['ffmpeg','-v','error','-i',str(old),'-t','134.333333','-an','-vf',"drawbox=x=0:y=0:w=1920:h=78:color=0x0e151f:t=fill,drawtext=fontfile="+font+":text='FUSION + ASTRA | REROUTE, THEN RETAIN COPPER':x=35:y=20:fontsize=37:fontcolor=white",'-c:v','libx264','-preset','fast','-crf','21','-threads','4','-y',str(first)],check=True)
clips.append(first)
start=134.333333;cues=json.loads((P.parent/'narration/script.json').read_text())[:7]
for c in cues:c['file']=str(P.parent/'narration'/c['file'])
for i,(a,b,dur,title,lines,speech) in enumerate(stages):
 png=f'panel-{i}.png';panel(png,title,lines,'Native Fusion recording | action 3x; inspection holds | waits removed | edited explanatory captions')
 clip=E/f'{i+1:02}.mp4';flt=f'[0:v]setpts=(PTS-STARTPTS)/3,crop=1970:1400:0:100,scale=1238:880,pad=1320:880:0:0:color=0x0e151f,pad=1920:1080:0:120:color=0x0e151f,tpad=stop_mode=clone:stop_duration={dur}[v];[v][1:v]overlay=0:0,fps=30,format=yuv420p[out]'
 subprocess.run(['ffmpeg','-v','error','-ss',str(a),'-t',str(b-a),'-i',str(raw),'-i',str(E/png),'-filter_complex',flt,'-map','[out]','-t',str(dur),'-an','-c:v','libx264','-preset','fast','-crf','21','-threads','4','-y',str(clip)],check=True);clips.append(clip)
 txt=E/f'voice-{i}.txt';mp=E/f'voice-{i}.mp3';txt.write_text(speech)
 if not mp.exists():subprocess.run(['adom-tts','say','@'+str(txt),'--out',str(mp),'--voice','en-US-AndrewNeural','--rate','+0%','--thread','Fusion trace routing'],env={**os.environ,'ADOM_TTS_NO_HINTS':'1'},check=True)
 cues.append(dict(start=start+.2,end=start+dur-.15,text=speech,file=str(mp)));manifest.append(dict(source=str(raw),start=a,end=b,speed=3,outputDuration=dur,inspectionHold=True,title=title));start+=dur
# Five-second hold after narration.
last=clips[-1];hold=E/'99-hold.mp4';subprocess.run(['ffmpeg','-v','error','-sseof','-0.1','-i',str(last),'-vf','tpad=stop_mode=clone:stop_duration=5','-t','5','-an','-c:v','libx264','-preset','fast','-crf','21','-threads','4','-y',str(hold)],check=True);clips.append(hold);start+=5
(E/'concat.txt').write_text(''.join("file '"+str(f)+"'\n" for f in clips));out=E/'fusion-bq25792-astra-copper-retention-silent.mp4';subprocess.run(['ffmpeg','-v','error','-f','concat','-safe','0','-i',str(E/'concat.txt'),'-c','copy','-movflags','+faststart','-y',str(out)],check=True)
(E/'script.json').write_text(json.dumps(cues,indent=2));(E/'edit.json').write_text(json.dumps(dict(duration=start,priorRoutingEnd=134.333333,stages=manifest),indent=2));print('FINISHED',start,flush=True)