← Commit history
docs/method.mdadded+51
@@ -0,0 +1,51 @@+# How the measurements work++For anyone changing the copper tracing or the router. Each section gives what was tried, what happened and why the code is the way it is.++## Reading copper through solder mask++Copper under solder mask reads lighter than bare laminate, but only against its own neighbourhood, because lighting varies across a photo. `trace.rs` divides a small average of the green channel (box radius 3 px at 40 px/mm) by a wide one (radius 120), using only mask pixels in both. Below about 0.9 is laminate. On the Driver photos this map reads almost like a copper plot.++Thin gaps between pieces of copper are blurred away by that average. So an oriented dark-line filter runs too: a Gaussian along the line (sigma 9 px), the second derivative of a Gaussian across it (sigma 2.6 px), at 16 angles. Grain cancels along the line, and a real gap survives. Its thresholds are fractions of the photo's 99th percentile response, so they follow the contrast of each photo.++Laminate plus gap lines is the barrier. Its skeleton's loose ends are walked on along their direction until they meet another line, which closes the small breaks. Everything left between barriers is an island. Silk over copper does not separate it, so silk stays passable; part bodies and pads do, because the copper under them cannot be seen.++Tried first, and abandoned:++- **Long straight gap lines with splitting at necks.** It found pours only roughly, and no tracks at all.+- **Merging over-segmented regions by boundary strength.** One leak in the gap network merges pours, and noise makes false cells.+- **Classifying material by hue.** Grain makes dark mask pixels read any hue, so most of the board was thrown away. `not_mask` says only what is definitely not mask.++Check any change by eye against `evidence/copper-islands-*.jpg`.++## From islands to a board++`islands.rs` draws an island as a pour if it is wide (half width at least 0.4 mm, area at least 0.8 mm²), and as a track if it is thin.++Pours carry their exact copper as runs of pixels in `rle`, as well as an outline. An outline cannot describe them: holes join up through the gap network into one component whose boundary is nearly the whole board, and subtracting that erases the largest pour.++KiCad needs each fill as a single polygon with its holes joined in by slits. `kicad_out::keyhole` does this left to right so slits never cross, and winds each hole against its outline. KiCad's triangulation reads a hole wound the same way as copper, and an even-odd check will not catch that.++Junk is filtered after drawing: short tracks of unknown net, tracks running into a foreign pad or onto a longer track of another net, vias on pads and overlapping vias.++## What connects to what++A pad joins an island when the copper around it is that island and no closed gap stands between them. `gap_share` takes the darkest ring around the pad. **This is the weakest measurement.** On the Driver photos it does not reliably tell a plated pad in a pour from one isolated in it, so disagreements with the nets so far are reported in `detect/copper-measured.json`, not applied. An inferred pin keeps its net, and the fill clears around it.++An island takes the net that clearly dominates the pins it touches, large pads counting for more. A screw lug decides by itself, since it is bolted to its pour. Nets cross vias to unnamed islands: one via is enough for a small island, whose only way off a face is a via, while two pours need at least two vias between them. Two different nets meeting at a via are reported and never merged, since a leak in the photo must not short two nets.++Vias tent over on the top face of the Driver board, so a bottom dome counts on its own. Top domes are too noisy, and count only where the bottom is hidden or they pair up. A via whose ring is isolated on every face it can be seen on leads nowhere visible. The program treats that as a candidate for an inner layer and starts routing from the layer count it implies. Check candidates by eye: on the Driver board all 3 look like ordinary domes in a stitching field, so they prove nothing.++## The router++`route.rs` works on a 0.05 mm grid per copper layer. Every piece of copper stamps a keep-out for the centre line of the narrowest track: the copper's own shape, plus the clearance, plus half a track. A cell holds a count, a sum and a sum of squares of the net ids stamped on it. All three agree only when every stamp is the same net, and subtracting a stamp restores the cell exactly. A wider track or a via tests a ring around the cell. This is exact, which is why the router's own copper produces almost no DRC errors.++Only what cannot move blocks: pads, holes, vias seen on the board and the board edge. Seen copper of another net costs extra to cross, six times its length if its net is unknown and twelve if known. Treating pours as walls left 235 of 245 connections open, because the copy tiles nearly all of both outer faces. A real via passes through a pour and the pour clears around it, and the fill here is computed after routing, so it clears too. The length that still crosses seen copper is reported, because it measures how far each stackup departs from the photo.++A connection that finds no path is searched again with routed copper passable at a cost. The connections on that path are ripped up, along with whatever landed on them, and queued again; the congested tiles get dearer. When passes stop improving, that stackup is the limit and the caller jumps two layers.++Inner layers are planes or signal layers. A plane net needs only a via per pin. On a stackup of n layers, the first plane net sits next to the top, the second next to the bottom, and everything between is routable. So 4 layers has no inner signal layer, and 6 has two.++## Why not AI Flow's router++The first version of this flow called `adom-aiflow route`. It routes signals only on the outer two layers, so more layers could not close anything. It also does not treat seen pours as occupied: on the Driver board, 495 of 505 new DRC errors at 4 layers involved a zone, with vias placed straight through phase pours. The timeline page is still AI Flow's.
docs/process-video/README.mdadded+76
@@ -0,0 +1,76 @@+# Process video: how it was made++The video was built after the run from what the run saved. Nothing was filmed live, so every shot below is a saved file, a recorded view or a drawing over one. It covers the Driver board test and runs 5 minutes 22 seconds.++## What differs from the AI Flow video standard++- **No clip review.** The standard asks for review of each clip before assembly. This one was assembled directly, because the final was asked for first.+- **The review scene shows the run's record.** It says vias are medium confidence, as the fourth pass recorded. The vias scene before it shows why that level is too high.++## Where each scene comes from++| Scene | Source |+|---|---|+| The request | Typed on screen from the saved prompt text. This is a re-enactment; the typing was not filmed. |+| A new program | Screenshot of the flow's timeline page, served from the finished run. |+| Photos | The four original photos, then evidence/outline-*.jpg, then the straightened board images. |+| Scale | board/top.jpg with the measured holes and rows from detect/holes-top.json drawn over it. |+| Parts | board/top.jpg with positions from parts.json, and evidence/pin1-U2.jpg. |+| Blocks | board/top.jpg with block outlines drawn from blocks.json and parts.json. |+| The miss | The bottom photo beside recorded view 73 (the sketched copy), with the quote from the session. |+| Reading the copper | Bottom photo, then two images regenerated for the video with the formula in trace.rs (the run's own debug images predate the rewrite), then evidence/copper-islands-*.jpg. |+| Vias | board/bottom.jpg with vias from copper.json. The red marks come from a screen run for the video, not from the flow. |+| The copy | Recorded view 81 in the flow window, then evidence/overlay-*.jpg. |+| Nets | Recorded schematic view 101. |+| The router | Recorded views 82 and 97, with a chart of all 17 runs from views.json. |+| Layer count | Recorded views 96, 97 and 98 with only inner copper showing, brightened for legibility, and the table from layers.json. |+| Rules | Recorded view 97 and the JLC experiment figures from layers.json. |+| Review and tests | Confidence levels from review.json, as the fourth pass recorded them, and test rows from tests.json. |+| Not done | Recorded views 97 and 101 beside the bottom photo. |+| The record | The flow window stepping through its recorded views. |+| Cost | Counted from the session log, 20:34 to 07:08 UTC. Gaps over 15 minutes are cut, which also removes some background router time. |++## Tools++Voice: `adom-tts` with `en-US-AndrewNeural`, the standard Adom narration voice, at the default rate. Stings: `dark-intro-16x9.mp4` and `dark-outro-16x9.mp4` from [adom/adom-video-brand](https://wiki.adom.inc/adom/adom-video-brand). Picture: one canvas page per scene, rendered frame by frame in headless Chrome and encoded with the ffmpeg from `imageio_ffmpeg`. Captions are separate files, not burned in.++The scripts are in [build/](build): `script.py` holds the narration, `composer.html` draws each scene, `render.js` renders them, `assemble.py` joins them and writes the captions. They use paths from the session that built them, so expect to adjust those.++## Voice-over script++**The request.** It starts with four photos of a motor driver board, and one request. Find the parts, the functional blocks, the pours and the routing. Copy everything you can see. Then work out what you can't, pick a layer count, and say how sure you are.++**A new program.** The first attempt reused the existing AI Flow program, and Caleb called that out. So this became its own Rust program, with its own copper reader and its own router. The program does the measuring. The AI does the naming. Eleven steps, and each one records what it produced.++**Photos.** Two photos of the top, two of the bottom. The program finds the board edge in each one and straightens it. Then it turns the bottom over to line up with the top. Thirty eight holes agree, two don't.++**Scale.** Scale comes from the board itself. The header rows have known pitches, two millimeters and two point five four. That gives forty six point six pixels per millimeter. The driver chip's six millimeter body settles which pitch standard the rows are.++**Parts.** Then the parts, a hundred and thirty seven of them. A hundred and three are placed by their own pads on the photo. Markings get read where they're legible, like this T I DRV eighty three twenty three S. Where they aren't, the program says so. Ninety one parts still need a test.++**Blocks.** Silkscreen and circuit role sort them into eleven blocks. Power entry, the three phase power stage, bridge sensing, the logic supply. The gate driver, the microcontroller, CAN, aux, the fan, motor temperature and the mounting holes.++**The miss.** Then came the real miss. The first copy had fifty eight pours, all on the bottom, and not a single track. Caleb looked at it and said fill and via recognition was completely lacking. That was correct.++**Reading the copper.** So the reader was rewritten. Under solder mask, copper looks a little lighter than bare laminate, compared with its own neighborhood. Thin dark lines mark the gaps between copper. Follow those gaps and the board falls apart into islands. One thousand four hundred forty four of them. That became a hundred sixteen pours and three hundred fourteen tracks, on both faces.++**Vias.** Vias are next. Two thousand six hundred eighty one candidates, and one thousand twenty eight kept. They're tented on the top, so only forty five show on both faces. Zoom in and the misses show. Up to a third land on something pale, mostly silkscreen letters and lines. And the three read as leading only inward look like ordinary domes in a stitching field. So that's no proof of inner layers.++**The copy.** All of that goes straight into a KiCad board, as photographed, before anything is guessed. Here's the copy laid over the top photo, and over the bottom.++**Nets.** Now for what nobody can see. Datasheets and the circuit fill in the rest of the nets. Seventy seven nets, two hundred forty nine connections to make. Ninety nine pins disagree with the copper they seem to touch. Those get reported, not quietly fixed.++**The router.** Then the router. It's written for this job, and it uses every inner layer. When photographed pours were hard walls, two hundred thirty five connections stayed open. Real vias pass through pours, so seen copper became expensive to cross instead of forbidden. Seventeen recorded runs later, six and eight layers are down to about thirty.++**Layer count.** The layer count comes from how hard that is. Four layers: fifty four open, and three hundred twenty seven millimeters cut through copper the photos show. Six layers: thirty four open, twenty four millimeters. Eight layers: twenty nine open, thirty millimeters. That's no real gain, since runs vary by about a quarter. So the proposal is six. It's a measure of difficulty. Nobody has seen inside this board.++**Rules.** The rules were set from the start: KiCad's defaults, point two millimeter track and clearance. Most of what's left open is fan-out at the two Q F N chips. The same copy at J L C style rules, point one two seven, leaves just two open at six layers. So that's mostly a rules limit. That run has no valid design rule check yet.++**Review and tests.** Then it reviews itself, in a loop. Four passes, and the last one changes nothing. Blocks and placement come out high. Identity, values, copper, vias and layers, medium. Connectivity is low, and it says why. Everything still unknown gets a test. A hundred and nineteen of them.++**Not done.** So this is not a finished board. Thirty four connections stay open. KiCad counts seventy six missing, because some seen copper isn't joined in the copy. The rails at the upper left of the bottom are largely missing, and signal bundles mostly read as gaps. And the inner copper is one way to connect it, not what's actually inside.++**The record.** Every stage is saved in the flow window. A hundred and ten recorded steps you can replay.++**Cost.** From the first prompt to the report took about six and a half hours of AI time, with idle gaps over fifteen minutes cut out. Nine hundred fifteen thousand output tokens, and two hundred seventy two million cache reads, across four hundred sixty three model calls. Next up: real measurements on this board, and a second board, before any of it goes public.+
docs/process-video/build/assemble.pyadded+74
@@ -0,0 +1,74 @@+import json,subprocess,imageio_ffmpeg,re+from script import SCENES+F=imageio_ffmpeg.get_ffmpeg_exe()+T=json.load(open('vo/timing.json'));D={d['id']:d['d'] for d in json.load(open('clips/durs.json'))}+INTRO,VOIN,LEAD,OUTRO=5.0,1.8,.1,12.0+WR={ # written form of each spoken sentence+'prompt':None,'flow':None,'photos':["Two photos of the top, two of the bottom.","The program finds the board edge in each one and straightens it.","Then it turns the bottom over to line up with the top. 38 holes agree, 2 don't."],+'scale':["Scale comes from the board itself. The header rows have known pitches, 2 mm and 2.54 mm.","That gives 46.6 pixels per millimeter.","The driver chip's 6 mm body settles which pitch standard the rows are."],+'parts':["Then the parts, 137 of them.","103 are placed by their own pads on the photo.","Markings get read where they're legible, like this TI DRV8323S.","Where they aren't, the program says so. 91 parts still need a test."],+'blocks':["Silkscreen and circuit role sort them into 11 blocks.","Power entry, the three-phase power stage, bridge sensing, the logic supply.","The gate driver, the microcontroller, CAN, AUX, the fan, motor temperature and the mounting holes."],+'miss':["Then came the real miss. The first copy had 58 pours, all on the bottom, and not a single track.","Caleb looked at it and said fill and via recognition was completely lacking.","That was correct."],+'copper':["So the reader was rewritten. Under solder mask, copper looks a little lighter than bare laminate, compared with its own neighborhood.","Thin dark lines mark the gaps between copper.","Follow those gaps and the board falls apart into islands. 1,444 of them.","That became 116 pours and 314 tracks, on both faces."],+'vias':["Vias are next. 2,681 candidates, and 1,028 kept.","They're tented on the top, so only 45 show on both faces.","Zoom in and the misses show. Up to a third land on something pale, mostly silkscreen letters and lines.","And the 3 read as leading only inward look like ordinary domes in a stitching field. So that's no proof of inner layers."],+'copy':None,+'nets':["Now for what nobody can see. Datasheets and the circuit fill in the rest of the nets. 77 nets, 249 connections to make.","99 pins disagree with the copper they seem to touch. Those get reported, not quietly fixed."],+'router':["Then the router. It's written for this job, and it uses every inner layer.","When photographed pours were hard walls, 235 connections stayed open.","Real vias pass through pours, so seen copper became expensive to cross instead of forbidden.","17 recorded runs later, 6 and 8 layers are down to about 30."],+'layers':["The layer count comes from how hard that is.","4 layers: 54 open, and 327 mm cut through copper the photos show.","6 layers: 34 open, 24 mm.","8 layers: 29 open, 30 mm. That's no real gain, since runs vary by about a quarter.","So the proposal is 6. It's a measure of difficulty. Nobody has seen inside this board."],+'rules':["The rules were set from the start: KiCad's defaults, 0.2 mm track and clearance.","Most of what's left open is fan-out at the two QFN chips.","The same copy at JLC-style rules, 0.127 mm, leaves just 2 open at 6 layers.","So that's mostly a rules limit. That run has no valid design rule check yet."],+'review':["Then it reviews itself, in a loop. 4 passes, and the last one changes nothing.","Blocks and placement come out high. Identity, values, copper, vias and layers, medium.","Connectivity is low, and it says why.","Everything still unknown gets a test. 119 of them."],+'notdone':["So this is not a finished board.","34 connections stay open. KiCad counts 76 missing, because some seen copper isn't joined in the copy.","The rails at the upper left of the bottom are largely missing, and signal bundles mostly read as gaps.","And the inner copper is one way to connect it, not what's actually inside."],+'record':["Every stage is saved in the flow window. 110 recorded steps you can replay."],+'cost':["From the first prompt to the report took about 6.5 hours of AI time, with idle gaps over 15 minutes cut out.","914,981 output tokens, and 272 million cache reads, across 463 model calls.","Next up: real measurements on this board, and a second board, before any of it goes public."],+}+# timeline+t=INTRO;starts={};vo={}+for s in T:+    i=s['id'];starts[i]=t+    vo[i]=VOIN if i=='prompt' else t+LEAD+    t+=D[i]+outro=t;total=t+OUTRO+# concat video+for n,o in(('dark-intro-16x9','intro'),('dark-outro-16x9','outro')):+    subprocess.run([F,'-y','-v','error','-i',f'brand/media/{n}.mp4','-an','-c:v','libx264','-preset','medium','-crf','14','-pix_fmt','yuv420p','-r','30',f'clips/_{o}.mp4'],check=True)+open('clips/_list.txt','w').write("file '_intro.mp4'\n"+''.join(f"file '{s['id']}.mp4'\n" for s in T)+"file '_outro.mp4'\n")+subprocess.run([F,'-y','-v','error','-f','concat','-safe','0','-i','clips/_list.txt','-c','copy','clips/_video.mp4'],check=True)+# audio: voice bus normalized as one take, stings as issued+inp=[];fl=[]+for k,s in enumerate(T):+    inp+=['-i',f"vo/{s['id']}.wav"];fl.append(f"[{k}:a]aresample=48000,aformat=channel_layouts=mono,adelay={int(vo[s['id']]*1000)}:all=1[v{k}]")+n=len(T);fl.append(''.join(f'[v{k}]' for k in range(n))+f"amix=inputs={n}:normalize=0:dropout_transition=0,loudnorm=I=-16:TP=-1.5:LRA=11,aresample=48000,aformat=channel_layouts=stereo[vo]")+inp+=['-i','brand/media/dark-intro-16x9.mp4','-i','brand/media/dark-outro-16x9.mp4']+fl.append(f"[{n}:a]aresample=48000,volume=0.8[si]");fl.append(f"[{n+1}:a]aresample=48000,volume=0.8,adelay={int(outro*1000)}:all=1[so]")+fl.append("[vo][si][so]amix=inputs=3:normalize=0:dropout_transition=0,alimiter=limit=0.95[a]")+subprocess.run([F,'-y','-v','error',*inp,'-filter_complex',';'.join(fl),'-map','[a]','-t',f'{total:.3f}','-ar','48000','-c:a','pcm_s16le','clips/_audio.wav'],check=True)+# chapters+meta=';FFMETADATA1\ntitle=Reverse engineering a PCB from photos: Driver_RE2\n'+ch=[(s['title'],starts[s['id']]) for s in T]+for k,(ti,st) in enumerate(ch):+    en=ch[k+1][1] if k+1<len(ch) else total+    meta+=f"[CHAPTER]\nTIMEBASE=1/1000\nSTART={int((0 if k==0 else st)*1000)}\nEND={int(en*1000)}\ntitle={ti}\n"+open('clips/_meta.txt','w').write(meta)+subprocess.run([F,'-y','-v','error','-i','clips/_video.mp4','-i','clips/_audio.wav','-i','clips/_meta.txt','-map','0:v','-map','1:a','-map_metadata','2','-c:v','copy','-c:a','aac','-b:a','192k','-movflags','+faststart','clips/final-master.mp4'],check=True)+# captions, cued to the final audio+def fmt(x,sep):+    h=int(x//3600);m=int(x%3600//60);s=x%60;return f"{h:02}:{m:02}:{s:06.3f}".replace('.',sep)+cues=[]+for s in T:+    lines=WR[s['id']] or s['lines'];off=vo[s['id']]+    for i,l in enumerate(lines):+        a=off+s['starts'][i];b=off+(s['starts'][i+1] if i+1<len(lines) else s['speech'][1])+        # split long sentences on phrase boundaries, time by character share+        parts=[];cur=''+        for w in l.split(' '):+            if len(cur)+len(w)+1>78 and cur:parts.append(cur);cur=w+            else:cur=(cur+' '+w).strip()+        parts.append(cur);tot=sum(map(len,parts));p=a+        for q in parts:+            e=p+(b-a)*len(q)/tot;cues.append((p,e-.02,q));p=e+vtt='WEBVTT\n\n'+''.join(f"{fmt(a,'.')} --> {fmt(b,'.')}\n{q}\n\n" for a,b,q in cues)+srt=''.join(f"{k+1}\n{fmt(a,',')} --> {fmt(b,',')}\n{q}\n\n" for k,(a,b,q) in enumerate(cues))+open('clips/final.vtt','w').write(vtt);open('clips/final.srt','w').write(srt)+open('clips/final-chapters.txt','w').write(''.join(f"{int(st//60)}:{int(st%60):02} {ti}\n" for ti,st in [('Intro',0)]+ch))+json.dump(dict(total=total,outro=outro,starts=starts,vo=vo,cues=len(cues)),open('clips/_tl.json','w'),indent=1)+print('total',round(total,2),'outro at',round(outro,2),'cues',len(cues))
docs/process-video/build/cap3.jsadded+38
@@ -0,0 +1,38 @@+const p=require(process.env.HOME+'/.cache/hero-studio/node_modules/puppeteer-core');+const sleep=ms=>new Promise(r=>setTimeout(r,ms));+const U='http://localhost:8891/';+(async()=>{const b=await p.launch({headless:'new',executablePath:process.env.HOME+'/.cache/puppeteer/chrome/linux-150.0.7871.24/chrome-linux64/chrome',args:['--no-sandbox','--disable-gpu'],protocolTimeout:600000});+// ---- flow window, 1x+let pg=await b.newPage();await pg.setViewport({width:1920,height:1080});await pg.goto(U,{waitUntil:'load'});await sleep(6000);+const clickN=async n=>{await pg.evaluate(n=>{const s=[...document.querySelectorAll('.n')].find(e=>e.textContent.trim()==='#'+n);let e=s;for(let i=0;i<3&&e.parentElement&&!e.onclick&&e.tagName!=='BUTTON';i++)e=e.parentElement;(s.closest('button,[data-n],.rec,.item,li')||e).scrollIntoView({block:'center'});(s.closest('button,[data-n],.rec,.item,li')||e).click()},n)};+const tab=async t=>{await pg.evaluate(t=>{[...document.querySelectorAll('button')].find(e=>e.textContent.trim()===t).click()},t)};+for(const n of [6,10,15,73,81,96,97,98,101]){await clickN(n);await sleep(n>=76?12000:7000);await pg.screenshot({path:`shots/f/rec${n}.png`});console.log('rec',n)}+await tab('Schematic');await sleep(14000);await pg.screenshot({path:'shots/f/sch101.png'});+await clickN(110);await sleep(3000);await tab('Findings');await sleep(2500);await pg.screenshot({path:'shots/f/findings.png'});+await pg.close();+// ---- pcb viewer, 2x, layer states+const shots=[+ ['101-mimic','top',{side:'Top',on:/Copper · Top|Silk · Top|Board edge/}],+ ['101-mimic','bot',{side:'Bottom',on:/Copper · Bot|Silk · Bot|Board edge/}],+ ['101-mimic','all',{side:'Top',on:/Copper|Silk · Top|Board edge/}],+ ['073-mimic','top',{side:'Top',on:/Copper · Top|Silk · Top|Board edge/}],+ ['073-mimic','bot',{side:'Bottom',on:/Copper · Bot|Silk · Bot|Board edge/}],+ ['096-connect','inner',{side:'Top',on:/Copper · In|Board edge/}],+ ['097-connect','inner',{side:'Top',on:/Copper · In|Board edge/}],+ ['098-connect','inner',{side:'Top',on:/Copper · In|Board edge/}],+ ['096-connect','all',{side:'Top',on:/Copper|Board edge/}],+ ['097-connect','all',{side:'Top',on:/Copper|Board edge/}],+ ['098-connect','all',{side:'Top',on:/Copper|Board edge/}],+ ['082-connect','all',{side:'Top',on:/Copper|Board edge/}],+];+for(const [v,name,o] of shots){pg=await b.newPage();await pg.setViewport({width:1920,height:1080,deviceScaleFactor:2});+ await pg.goto(`${U}files/views/${v}-pcb.html`,{waitUntil:'load',timeout:180000});await sleep(9000);+ await pg.evaluate(o=>{[...document.querySelectorAll('button')].find(e=>e.textContent.trim()===o.side)?.click();+   const re=new RegExp(o.on);document.querySelectorAll('.lyr').forEach(l=>{const c=l.querySelector('input');const want=re.test(l.querySelector('.lyr-name').textContent);if(c.checked!==want)c.click()});+   document.querySelectorAll('.panel').forEach(x=>x.style.display='none');+   [...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Fit')?.click();window.AdomPcb?.fit?.()},{side:o.side,on:o.on.source});+ await sleep(5000);await pg.screenshot({path:`shots/p/${v.slice(0,3)}-${name}.png`});console.log(v,name,await pg.evaluate(()=>[...document.querySelectorAll('.lyr-name')].length));await pg.close();}+// schematic 2x+pg=await b.newPage();await pg.setViewport({width:1920,height:1080,deviceScaleFactor:2});await pg.goto(U+'files/views/101-mimic-sch.html',{waitUntil:'load',timeout:180000});await sleep(10000);await pg.screenshot({path:'shots/p/101-sch.png'});+console.log(await pg.evaluate(()=>[...document.querySelectorAll('select option')].map(e=>e.textContent).slice(0,20)));+await b.close()})().catch(e=>{console.error(e);process.exit(1)});
docs/process-video/build/composer.htmladded+114
@@ -0,0 +1,114 @@+<!doctype html><meta charset=utf-8><style>+@font-face{font-family:Sat;src:url(fonts/satoshi-400.woff2);font-weight:400}@font-face{font-family:Sat;src:url(fonts/satoshi-500.woff2);font-weight:500}+@font-face{font-family:Fam;src:url(fonts/familjen-grotesk-500.woff2);font-weight:500}@font-face{font-family:Fam;src:url(fonts/familjen-grotesk-700.woff2);font-weight:700}+@font-face{font-family:Jet;src:url(fonts/jetbrains-mono-400.woff2);font-weight:400}@font-face{font-family:Jet;src:url(fonts/jetbrains-mono-500.woff2);font-weight:500}+body{margin:0;background:#0d1117}canvas{display:block}</style><canvas id=c width=1920 height=1080></canvas><script>+const c=document.getElementById('c').getContext('2d');c.imageSmoothingQuality='high';+const BG='#0d1117',PANEL='#161b22',EDGE='#30363d',INK='#e6edf3',MUT='#8b949e',TEAL='#00b8b0',TEAL2='#00e6dc',AMB='#d29922',RED='#f85149',GRN='#3fb950',VIO='#a371f7';+let D,IM={};const LEAD=.25,TAIL=.45;+const load=n=>new Promise((r,j)=>{const i=new Image();i.onload=()=>{IM[n]=i;r()};i.onerror=j;i.src=n+'.jpg'});+const cl=(x,a=0,b=1)=>Math.min(b,Math.max(a,x)),ez=x=>{x=cl(x);return x*x*(3-2*x)},lerp=(a,b,t)=>a+(b-a)*t;+const K=(a,b,t)=>a.map((v,i)=>lerp(v,b[i],ez(t)));+// camera: [x,y,w] in image fractions, height follows the target's aspect+function img(n,cam,R,al=1){const I=IM[n];if(al<=0)return;const[x,y,w]=cam,rw=R[2],rh=R[3],sw=w*I.width,sh=sw*rh/rw;c.save();c.globalAlpha=al;rr(R,14);c.clip();c.drawImage(I,x*I.width,y*I.height,sw,sh,R[0],R[1],rw,rh);c.restore()}+function rr(R,r){c.beginPath();c.roundRect(R[0],R[1],R[2],R[3],r)}+function frame(R){c.save();rr(R,14);c.lineWidth=1.5;c.strokeStyle=EDGE;c.stroke();c.restore()}+const BW=1168,B=[36,50,BW,980]; // board panel, 1.182 aspect+const COL=[1244,46,640,988];+const W=2946,H=2471;// mm to panel px given camera+const mm=(x,y,cam,R)=>{const[cx,cy,cw]=cam,s=R[2]/(cw*W);return[R[0]+((x-100)*40-cx*W)*s,R[1]+((y-100)*40-cy*H)*s,s]};+function clipR(R,f){c.save();rr(R,14);c.clip();f();c.restore()}+function T(s,x,y,o={}){c.save();c.globalAlpha=o.a??1;c.font=`${o.w||400} ${o.s||24}px ${o.f||'Sat'}`;c.fillStyle=o.c||INK;c.textAlign=o.al||'left';c.textBaseline='alphabetic';c.fillText(s,x,y);c.restore()}+function wrap(s,w,font){c.save();c.font=font;const L=[];let l='';for(const t of s.split(' ')){const n=l?l+' '+t:t;if(c.measureText(n).width>w&&l){L.push(l);l=t}else l=n}L.push(l);c.restore();return L}+function P(s,x,y,w,o={}){const f=`${o.w||400} ${o.s||24}px ${o.f||'Sat'}`;let yy=y;for(const l of wrap(s,w,f)){T(l,x,yy,o);yy+=(o.lh||(o.s||24)*1.36)}return yy}+function head(i,title){T(title,COL[0],92,{f:'Fam',w:700,s:36})}+// stat card, slides in from cue+function stat(y,big,label,t,cue,col=TEAL,hgt=120){const a=ez((t-cue)/.45);if(a<=0)return y+hgt+14;const x=COL[0]+(1-a)*18;c.save();c.globalAlpha=a;c.fillStyle=PANEL;rr([x,y,COL[2],hgt],12);c.fill();c.strokeStyle=EDGE;c.lineWidth=1;c.stroke();c.fillStyle=col;rr([x,y+12,4,hgt-24],2);c.fill();c.restore();+ T(big,x+26,y+54,{f:'Fam',w:700,s:42,c:col===MUT?INK:col,a});P(label,x+26,y+88,COL[2]-52,{s:21,c:'#c3ccd6',a,lh:27});return y+hgt+14}+function card(s,x,y,w,o={}){const a=o.a??1;if(a<=0)return;const f=`${o.w||500} ${o.s||26}px ${o.f||'Sat'}`,L=wrap(s,w-48,f),h=L.length*(o.s||26)*1.36+30;c.save();c.globalAlpha=a*.94;c.fillStyle='#10161e';rr([x,y-h,w,h],12);c.fill();c.strokeStyle=o.b||TEAL;c.lineWidth=1.5;c.stroke();c.restore();L.forEach((l,i)=>T(l,x+24,y-h+15+(o.s||26)*(1.02+i*1.36),{...o,a}))}+const on=(t,a,f=.4)=>ez((t-a)/f);const seg=(t,a,b,f=.45)=>ez((t-a)/f)*(1-ez((t-b)/f));+function ring(x,y,r,col,lw=2.5,a=1){c.save();c.globalAlpha=a;c.strokeStyle=col;c.lineWidth=lw;c.beginPath();c.arc(x,y,r,0,7);c.stroke();c.restore()}+function label(s,x,y,o={}){c.save();c.font=`500 ${o.s||20}px Sat`;const w=c.measureText(s).width+22;c.globalAlpha=o.a??1;c.fillStyle='rgba(13,17,23,.86)';rr([x,y-26,w,34],7);c.fill();c.strokeStyle=o.b||EDGE;c.lineWidth=1;c.stroke();c.fillStyle=o.c||INK;c.textBaseline='alphabetic';c.fillText(s,x+11,y-3);c.restore()}+const FULL=[0,0,1];const fit=(x=0,y=0,w=1)=>[x,y,w];+const PROMPT="Can you rewrite this for reverse engineering PCBs from pictures (see Driver 1 through 4). We would want to THings to add in: 1) Identify as many parts as possible. 2) Identify functional blocks. 3) Identify zones and routing. 4) Directly mimic all of this that is visible. 5) from here save the project, then attempt to make the final connections and propose the layer count by how challenging it is to do so. You can also infer planes and powers by the power and ground that you can see routed. Since direct vias to internal are indicative of a plane, you will have to infer these items if it is gnd for instance. 6) do a final looping review stating your confidence in the results. 7) provide a list of tests that can confirm anything that is unknown.";+const SC={+prompt(t,C,d){const R=[160,130,1600,820];c.fillStyle=PANEL;rr(R,16);c.fill();c.strokeStyle=EDGE;c.lineWidth=1.5;c.stroke();+ T('The prompt',R[0]+40,R[1]+56,{f:'Fam',w:700,s:30});T('re-enacted from the saved text',R[0]+200,R[1]+56,{s:20,c:MUT});+ const n=Math.floor(PROMPT.length*cl(t/(d*.93)));const s=PROMPT.slice(0,n);const f='400 31px Jet';const L=wrap(s,R[2]-84,f);L.forEach((l,i)=>T(l,R[0]+42,R[1]+118+i*50,{f:'Jet',s:31,c:i==L.length-1?INK:'#c9d4de'}));+ const last=L[L.length-1]||'';c.font=f;const cx=R[0]+42+c.measureText(last).width+4;if(Math.floor(t*2.5)%2==0||n<PROMPT.length){c.fillStyle=TEAL;c.fillRect(cx,R[1]+118+(L.length-1)*50-27,15,34)}},+flow(t,C,d){const k=K([0,.02,.98],[.03,.17,.72],t/d);const A=[0,0,1920,1080];img('timeline',k,A);+ const m=['Version 0.1 reused the AI Flow binary, now archived','Version 0.2: adom-aiflow-reverse, in Rust, with trace.rs, islands.rs and route.rs','The program measures. The AI names.','11 steps, each one recorded'];+ m.forEach((s,i)=>card(s,60,1040,1100,{a:seg(t,C[i],C[i+1]??99),s:28}))},+photos(t,C,d){const a0=1-on(t,C[1]-.1);if(a0>0){const G=[[60,46,880,492],[980,46,880,492],[60,548,880,492],[980,548,880,492]];['raw_Driver1','raw_Driver2','raw_Driver3','raw_Driver4'].forEach((n,i)=>{const z=lerp(1,.9,t/6),I=IM[n];const asp=G[i][2]/G[i][3];const hw=z*Math.min(1,asp*I.height/I.width);img(n,[.5-hw/2,.5-(hw*I.width/asp/I.height)/2,hw],G[i],a0);label(['Top, photo 1','Top, photo 2','Bottom, photo 3','Bottom, photo 4'][i],G[i][0]+14,G[i][1]+46,{a:a0})})}+ const L=[60,120,880,738],Rr=[980,120,880,738];+ const a1=seg(t,C[1]-.1,C[2]-.1);if(a1>0){const z=lerp(1,.94,(t-C[1])/5);for(const[n,R,s]of[['outline-Driver1',L,'Board edge found, top'],['outline-Driver3',Rr,'Board edge found, bottom']]){const I=IM[n],asp=R[2]/R[3],hw=z*Math.min(1,asp*I.height/I.width);img(n,[.5-hw/2,.5-(hw*I.width/asp/I.height)/2,hw],R,a1);label(s,R[0]+14,R[1]+46,{a:a1})}}+ const a2=on(t,C[2]-.1);if(a2>0){const R2=[60,120,880,738],R3=[980,120,880,738],p=(t-C[2]-.5)/1.5;img('top',[0,.0,1],R2,a2);label('Top, straightened',R2[0]+14,R2[1]+46,{a:a2});const f=Math.cos(Math.PI*cl(p));c.save();const cy=R3[1]+R3[3]/2;c.globalAlpha=a2;c.translate(0,cy);c.scale(1,Math.max(.002,Math.abs(f)));c.translate(0,-cy);if(f>0){const I=IM['prep-Driver3'];c.drawImage(I,0,0,I.width,I.width*R3[3]/R3[2],R3[0],R3[1],R3[2],R3[3])}else c.drawImage(IM.bottom,0,0,W,W*R3[3]/R3[2],R3[0],R3[1],R3[2],R3[3]);c.restore();label(f>0?'Bottom, as photographed':'Bottom, turned over top to bottom',R3[0]+14,R3[1]+46,{a:a2});+  const b=on(t,C[2]+2.4);if(b>0){T('38 holes agree, 2 don\'t',960,960,{f:'Fam',w:700,s:46,c:TEAL,al:'center',a:b});T('Bottom aligned to top on 40 holes, 0.204 mm residual',960,1006,{s:24,c:'#c3ccd6',al:'center',a:b})}}},+scale(t,C,d){head(3,'Scale');const a=K(FULL,[.62,.2,.36],(t-C[1]+.6)/2.2);const b=K(a,[.31,.28,.22],(t-C[2]+.2)/1.8);const cam=t<C[2]-.2?a:b;img('top',cam,B);+ clipR(B,()=>{D.holes.forEach(h=>{const[x,y,s]=mm(h.x,h.y,cam,B);ring(x,y,Math.max(5,h.outer_mm*20*s),'rgba(230,237,243,.45)',1.5)});+ D.rows.forEach((r,i)=>{const a=on(t,1+i*.45);if(a<=0)return;const col=Math.abs(r.pitch_mm-2.54)<.1?AMB:TEAL;c.save();c.globalAlpha=a;c.strokeStyle=col;c.lineWidth=3;c.beginPath();r.holes.forEach((j,k)=>{const h=D.holes[j],[x,y]=mm(h.x,h.y,cam,B);k?c.lineTo(x,y):c.moveTo(x,y)});c.stroke();c.restore();r.holes.forEach(j=>{const h=D.holes[j],[x,y,s]=mm(h.x,h.y,cam,B);ring(x,y,Math.max(7,h.outer_mm*22*s),col,3,a)})});+ const u=on(t,C[2]+.2);if(u>0){const[x,y,s]=mm(134.671,127.936,cam,B);c.save();c.translate(x,y);c.rotate(Math.PI/4);c.strokeStyle=TEAL2;c.lineWidth=3;c.globalAlpha=u;const q=6*40*s;c.strokeRect(-q/2,-q/2,q,q);c.restore()}});frame(B);+ let y=120;y=stat(y,'2.00 mm','Five header rows on the top face',t,C[0]+.8);y=stat(y,'2.54 mm','One header row, a second standard pitch',t,C[0]+2.4,AMB);y=stat(y,'46.599 px/mm','Solved from those pitches, then the photos are redrawn at 40 px/mm',t,C[1],TEAL,146);y=stat(y,'6 x 6 mm','DRV8323S body from TI. It measures 41 px/mm on the photo, near enough to rule out a 1 mm row standard. The precise number comes from the holes.',t,C[2],MUT,206)},+parts(t,C,d){head(4,'Parts');const k=t<C[2]-.3?K(FULL,[.03,.03,.94],t/d):t<C[3]-.3?K(FULL,[.28,.27,.36],(t-C[2]+.3)/1.4):K([.28,.27,.36],FULL,(t-C[3]+.3)/1.2);img('top',k,B);+ clipR(B,()=>{const n=D.parts.length;D.parts.forEach((p,i)=>{const a=on(t,.2+i/n*(C[1]-.5),.12);if(a<=0)return;const[x,y,s]=mm(p.x,p.y,k,B);let col=TEAL2,al=a,r=Math.max(4,1.1*40*s);+  if(t>C[1]&&t<C[2]-.3){if(!p.pads){col=MUT;al=.4}else col=GRN}if(t>C[3]){if(p.unk)col=AMB;else{col=MUT;al=.35}}+  if(t>C[2]-.3&&t<C[3])al=a*.35;c.save();c.globalAlpha=al;c.strokeStyle=col;c.lineWidth=2.2;c.strokeRect(x-r,y-r,2*r,2*r);c.restore()})});frame(B);+ const cn=Math.round(137*cl((t-.2)/(C[1]-.5)));let y=120;y=stat(y,String(cn),'parts found, every one on the top face',t,.2);y=stat(y,'103','placed by their own pads on the photo; the rest by body centre or a direct reading',t,C[1],GRN,146);+ const pa=seg(t,C[2],C[3]-.1);if(pa>0){const Z=[COL[0]+50,y,540,540];img('pin1-U2',[0,0,1],Z,pa);c.save();c.globalAlpha=pa;rr(Z,14);c.strokeStyle=EDGE;c.stroke();c.restore();label('U2: marking read, DRV8323S',Z[0]+12,Z[1]+500,{a:pa,c:TEAL2})}+ stat(y,'91','parts with something inferred or unknown, each with a test in the list',t,C[3],AMB,146)},+blocks(t,C,d){head(5,'Blocks');const k=K(FULL,[.02,.02,.96],t/d);img('top',k,B);const pal=[TEAL2,'#f0883e','#ffd33d','#3fb950','#a371f7','#58a6ff','#f778ba','#7ee787','#d2a8ff','#ff7b72','#e6edf3'];+ const b=D.blocks,tm=[C[0]+.6,C[1]+.2,C[1]+1.5,C[1]+2.5,C[1]+3.6,C[2]+.2,C[2]+1.8,C[2]+2.4,C[2]+3,C[2]+3.8,C[2]+4.9];+ clipR(B,()=>b.forEach((q,i)=>{const a=on(t,tm[i],.35);if(a<=0)return;const cur=i==b.length-1||t<tm[i+1];const w=1;if(q.id=='mechanical'){q.pts.forEach(p=>{const[x,y,s]=mm(p[0],p[1],k,B);ring(x,y,70*s,pal[i],3,a)});return}+  c.save();c.beginPath();q.hull.forEach((p,j)=>{const[x,y]=mm(p[0],p[1],k,B);j?c.lineTo(x,y):c.moveTo(x,y)});c.closePath();c.lineJoin='round';c.strokeStyle=pal[i];c.globalAlpha=a*(cur?1:.75);c.lineWidth=cur?46:34;c.globalAlpha=a*(cur?.34:.16);c.stroke();c.fillStyle=pal[i];c.fill();c.globalAlpha=a*(cur?1:.5);c.lineWidth=0;c.restore();+  q.pts.forEach(p=>{const[x,y,s]=mm(p[0],p[1],k,B);c.save();c.globalAlpha=a*(cur?1:.55);c.fillStyle=pal[i];c.beginPath();c.arc(x,y,cur?6:4,0,7);c.fill();c.restore()})}));frame(B);+ b.forEach((q,i)=>{const a=on(t,tm[i],.35),y=132+i*72;const cur=a>0&&(i==b.length-1||t<tm[i+1]);c.save();c.globalAlpha=.25+.75*a;c.fillStyle=cur?'#1c2128':PANEL;rr([COL[0],y,COL[2],62],10);c.fill();c.fillStyle=a>0?pal[i]:EDGE;rr([COL[0]+12,y+14,10,34],4);c.fill();c.restore();T(q.name,COL[0]+36,y+40,{f:'Fam',w:500,s:27,a:.3+.7*a});T(q.n+' parts',COL[0]+COL[2]-20,y+40,{f:'Jet',s:20,c:MUT,al:'right',a:.3+.7*a})})},+miss(t,C,d){const L=[40,60,910,764],Rr=[970,60,910,764],z=lerp(1,.86,t/d),k=[.02,.0,z];img('bottom',k,L);img('p_073-bot',k,Rr);frame(L);frame(Rr);label('Bottom photo',L[0]+14,L[1]+46);label('First copy, bottom copper',Rr[0]+14,Rr[1]+46);+ const a=on(t,.8);T('58',60,930,{f:'Fam',w:700,s:72,c:AMB,a});T('pours, all on the bottom',150,928,{s:30,a});T('0',640,930,{f:'Fam',w:700,s:72,c:RED,a});T('tracks on either face',690,928,{s:30,a});+ const q=on(t,C[1]);if(q>0){c.save();c.globalAlpha=q;c.fillStyle=PANEL;rr([60,960,1800,84],12);c.fill();c.strokeStyle=TEAL;c.lineWidth=1.5;c.stroke();c.restore();T('"the fill and via recognition is completely lacking"',90,1014,{f:'Fam',w:500,s:36,a:q,c:INK});T('Caleb, on the first copy',1830,1012,{s:22,c:MUT,al:'right',a:q})}},+copper(t,C,d){head(6,'Reading the copper');const k=K([.25,.28,.42],[.3,.33,.42],t/C[2]);const k2=K(k,FULL,(t-C[2])/1.6);const cam=t<C[2]?k:k2;+ img('bottom',cam,B);img('ratio-bottom',cam,B,seg(t,C[0]+2.6,C[1]+.5,.7));img('lines-bottom',cam,B,seg(t,C[1]-.1,C[2]+.2,.6));img('copper-islands-bottom',cam,B,seg(t,C[2],C[3]+.1,.6));img('copper-islands-top',FULL,B,on(t,C[3],.6));frame(B);+ const tag=t<C[0]+3?'Bottom photo':t<C[1]?'Green channel over its neighbourhood':t<C[2]?'Dark line filter, 16 angles':t<C[3]?'Islands, bottom':'Islands, top';label(tag,B[0]+14,B[1]+46);+ let y=120;y=stat(y,'Below 0.9','Local brightness ratio, box of 3 px over box of 120 px. Under about 0.9 is laminate, above is copper.',t,C[0]+2.6,TEAL,178);y=stat(y,'Gap lines','Thin dark ridges closing the outline of each island. Silkscreen can be crossed. Parts and pads can\'t.',t,C[1],VIO,178);y=stat(y,'1,444','islands read from the two faces',t,C[2]);y=stat(y,'116 pours','87 on top, 29 on the bottom',t,C[3],GRN);y=stat(y,'314 tracks','222 on top, 92 on the bottom',t,C[3]+.4,GRN)},+vias(t,C,d){head(7,'Vias');const a=K(FULL,[.015,.015,.97],t/C[2]);const zo=[.0,.72,.25];const cam=t<C[2]-.2?a:K(a,zo,(t-C[2]+.2)/1.7);img('bottom',cam,B);+ clipR(B,()=>{const bad=t>C[2];D.vias.forEach((v,i)=>{const q=on(t,.4+(i/D.vias.length)*(C[1]-1),.1);if(q<=0)return;const[x,y,s]=mm(v.x,v.y,cam,B);const both=t>C[1]&&t<C[2]-.2&&v.k=='both';const pale=bad&&v.p;let col=both?AMB:pale?RED:TEAL,al=q*(t>C[1]?(both||pale?1:.3):.9);if(t>C[3]&&!(v.k=='inner'))al*=.45;c.save();c.globalAlpha=al;c.fillStyle=col;c.beginPath();c.arc(x,y,Math.max(both?4.5:2.2,.3*40*s),0,7);c.fill();c.restore()});+ const q=on(t,C[3]);if(q>0)D.vias.filter(v=>v.k=='inner').forEach(v=>{const[x,y,s]=mm(v.x,v.y,cam,B);ring(x,y,Math.max(14,.7*40*s),TEAL2,4,q)})});frame(B);label(t<C[2]?'Kept vias on the bottom photo':t<C[3]?'Red: on pale pixels, likely silkscreen':'Ringed: two of the three "inward" vias',B[0]+14,B[1]+46);+ const cn=Math.round(1028*cl((t-.4)/(C[1]-1)));let y=120;y=stat(y,'2,681','via candidates found on the photos',t,.4,MUT);y=stat(y,cn.toLocaleString(),'kept after pads, duplicates and the board edge are ruled out',t,.8,TEAL,146);y=stat(y,'45','seen on both faces, because vias tent over on the top',t,C[1],AMB,146);y=stat(y,'Up to 338','land on pale pixels, mostly silkscreen letters and lines. Those look false.',t,C[2],RED,146);y=stat(y,'3','read as leading only inward. Up close, ordinary domes. No proof of inner layers.',t,C[3],AMB,146)},+copy(t,C,d){const a=1-on(t,C[1]-.2);if(a>0)img('f_rec81',K(FULL,[.14,.05,.82],t/C[1]),[0,0,1920,1080],a);const b=on(t,C[1]-.2);if(b>0){const k=K(FULL,[.04,.04,.92],(t-C[1])/3);head(8,'The copy');img('overlay-top',k,B,b);img('overlay-bottom',k,B,b*on(t,C[1]+1.5));frame(B);label(t<C[1]+1.5?'Copy over the top photo':'Copy over the bottom photo',B[0]+14,B[1]+46,{a:b});let y=120;y=stat(y,'137','footprints',t,C[1]);y=stat(y,'116','pours filled as photographed',t,C[1]+.3);y=stat(y,'146','design rule errors before routing, listed in the report',t,C[1]+.6,AMB)}},+nets(t,C,d){const A=[0,0,1920,1080],k=K([.235,.02,.46],[.24,.42,.40],t/d);img('p_101-sch',k,A);+ const x=1280,w=600,o=[['77','nets, completed from datasheets and the circuit',TEAL,.3],['249','connections left to make',TEAL,3],['99','pins disagree with the copper they seem to touch. Reported, not applied.',AMB,C[1]]];let y=70;o.forEach(([b,l,col,cu])=>{const q=on(t,cu);if(q<=0)return;const f='400 22px Sat',L=wrap(l,w-52,f),h=74+L.length*29;c.save();c.globalAlpha=q*.96;c.fillStyle=PANEL;rr([x,y,w,h],12);c.fill();c.strokeStyle=EDGE;c.stroke();c.fillStyle=col;rr([x,y+12,4,h-24],2);c.fill();c.restore();T(b,x+26,y+54,{f:'Fam',w:700,s:42,c:col,a:q});L.forEach((s,i)=>T(s,x+26,y+88+i*29,{s:22,a:q,c:'#c3ccd6'}));y+=h+14});label('Schematic, generated by the flow',30,1050)},+router(t,C,d){head(9,'The router');const k=K(FULL,[.03,.03,.94],t/d);img('p_097-all',k,B);img('p_082-all',k,B,seg(t,C[1]-.1,C[3]-.1));frame(B);label(t>C[1]&&t<C[3]?'Run 82: pours as hard walls':'Final 6-layer run',B[0]+14,B[1]+46);+ const X=COL[0],Y=128,Wd=COL[2],Ht=540;c.fillStyle=PANEL;rr([X,Y,Wd,Ht],12);c.fill();c.strokeStyle=EDGE;c.lineWidth=1;c.stroke();const px=78,py=46;const R=D.runs,col={4:TEAL2,6:AMB,8:VIO};const fx=i=>X+px+i*(Wd-px-30)/(R.length-1),fy=o=>Y+Ht-py-(o/250)*(Ht-py-30);+ for(const g of[0,50,100,150,200,250]){c.strokeStyle='#21262d';c.beginPath();c.moveTo(X+px,fy(g));c.lineTo(X+Wd-30,fy(g));c.stroke();T(String(g),X+px-12,fy(g)+7,{f:'Jet',s:18,c:MUT,al:'right'})}T('Open connections, each recorded run',X+px,Y+28,{s:20,c:'#c3ccd6'});+ const n=lerp(1,R.length,ez((t-C[1])/(C[3]+2.6-C[1])));R.forEach((r,i)=>{const a=cl(n-i);if(a<=0)return;ring(fx(i),fy(r.o),6,col[r.l],3,a);if(i>0&&a>=1){}});[4,6,8].forEach((l,i)=>{ring(X+px+10+i*150,Y+Ht-18,6,col[l],3);T(l+' layers',X+px+24+i*150,Y+Ht-11,{s:19,c:'#c3ccd6'})});+ const a0=seg(t,C[1],C[2]+99);if(a0>0)label('235 open',fx(0)+14,fy(235)+10,{a:a0,c:TEAL2});+ const e=on(t,C[3]+2.2);if(e>0){const L=R.length;[['54',L-3],['34',L-2],['29',L-1]].forEach(([s,i])=>label(s,fx(i)-20,fy(R[i].o)-16,{a:e,s:18}))}+ let y=Y+Ht+14;y=stat(y,'Every signal layer','A through-via router on a 0.05 mm grid, working all layers of the stackup',t,C[0]+.3,TEAL,130);y=stat(y,'Costs 6x to 12x','Crossing seen copper of another net is allowed but expensive, and the length crossed is reported',t,C[2],AMB,146)},+layers(t,C,d){head(10,'Layer count');const n=t<C[2]?'096':t<C[3]?'097':'098',cam=K(FULL,[.03,.03,.94],t/d);img('p_096-inner',cam,B);img('p_097-inner',cam,B,on(t,C[2]-.1));img('p_098-inner',cam,B,seg(t,C[3]-.1,C[4]-.2));frame(B);label(t<C[2]?'Routed inner copper, 4 layers':t<C[3]||t>C[4]?'Routed inner copper, 6 layers':'Routed inner copper, 8 layers',B[0]+14,B[1]+46);+ const X=COL[0],Y=130,rows=[[4,54,17,'327 mm',C[1]],[6,34,6,'24 mm',C[2]],[8,29,9,'30 mm',C[3]]];const cols=[20,150,290,430];T('Layers',X+cols[0],Y+24,{s:21,c:MUT});T('Open',X+cols[1],Y+24,{s:21,c:MUT});T('New DRC',X+cols[2],Y+24,{s:21,c:MUT});T('Crossing',X+cols[3],Y+24,{s:21,c:MUT});+ rows.forEach((r,i)=>{const y=Y+44+i*128,a=on(t,r[4]);const cur=(t>=r[4]&&(i==2?t<C[4]:t<rows[i+1][4]))||(i==1&&t>=C[4]);c.save();c.globalAlpha=.3+.7*a;c.fillStyle=cur?'#1c2128':PANEL;rr([X,y,COL[2],114],12);c.fill();c.strokeStyle=(i==1&&t>=C[4])?TEAL:EDGE;c.lineWidth=(i==1&&t>=C[4])?2.5:1;c.stroke();c.restore();if(a<=0){T(String(r[0]),X+cols[0],y+72,{f:'Fam',w:700,s:48,a:.35});return}+  T(String(r[0]),X+cols[0],y+72,{f:'Fam',w:700,s:48,a});T(String(r[1]),X+cols[1],y+72,{f:'Fam',w:700,s:48,c:AMB,a});T(String(r[2]),X+cols[2],y+72,{f:'Fam',w:500,s:38,a});T(r[3],X+cols[3],y+72,{f:'Fam',w:700,s:38,c:i==0?RED:GRN,a})});+ let y=Y+44+3*128+8;y=stat(y,'Runs vary by a quarter','6 layers ranged from 25 to 34 open on repeat runs, so 6 and 8 can\'t be separated on open count',t,C[3]+2,MUT,146);y=stat(y,'6 proposed','From routing difficulty only. T-layers in the test list settles it.',t,C[4],TEAL,120)},+rules(t,C,d){head(11,'Rules');const a=K(FULL,[.03,.03,.94],Math.min(t,C[1])/C[1]);const fo=[.33,.3,.36];const cam=t<C[1]-.3?a:t<C[2]-.3?K(a,fo,(t-C[1]+.3)/1.3):K(fo,[.3,.27,.42],(t-C[2]+.3)/4);img('p_097-all',cam,B);clipR(B,()=>{const q=on(t,C[1]);if(q>0)for(const[x,y]of[[134.671,127.936],[150.039,132.522]]){const[u,v,s]=mm(x,y,cam,B);ring(u,v,4.7*40*s,TEAL2,3.5,q)}});frame(B);+ let y=120;y=stat(y,'KiCad default','Track and clearance 0.2 mm, via 0.6 / 0.3 mm, edge 0.5 mm. Set at the start for zones, router and DRC.',t,C[0]+.2,TEAL,178);y=stat(y,'At U1 and U2','Nearly all 34 open pins are fan-outs at these two QFNs, plus a misplaced R10',t,C[1],AMB,146);+ const q=on(t,C[2]);if(q>0){const X=COL[0];c.save();c.globalAlpha=q;c.fillStyle=PANEL;rr([X,y,COL[2],220],12);c.fill();c.strokeStyle=EDGE;c.stroke();c.restore();T('Open connections',X+22,y+40,{s:22,c:MUT,a:q});T('4 layers',X+300,y+40,{s:22,c:MUT,a:q});T('6 layers',X+460,y+40,{s:22,c:MUT,a:q});T('KiCad default',X+22,y+102,{s:26,a:q});T('54',X+300,y+106,{f:'Fam',w:700,s:42,a:q});T('34',X+460,y+106,{f:'Fam',w:700,s:42,a:q});T('JLC-style, 0.127',X+22,y+176,{s:26,a:q});T('30',X+300,y+180,{f:'Fam',w:700,s:42,a:q});T('2',X+460,y+180,{f:'Fam',w:700,s:42,c:GRN,a:q});y+=234}+ stat(y,'No valid DRC yet','The shared KiCad service only checks its default rules',t,C[3],RED,120)},+review(t,C,d){const cats=[['Blocks','High',GRN,0],['Placement','High',GRN,0],['Identity','Medium',AMB,1],['Values','Medium',AMB,1],['Copper','Medium',AMB,1],['Vias','Medium',AMB,1],['Layers','Medium',AMB,1],['Connectivity','Low',RED,2]];const why={Blocks:'Silkscreen labels and parts agree',Placement:'Most parts placed on their pads',Identity:'Key chips read, others by role',Values:'Most MLCC values unreadable',Copper:'Follows the photos, with known misses',Vias:'Only 45 confirmed on both faces',Layers:'From routing difficulty, never seen',Connectivity:'99 pin clashes, 827 mm² of pour with no net'};+ T('Review and tests',60,90,{f:'Fam',w:700,s:40});[5,0,1,0].forEach((n,i)=>{const q=on(t,.4+i*.7),x=60+i*200;c.save();c.globalAlpha=q;c.fillStyle=PANEL;rr([x,118,186,62],10);c.fill();c.strokeStyle=(i==3)?TEAL:EDGE;c.stroke();c.restore();T('Pass '+(i+1),x+14,157,{s:20,c:MUT,a:q});T(String(n),x+160,160,{f:'Fam',w:700,s:32,al:'right',c:n?AMB:GRN,a:q})});+ const tm=[C[1]+.2,C[1]+.2,C[1]+2.2,C[1]+2.6,C[1]+3,C[1]+3.4,C[1]+3.8,C[2]];cats.forEach((q,i)=>{const col=i%2,row=Math.floor(i/2),x=60+col*560,y=206+row*208;const a=on(t,tm[i]);c.save();c.globalAlpha=.3+.7*a;c.fillStyle=PANEL;rr([x,y,544,192],14);c.fill();c.strokeStyle=(q[3]==2&&a>0)?RED:EDGE;c.lineWidth=(q[3]==2&&a>0)?2.5:1;c.stroke();c.restore();T(q[0],x+26,y+56,{f:'Fam',w:500,s:32,a:.4+.6*a});T(a>0?q[1]:'',x+26,y+112,{f:'Fam',w:700,s:46,c:q[2],a});P(why[q[0]],x+26,y+156,492,{s:22,c:'#c3ccd6',a})});+ const X=1216,Y=46,Wd=668,Ht=988,q=on(t,C[3]-.3);c.save();c.globalAlpha=.3+.7*q;c.fillStyle=PANEL;rr([X,Y,Wd,Ht],14);c.fill();c.strokeStyle=EDGE;c.stroke();c.restore();T('119 tests',X+26,Y+56,{f:'Fam',w:700,s:38,c:TEAL,a:.3+.7*q});T('settle the 1,632 open items',X+210,Y+54,{s:22,c:'#c3ccd6',a:.3+.7*q});+ clipR([X,Y+78,Wd,Ht-90],()=>{const off=Math.max(0,t-C[3]+.2)*92;D.tests.forEach((r,i)=>{const y=Y+120+i*66-off;if(y<Y+40||y>Y+Ht+40)return;T(r.id,X+26,y,{f:'Jet',s:20,c:TEAL2,a:.3+.7*q,w:500});T(r.m,X+26,y+29,{s:19,c:'#c3ccd6',a:.3+.7*q})})})},+notdone(t,C,d){const a=seg(t,-1,C[1]-.1);if(a>0){T('Not a finished board',960,520,{f:'Fam',w:700,s:96,al:'center',a});T('Here is what is still wrong',960,590,{s:34,c:'#c3ccd6',al:'center',a})}+ const b=seg(t,C[1]-.1,C[2]-.1);if(b>0){head(12,'Not done');img('p_097-all',K(FULL,[.05,.05,.9],(t-C[1])/6),B,b);frame(B);c.save();c.globalAlpha=b;c.restore();let y=120;y=stat(y,'34','connections open at 6 layers, nearly all QFN fan-outs',t,C[1],RED,146);y=stat(y,'76','missing per KiCad, because some seen copper of one net is not joined in the copy',t,C[1]+2.6,AMB,146);y=stat(y,'146','DRC errors before routing, 62 of them courtyard overlaps',t,C[1]+4.4,AMB,146)}+ const e=seg(t,C[2]-.1,C[3]-.1);if(e>0){const L=[40,120,910,770],Rr=[970,120,910,770],k=K([0,0,.5],[0,.02,.42],(t-C[2])/6);img('bottom',k,L,e);img('p_101-bot',k,Rr,e);frame(L);frame(Rr);label('Bottom photo, upper left',L[0]+14,L[1]+46,{a:e});label('The copy, same area',Rr[0]+14,Rr[1]+46,{a:e});T('Rails largely missing. 637 short fragments dropped as misreadings.',960,970,{f:'Fam',w:500,s:36,al:'center',a:e})}+ const f=on(t,C[3]-.1);if(f>0){head(12,'Not done');img('p_097-inner',K(FULL,[.05,.05,.9],(t-C[3])/5),B,f);frame(B);label('Routed inner copper',B[0]+14,B[1]+46,{a:f});stat(120,'One possible way','to make the connections. In1 as ground and In4 as 3.3 volts are inferred, not seen.',t,C[3],AMB,146)}},+record(t,C,d){const L=['f_rec6','f_rec10','f_rec15','f_rec73','f_rec81','f_rec96','f_rec97','f_rec101','f_sch101'];const q=t/d*L.length,i=Math.min(L.length-1,Math.floor(q));const z=lerp(1,.94,q-i);img(L[i],[(1-z)/2,(1-z)/2,z],[0,0,1920,1080]);if(q-i<.2&&i>0)img(L[i-1],[.03,.03,.94],[0,0,1920,1080],1-(q-i)/.2)},+cost(t,C,d){T('What it cost',60,110,{f:'Fam',w:700,s:52});const G=[['6.6 h','AI time, idle gaps over 15 min cut',TEAL,C[0]+.6],['10.6 h','by the clock, 20:34 to 07:08 UTC',MUT,C[0]+2.6],['914,981','output tokens',TEAL,C[1]+.2],['272.3 M','cache read tokens',TEAL,C[1]+2.6],['3.1 M','cache write tokens',MUT,C[1]+4.4],['463','model calls, one thread, no sub-agents',TEAL,C[1]+5.6]];+ G.forEach(([b,l,col,cu],i)=>{const x=60+(i%3)*610,y=160+Math.floor(i/3)*220,a=on(t,cu);c.save();c.globalAlpha=a;c.fillStyle=PANEL;rr([x+(1-a)*14,y,590,200],14);c.fill();c.strokeStyle=EDGE;c.stroke();c.restore();T(b,x+30,y+92,{f:'Fam',w:700,s:68,c:col==MUT?INK:col,a});P(l,x+30,y+148,530,{s:26,c:'#c3ccd6',a})});+ const a=on(t,C[0]+1);P('Counted from the session log, from the first prompt to the last report. Cutting the gaps also removes some background router time. This video is not included.',60,650,1800,{s:24,c:MUT,a});+ const n=on(t,C[2]);if(n>0){c.save();c.globalAlpha=n;c.fillStyle=PANEL;rr([60,726,1790,240],14);c.fill();c.strokeStyle=TEAL;c.lineWidth=2;c.stroke();c.restore();T('Next',96,790,{f:'Fam',w:700,s:40,c:TEAL,a:n});T('Measurements on this board: continuity, stackup, netlist',96,850,{s:32,a:n});T('A second board, then a decision about making it public',96,904,{s:32,a:n})}},+};+window.ready=(async()=>{D=await(await fetch('data.json')).json();const L=['top','bottom','timeline','raw_Driver1','raw_Driver2','raw_Driver3','raw_Driver4','outline-Driver1','outline-Driver3','prep-Driver3','pin1-U2','copper-islands-bottom','copper-islands-top','overlay-top','overlay-bottom','ratio-bottom','lines-bottom','p_073-bot','p_082-all','p_096-inner','p_097-inner','p_098-inner','p_097-all','p_101-bot','p_101-sch','f_rec6','f_rec10','f_rec15','f_rec73','f_rec81','f_rec96','f_rec97','f_rec101','f_sch101'];+ const need=window.NEED||L;await Promise.all(need.map(load));await document.fonts.load('700 30px Fam');await document.fonts.load('400 20px Sat');await document.fonts.load('500 20px Sat');await document.fonts.load('400 20px Jet');await document.fonts.load('500 20px Fam');await document.fonts.load('500 20px Jet');return 1})();+window.draw=(id,t,dur,lead)=>{const s=D.timing.find(x=>x.id===id);const C=s.starts.map(v=>v+lead);c.globalAlpha=1;c.fillStyle=BG;c.fillRect(0,0,1920,1080);SC[id](t,C,dur);const f=Math.min(ez(t/.25),ez((dur-t)/.25));if(f<1){c.save();c.globalAlpha=1-f;c.fillStyle=BG;c.fillRect(0,0,1920,1080);c.restore()}};+window.grab=()=>document.getElementById('c').toDataURL('image/jpeg',.94).slice(23);+</script>
docs/process-video/build/prep.pyadded+65
@@ -0,0 +1,65 @@+import json,glob,os,numpy as np,cv2+from PIL import Image,ImageEnhance+R='/home/adom/project/Driver_RE2/reverse-flow/'+# ---- pcb shots cropped to the board+ref=np.asarray(Image.open('shots/p/097-inner.png').convert('RGB')).astype(int)+bg=ref[5,5];m=(abs(ref-bg).sum(2)>25);ys=np.where(m.mean(1)>.25)[0];xs=np.where(m.mean(0)>.25)[0]+box=(xs[0],ys[0],xs[-1]+1,ys[-1]+1);print('board box',box,(box[2]-box[0])/(box[3]-box[1]))+for f in glob.glob('shots/p/*.png'):+    n=os.path.basename(f)[:-4];im=Image.open(f).convert('RGB')+    if n.endswith('sch'): im.save(f'a/p_{n}.jpg',quality=92);continue+    c=im.crop(box)+    if 'inner' in n:+        a=np.asarray(c).astype(float)/255;a=np.clip(a**.62*1.25,0,1);c=Image.fromarray((a*255).astype('uint8'))+    c.save(f'a/p_{n}.jpg',quality=92)+for f in glob.glob('shots/f/*.png'):Image.open(f).convert('RGB').save('a/f_'+os.path.basename(f)[:-4]+'.jpg',quality=92)+Image.open('shots/timeline.png').convert('RGB').save('a/timeline.jpg',quality=92)+# ---- evidence and photos+for n in ['outline-Driver1','outline-Driver3','prep-Driver3','pin1-U2','copper-islands-bottom','copper-islands-top','overlay-top','overlay-bottom']:+    Image.open(R+f'evidence/{n}.jpg').convert('RGB').save(f'a/{n}.jpg',quality=92)+for n in ['top','bottom']:Image.open(R+f'board/{n}.jpg').convert('RGB').save(f'a/{n}.jpg',quality=92)+for n in ['Driver1.JPG','Driver2.JPG','Driver3.JPG','Driver4.jpg']:+    im=Image.open(R+'photos/'+n).convert('RGB');im.thumbnail((1400,1400));im.save('a/raw_'+n[:7]+'.jpg',quality=90)+# ---- the reader's two signals, same formula as trace.rs, on the bottom photo+img=cv2.imread('a/bottom.jpg');g=img[:,:,1].astype(np.float32)+fine=cv2.blur(g,(7,7));wide=cv2.blur(g,(241,241));ratio=fine/np.maximum(wide,1)+v=np.clip((ratio-.7)/(1.15-.7),0,1);cv2.imwrite('a/ratio-bottom.jpg',(v*255).astype('uint8'),[cv2.IMWRITE_JPEG_QUALITY,92])+best=np.zeros_like(g);r=int(3*9)+ys,xs=np.mgrid[-r:r+1,-r:r+1].astype(np.float32)+for k in range(16):+    th=k*np.pi/16;u=xs*np.cos(th)+ys*np.sin(th);w=-xs*np.sin(th)+ys*np.cos(th)+    s1,s2=9.,2.6;K=np.exp(-u*u/(2*s1*s1))*(w*w/s2**4-1/s2**2)*np.exp(-w*w/(2*s2*s2));K-=K.mean();K/=np.abs(K).sum()+    best=np.maximum(best,cv2.filter2D(g,-1,K))+p=np.percentile(best[best>0],99);L=np.clip((best-.34*p)/(.75*p-.34*p),0,1)[...,None]+base=(img*.42).astype(np.float32);col=np.array([176,184,0],np.float32)  # teal, BGR+out=base*(1-L)+col*L;out=np.clip(out+60*L,0,255).astype('uint8');cv2.imwrite('a/lines-bottom.jpg',out,[cv2.IMWRITE_JPEG_QUALITY,92])+# ---- data+P=json.load(open(R+'parts.json'));P=P if isinstance(P,list) else P['parts']+parts=[dict(r=p['ref'],x=p['pos_mm'][0],y=p['pos_mm'][1],pads=p.get('snap',{}).get('method')=='pads',unk=any(v in('UNKNOWN','INFERRED') for v in p.get('fieldTags',{}).values())) for p in P]+pos={p['r']:(p['x'],p['y']) for p in parts}+def hull(pts):+    pts=sorted(set(pts))+    if len(pts)<3:return pts+    def cr(o,a,b):return (a[0]-o[0])*(b[1]-o[1])-(a[1]-o[1])*(b[0]-o[0])+    lo=[];up=[]+    for q in pts:+        while len(lo)>1 and cr(lo[-2],lo[-1],q)<=0:lo.pop()+        lo.append(q)+    for q in pts[::-1]:+        while len(up)>1 and cr(up[-2],up[-1],q)<=0:up.pop()+        up.append(q)+    return lo[:-1]+up[:-1]+blocks=[dict(id=b['id'],name=b['name'],n=len(b['parts']),pts=[pos[x] for x in b['parts'] if x in pos],hull=hull([pos[x] for x in b['parts'] if x in pos])) for b in json.load(open(R+'blocks.json'))['blocks']]+c=json.load(open(R+'copper.json'))+vias=[dict(x=v['pos_mm'][0],y=v['pos_mm'][1],k=('inner' if v.get('kind')=='inner' else 'both' if v.get('seen')=='both-faces' else 'v')) for v in c['vias']]+h=json.load(open(R+'detect/holes-top.json'))+runs=[]+import re+for e in json.load(open(R+'views.json'))['entries']:+    m=re.match(r'(\d)-layer route: (\d+) open, (\d+) new DRC',e.get('reason',''))+    if m and e['n']>=82:runs.append(dict(n=e['n'],l=int(m[1]),o=int(m[2]),d=int(m[3])))+T=json.load(open(R+'tests.json'));T=T if isinstance(T,list) else T['tests']+tests=[dict(id=t['id'],m=t['method'][:64]) for t in T]+order=sorted(tests,key=lambda t:(not t['id'].startswith(('T-layers','T-via','T-net','T-plane','T-zone')),t['id']))+print(len(runs),runs[0],runs[-1],[t['id'] for t in order[:12]],len(vias),sum(v['k']=='both' for v in vias))+json.dump(dict(parts=parts,blocks=blocks,vias=vias,holes=h['holes'],rows=[r for r in h['rows'] if r['pitch_mm']<3],runs=runs,tests=order,timing=json.load(open('vo/timing.json'))),open('a/data.json','w'))
docs/process-video/build/render.jsadded+19
@@ -0,0 +1,19 @@+const p=require(process.env.HOME+'/.cache/hero-studio/node_modules/puppeteer-core'),fs=require('fs'),cp=require('child_process'),http=require('http'),path=require('path');+const F=process.env.FF,T=JSON.parse(fs.readFileSync('a/data.json')).timing,MODE=process.argv[2]||'still',ONLY=process.argv[3]?process.argv[3].split(','):null;+const INTRO=5,VO_IN=1.8,LEAD=.1,TAIL=.3;+const durOf=s=>s.id==='prompt'?Math.max(4,s.speech[1]+VO_IN-INTRO+TAIL):s.id==='cost'?s.speech[1]+LEAD-1.2:s.speech[1]+LEAD+TAIL;+const leadOf=s=>s.id==='prompt'?(VO_IN-INTRO):LEAD;+const srv=http.createServer((q,r)=>{const f=path.join('a',decodeURIComponent(q.url.split('?')[0]));fs.readFile(f,(e,b)=>{if(e){r.writeHead(404);return r.end()}r.writeHead(200,{'content-type':f.endsWith('.html')?'text/html':f.endsWith('.json')?'application/json':f.endsWith('.woff2')?'font/woff2':'image/jpeg'});r.end(b)})}).listen(0,'127.0.0.1',async()=>{const port=srv.address().port;+ const b=await p.launch({headless:'new',executablePath:process.env.HOME+'/.cache/puppeteer/chrome/linux-150.0.7871.24/chrome-linux64/chrome',args:['--no-sandbox','--disable-gpu'],protocolTimeout:3600000});+ const list=T.filter(s=>!ONLY||ONLY.includes(s.id));+ const one=async s=>{const pg=await b.newPage();pg.on('pageerror',e=>console.error('ERR',s.id,e.message));await pg.setViewport({width:1920,height:1080});await pg.goto(`http://127.0.0.1:${port}/composer.html`);await pg.evaluate(()=>window.ready);+  const d=durOf(s),lead=leadOf(s),C=s.starts.map(v=>v+lead);+  if(MODE==='still'){const ts=[...C.map((c,i)=>Math.min(d-.3,(c+(C[i+1]??d))/2)),...(C.length>1?[Math.min(d-.4,C[C.length-1]+Math.max(1,(d-C[C.length-1])*.85))]:[])];+   for(let i=0;i<ts.length;i++){await pg.evaluate((id,t,d,l)=>window.draw(id,t,d,l),s.id,ts[i],d,lead);const j=await pg.evaluate(()=>window.grab());fs.writeFileSync(`shots/st_${s.id}_${i}.jpg`,Buffer.from(j,'base64'))}}+  else{const n=Math.round(d*30),ff=cp.spawn(F,['-y','-v','error','-f','image2pipe','-c:v','mjpeg','-r','30','-i','-','-c:v','libx264','-preset','medium','-crf','17','-pix_fmt','yuv420p','-r','30',`clips/${s.id}.mp4`],{stdio:['pipe','inherit','inherit']});+   for(let i=0;i<n;i++){const j=await pg.evaluate((id,t,d,l)=>{window.draw(id,t,d,l);return window.grab()},s.id,i/30,d,lead);if(!ff.stdin.write(Buffer.from(j,'base64')))await new Promise(r=>ff.stdin.once('drain',r))}+   ff.stdin.end();await new Promise(r=>ff.on('close',r));console.log('done',s.id,n)}+  await pg.close()};+ await Promise.all(list.map(one));await b.close();srv.close();+ fs.writeFileSync('clips/durs.json',JSON.stringify(T.map(s=>({id:s.id,title:s.title,d:Math.round(durOf(s)*30)/30}))));+});
docs/process-video/build/script.pyadded+20
@@ -0,0 +1,20 @@+SCENES=[+("prompt","The request",["It starts with four photos of a motor driver board, and one request.","Find the parts, the functional blocks, the pours and the routing.","Copy everything you can see.","Then work out what you can't, pick a layer count, and say how sure you are."]),+("flow","A new program",["The first attempt reused the existing AI Flow program, and Caleb called that out.","So this became its own Rust program, with its own copper reader and its own router.","The program does the measuring. The AI does the naming.","Eleven steps, and each one records what it produced."]),+("photos","Photos",["Two photos of the top, two of the bottom.","The program finds the board edge in each one and straightens it.","Then it turns the bottom over to line up with the top. Thirty eight holes agree, two don't."]),+("scale","Scale",["Scale comes from the board itself. The header rows have known pitches, two millimeters and two point five four.","That gives forty six point six pixels per millimeter.","The driver chip's six millimeter body settles which pitch standard the rows are."]),+("parts","Parts",["Then the parts, a hundred and thirty seven of them.","A hundred and three are placed by their own pads on the photo.","Markings get read where they're legible, like this T I DRV eighty three twenty three S.","Where they aren't, the program says so. Ninety one parts still need a test."]),+("blocks","Blocks",["Silkscreen and circuit role sort them into eleven blocks.","Power entry, the three phase power stage, bridge sensing, the logic supply.","The gate driver, the microcontroller, CAN, aux, the fan, motor temperature and the mounting holes."]),+("miss","The miss",["Then came the real miss. The first copy had fifty eight pours, all on the bottom, and not a single track.","Caleb looked at it and said fill and via recognition was completely lacking.","That was correct."]),+("copper","Reading the copper",["So the reader was rewritten. Under solder mask, copper looks a little lighter than bare laminate, compared with its own neighborhood.","Thin dark lines mark the gaps between copper.","Follow those gaps and the board falls apart into islands. One thousand four hundred forty four of them.","That became a hundred sixteen pours and three hundred fourteen tracks, on both faces."]),+("vias","Vias",["Vias are next. Two thousand six hundred eighty one candidates, and one thousand twenty eight kept.","They're tented on the top, so only forty five show on both faces.","Zoom in and the misses show. Up to a third land on something pale, mostly silkscreen letters and lines.","And the three read as leading only inward look like ordinary domes in a stitching field. So that's no proof of inner layers."]),+("copy","The copy",["All of that goes straight into a KiCad board, as photographed, before anything is guessed.","Here's the copy laid over the top photo, and over the bottom."]),+("nets","Nets",["Now for what nobody can see. Datasheets and the circuit fill in the rest of the nets. Seventy seven nets, two hundred forty nine connections to make.","Ninety nine pins disagree with the copper they seem to touch. Those get reported, not quietly fixed."]),+("router","The router",["Then the router. It's written for this job, and it uses every inner layer.","When photographed pours were hard walls, two hundred thirty five connections stayed open.","Real vias pass through pours, so seen copper became expensive to cross instead of forbidden.","Seventeen recorded runs later, six and eight layers are down to about thirty."]),+("layers","Layer count",["The layer count comes from how hard that is.","Four layers: fifty four open, and three hundred twenty seven millimeters cut through copper the photos show.","Six layers: thirty four open, twenty four millimeters.","Eight layers: twenty nine open, thirty millimeters. That's no real gain, since runs vary by about a quarter.","So the proposal is six. It's a measure of difficulty. Nobody has seen inside this board."]),+("rules","Rules",["The rules were set from the start: KiCad's defaults, point two millimeter track and clearance.","Most of what's left open is fan-out at the two Q F N chips.","The same copy at J L C style rules, point one two seven, leaves just two open at six layers.","So that's mostly a rules limit. That run has no valid design rule check yet."]),+("review","Review and tests",["Then it reviews itself, in a loop. Four passes, and the last one changes nothing.","Blocks and placement come out high. Identity, values, copper, vias and layers, medium.","Connectivity is low, and it says why.","Everything still unknown gets a test. A hundred and nineteen of them."]),+("notdone","Not done",["So this is not a finished board.","Thirty four connections stay open. KiCad counts seventy six missing, because some seen copper isn't joined in the copy.","The rails at the upper left of the bottom are largely missing, and signal bundles mostly read as gaps.","And the inner copper is one way to connect it, not what's actually inside."]),+("record","The record",["Every stage is saved in the flow window. A hundred and ten recorded steps you can replay."]),+("cost","Cost",["From the first prompt to the report took about six and a half hours of AI time, with idle gaps over fifteen minutes cut out.","Nine hundred fifteen thousand output tokens, and two hundred seventy two million cache reads, across four hundred sixty three model calls.","Next up: real measurements on this board, and a second board, before any of it goes public."]),+]
docs/process-video/build/tts.pyadded+27
@@ -0,0 +1,27 @@+import subprocess,json,re,concurrent.futures as cf,imageio_ffmpeg+from script import SCENES+F=imageio_ffmpeg.get_ffmpeg_exe()+def go(s):+    n,title,lines=s+    txt=' '.join(lines)+    subprocess.run(['adom-tts','say','--out',f'vo/{n}.mp3','--voice','en-US-AndrewNeural',txt],check=True,capture_output=True)+    subprocess.run([F,'-y','-v','error','-i',f'vo/{n}.mp3','-ar','48000','-ac','1',f'vo/{n}.wav'],check=True)+    e=subprocess.run([F,'-i',f'vo/{n}.wav','-af','silencedetect=noise=-40dB:d=0.12','-f','null','-'],capture_output=True,text=True).stderr+    st=[float(x) for x in re.findall(r'silence_start: ([\d.]+)',e)];en=[float(x) for x in re.findall(r'silence_end: ([\d.]+)',e)]+    D=re.search(r'Duration: (\d+):(\d+):([\d.]+)',e);dur=int(D[1])*3600+int(D[2])*60+float(D[3])+    # first and last speech+    t0=en[0] if st and st[0]<0.05 else 0.0+    gaps=[(a,b) for a,b in zip(st,en) if a>t0+.05]+    t1=dur+    if st and len(st)>len(en): t1=st[-1]; +    elif gaps and gaps[-1][1]>=dur-.02: t1=gaps[-1][0]; gaps=gaps[:-1]+    chars=[len(l) for l in lines];tot=sum(chars);cum=0;starts=[t0]+    for c in chars[:-1]:+        cum+=c;want=t0+(t1-t0)*cum/tot+        g=min(gaps,key=lambda g:abs((g[0]+g[1])/2-want)) if gaps else None+        starts.append(((g[0]+g[1])/2) if g and abs((g[0]+g[1])/2-want)<1.6 else want)+    return dict(id=n,title=title,lines=lines,starts=starts,speech=[t0,t1],dur=dur,gaps=len(gaps))+with cf.ThreadPoolExecutor(6) as x: R=list(x.map(go,SCENES))+json.dump(R,open('vo/timing.json','w'),indent=1)+for r in R: print(r['id'],round(r['dur'],1),[round(s,1) for s in r['starts']],r['gaps'])+print('total',sum(r['dur'] for r in R))
docs/process-video/process-video-icon.jpgadded
⋯ 1 unchanged line ⋯
docs/status.mdadded+43
@@ -0,0 +1,43 @@+# Status++Verified on one board only: the Driver motor controller (Driver 1 to 4 photos, 2026-11-04), rules `kicad-default`. Nothing here has been checked against a real netlist, stackup or X-ray.++## What it does on that board++| Measure | Result |+|---|---|+| Parts placed | 137, 103 by their own pads on the photo |+| Copper traced | 116 pours and 314 tracks on both faces, checked by eye against the photos |+| Vias kept | 1028 of 2681 candidates; 45 confirmed on both faces; up to 338 look false |+| Copy's own DRC, unrouted | 146 errors: 62 courtyard overlaps, 59 clearance, 30 edge, 3 solder mask bridge, 1 hole in a courtyard |++| Layers | Open of 249 | New DRC errors | Missing per KiCad | Vias added | Track mm | Crossing seen copper, mm |+|---|---|---|---|---|---|---|+| 4 | 54 | 17 | 84 | 210 | 1002 | 327 |+| 6 (proposed) | 34 | 6 | 76 | 230 | 1165 | 24 |+| 8 | 29 | 9 | 69 | 215 | 1187 | 30 |++Routing the same stackup again varies: 6 layers left 25 to 34 open across runs.++With the JLC 4-layer capabilities (0.127 mm track and clearance, 0.45/0.2 mm via), the same copy leaves 30 open at 4 layers and 2 at 6, crossing 385 mm and 39 mm. The open connections at default rules are almost all QFN fan-outs, so they are mainly a rules limit. KiCad's shared service checks only its default rules, so that run's DRC counts do not apply: its gate needs a native `kicad-cli`.++## Known gaps++- **No stackup closes every connection at default rules.**+- **False vias.** Up to 338 of the 1028 kept vias land on pale pixels (a 0.35 mm patch more than a quarter pale), mostly silkscreen letters and lines. They feed net assignment and block the router. The via filter does not reject silkscreen yet.+- **Inner vias are not proof.** The 3 read as leading only inward look like ordinary domes in a stitching field, so the photos prove no minimum layer count.+- **Scale has one source.** The body check only picks the pitch standard (41 px/mm against 46.6), so the number rests on the hole rows alone.+- **The saved snapshot is out of date.** The last one predates the final copper.+- **Bottom's upper-left rails are largely missing** from the copy. The photo is darker there and the islands fragment.+- **Signal bundles on the bottom** come out mostly as gaps in the pour, not separate tracks. Tracks of unknown net shorter than 0.8 mm are dropped, 637 fragments in all.+- **Pad connections are weak evidence.** The ring test does not reliably tell a joined plated pad from an isolated one on these photos. 99 pins disagree with the copper they appear to touch, and 4 net pairs meet at vias.+- **827 mm² of pour and 94 mm of track have no net.**+- **Via sizes are the rule's**, not measured.+- **Routed copper is one way to make the connections**, not what is inside the board. Inner layers are never seen.+- **Tracks do not yet claim copper of unknown net.** The router pays to cross it where it could be following it.+- **KiCad refilling zones** (B key) will flood traced pours, because the outline cannot carry a pour's holes. The written fill is the measured one.+- **Only tested on one board.** Thresholds came from these photos.++## Before making it public++Fix the via filter, verify against a second board and a real netlist or X-ray, and decide the rules question above.
package.jsonadded+82
@@ -0,0 +1,82 @@+{+  "slug": "ai-flow-reverse-engineering",+  "type": "app",+  "version": "0.2.0",+  "title": "AI Flow: Reverse Engineering",+  "description": "Reverse engineer a PCB from photos: identify the parts and functional blocks, trace every pour, track and via from the pictures into a KiCad copy, route the connections nobody can see on its own N-layer router, jump the layer count until they close and the DRC passes, review in a loop with a confidence level per category, and list the tests that settle every unknown. Every claim carries an evidence tag (seen, measured, datasheet, inferred, unknown). Rust source on this page; build it with ./build.sh.",+  "category": "Developer Tools",+  "license": "MIT",+  "bin": {+    "adom-aiflow-reverse": "bin/adom-aiflow-reverse"+  },+  "dependencies": {+    "adom/service-kicad": "*",+    "adom/adom-2dboard": "*",+    "adom/adom-schematic": "*"+  },+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  },+  "bundled": true,+  "hero": {+    "type": "image",+    "path": "screenshots/hero.png"+  },+  "brief": "Photos of a PCB to a tagged KiCad copy with its traced copper, a layer-count proposal and the tests that settle what the photos cannot show.",+  "discovery_pitch": "Reach for this when someone has photos of a board and wants it identified, copied into KiCad, or wants to know how many layers it has and what is inside.",+  "discovery_triggers": [+    "reverse engineer this board",+    "reverse engineer a pcb from photos",+    "clone this board",+    "copy this pcb into kicad",+    "recreate the board from pictures",+    "identify the parts on this pcb",+    "how many layers is this board",+    "infer the inner layers",+    "pcb teardown",+    "board photos to kicad",+    "adom-aiflow-reverse"+  ],+  "sample_prompts": [+    {+      "label": "Reverse engineer from photos",+      "prompt": "Reverse engineer the board in Driver1.JPG to Driver4.jpg into KiCad and tell me how confident you are"+    },+    {+      "label": "Layer count",+      "prompt": "From these two photos, how many layers does this board have and what are the inner layers?"+    },+    {+      "label": "Test list",+      "prompt": "List the bench tests that would confirm everything you inferred about this board"+    }+  ],+  "tags": [+    "reverse engineering",+    "pcb",+    "kicad",+    "teardown",+    "photos",+    "layer count",+    "ai flow"+  ],+  "org": "adom",+  "files": [+    "README.md",+    "package.json",+    "Cargo.toml",+    "Cargo.lock",+    "LICENSE",+    "build.sh",+    "install.sh",+    "uninstall.sh",+    "src/**",+    "flows/**",+    "assets/**",+    "skills/**",+    "docs/method.md",+    "docs/status.md",+    "bin/**"+  ]+}\ No newline at end of file