AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
Wait for recreated tour frames and document small-component marking checks #10
The library tour could fail immediately after Hydrogen recreated its tab, because its readiness loop asserted on the transient missing-frame response. Retry that specific response during readiness; renderer and recording failures still refuse. This fixes issue #19.
Also document lessons from the full 40-part ESC marking run: fit text to the body rather than raised terminals, reject text overflow, preserve colours and plain variants, compare orientations, and cache lettering separately when base-model redistribution remains unresolved.
Validated by recording the complete marked library after a recreated-tab event: 40 models, 80.67-second detailed tour and 3.17-second moving overview. Contact sheets inspected; published wiki WebView2 playback decoded 43 frames, advanced to 1.62 seconds, 1280x720, no media error. Native KiCad resolved all 128 expected model references, and the successful second viewer attempt visibly showed marked chips on the actual board. This is an additive client helper/docs change; released final-compose selection remains separate.
Diff Skip to comments
@@ -0,0 +1,55 @@+#!/usr/bin/env python3+"""Build/serve an actual moving 3D library tour; keep detail separate from final overview."""+import argparse,pathlib,json,hashlib,shutil,http.server,subprocess,urllib.request,time,os,sys,struct+p=argparse.ArgumentParser();p.add_argument('action',choices=['build','serve','record','verify']);p.add_argument('--ai-thread');p.add_argument('--run',type=pathlib.Path);p.add_argument('--manifest',type=pathlib.Path);p.add_argument('--out',type=pathlib.Path,required=True);p.add_argument('--port',type=int,default=8878);a=p.parse_args();a.out=a.out.resolve()+def digest(path):return hashlib.sha256(path.read_bytes()).hexdigest()+if a.action=='build':+ if not a.manifest:p.error('--manifest is required for build')+ src=json.loads(a.manifest.read_text());assert src.get('eda') in ['kicad','altium','fusion'],'explicit target EDA required';assert src.get('mpnMarking') in ['on','off'],'resolve the marking preference before building the selected library'+ rows=src.get('components',[]);assert rows,'empty component library';seen=set();a.out.mkdir(parents=True,exist_ok=True);(a.out/'models').mkdir(exist_ok=True);result=[]+ for r in rows:+ assert r['mpn'] not in seen,'duplicate component identity';seen.add(r['mpn']);selected=r['selectedModel'];expected='mpn' if src['mpnMarking']=='on' else 'plain';assert selected['variant']==expected,f"{r['mpn']}: tour/board variant mismatch; no silent plain fallback"+ for k in ['step','glb']:+ f=pathlib.Path(selected[k]);f=f if f.is_absolute() else a.manifest.parent/f;assert f.is_file() and f.stat().st_size>0,f'missing {k}: {r["mpn"]}';assert digest(f)==selected[k+'Sha256'],f'stale {k} hash: {r["mpn"]}'+ raw=f.read_bytes();assert raw[:4]==b'glTF' and struct.unpack_from('<I',raw,8)[0]==len(raw) and raw[16:20]==b'JSON',f'invalid GLB bytes: {r["mpn"]}'+ assert r.get('wiki') and r.get('provenance'),'wiki/provenance link required';glb=pathlib.Path(selected['glb']);glb=glb if glb.is_absolute() else a.manifest.parent/glb;name=digest(glb)+'.glb';shutil.copy2(glb,a.out/'models'/name);result.append({'mpn':r['mpn'],'wiki':r['wiki'],'provenance':r['provenance'],'glb':'models/'+name,'variant':expected,'stepSha256':selected['stepSha256'],'glbSha256':selected['glbSha256']})+ obj={'schemaVersion':1,'eda':src['eda'],'mpnMarking':src['mpnMarking'],'components':result,'secondsPerComponent':2,'projectUrl':src.get('projectUrl'),'selectionSha256':digest(a.manifest),'nativeLibraryEvidence':src.get('nativeLibraryEvidence'),'finalVideoPolicy':{'overviewSeconds':5,'detailIsSeparate':True,'finalMaximumSeconds':120,'wikiScrollSnippets':'optional; separate from default overview'}};(a.out/'tour.json').write_text(json.dumps(obj,indent=2));shutil.copy2(pathlib.Path(__file__).with_suffix('.html'),a.out/'index.html');print(f'Built {len(rows)}-component tour for {src["eda"]}; selected variants {src["mpnMarking"]}; this does not certify native library installation.')+elif a.action=='serve':+ assert (a.out/'tour.json').exists(),'build first'+ class Handler(http.server.SimpleHTTPRequestHandler):+ def __init__(self,*args,**kwargs):super().__init__(*args,directory=str(a.out),**kwargs)+ def do_POST(self):+ if self.path not in ['/recordings/detail.webm','/recordings/overview.webm']:self.send_error(404);return+ size=int(self.headers.get('Content-Length','0'))+ if size<=0 or size>256*1024*1024:self.send_error(413);return+ dest=a.out/self.path.lstrip('/');dest.parent.mkdir(exist_ok=True);dest.write_bytes(self.rfile.read(size));self.send_response(200);self.end_headers();self.wfile.write(b'OK')+ def log_message(self,*args):pass+ print(f'Library tour serving on http://localhost:{a.port}/',flush=True);http.server.ThreadingHTTPServer(('127.0.0.1',a.port),Handler).serve_forever()+elif a.action=='record':+ control=pathlib.Path.home()/'.adom/hydrogen-control-url';assert control.exists(),'Hydrogen control unavailable; open the tour in Pup and use the recording controls, then verify';base=control.read_text().strip();url=f'http://localhost:{a.port}/';subprocess.run(['adom-cli','hydrogen','webview','open-or-refresh','--name','Component library tour','--url',url],check=True)+ def evaluate(js,waitForFrame=False):+ q={'target':url,'js':js};r=json.load(urllib.request.urlopen(urllib.request.Request(base+'/eval-in',data=json.dumps(q).encode(),headers={'Content-Type':'application/json'}),timeout=20));+ if not r.get('ok') and waitForFrame and 'frame/context not found' in str(r.get('error')):return {'ready':False}+ assert r.get('ok'),r;return r.get('value')+ deadline=time.monotonic()+180+ while time.monotonic()<deadline:+ state=evaluate('window.libraryTour?.stats?.() || {ready:false,error:window.libraryTour?.error}',True)+ if state and state.get('error'):raise RuntimeError(state)+ if state and state.get('ready'):break+ time.sleep(2)+ else:raise RuntimeError('Tour did not load; no recording accepted')+ for kind in ['detail','overview']:+ target=a.out/'recordings'/(kind+'.webm');old=target.stat().st_mtime_ns if target.exists() else 0;evaluate(f"void window.libraryTour.record({json.dumps(kind)})");deadline=time.monotonic()+max(60,len(json.load(open(a.out/'tour.json'))['components'])*2+60)+ while time.monotonic()<deadline:+ time.sleep(2);state=evaluate('window.libraryTour.stats()')+ if state.get('error'):raise RuntimeError(state)+ if target.exists() and target.stat().st_mtime_ns!=old and not state['recording']:break+ else:raise RuntimeError('Recording did not complete')+ print('Recorded',kind,flush=True)+ subprocess.run([sys.executable,__file__,'verify','--out',str(a.out)],check=True)+elif a.action=='verify':+ src=json.load(open(a.out/'tour.json'));clips=[]+ for kind in ['detail','overview']:+ raw=a.out/'recordings'/(kind+'.webm');assert raw.exists(),f'missing {kind} recording';mp4=raw.with_suffix('.mp4');subprocess.run(['ffmpeg','-y','-v','error','-i',str(raw),'-an','-vf','fps=24,scale=1280:720','-c:v','libx264','-preset','veryfast','-crf','28','-pix_fmt','yuv420p','-movflags','+faststart',str(mp4)],check=True);meta=json.loads(subprocess.check_output(['ffprobe','-v','error','-show_format','-show_streams','-of','json',str(mp4)]));seconds=float(meta['format']['duration']);assert mp4.stat().st_size<10_000_000,'clip exceeds page cap; reduce bitrate before publishing';assert int(meta['streams'][0]['nb_frames'])<=seconds*25+2,'unexpected frame duplication';expected=5 if kind=='overview' else 2*len(src['components']);assert abs(seconds-expected)<3,f'{kind}: wrong duration {seconds}';sheet=raw.with_name(kind+'-sheet.png');subprocess.run(['ffmpeg','-y','-v','error','-i',str(mp4),'-vf',f'fps=1/{max(seconds/12,.1)},scale=320:-2,tile=4x3','-frames:v','1',str(sheet)],check=True);clips.append({'kind':kind,'raw':str(raw),'file':str(mp4),'seconds':seconds,'sha256':digest(mp4),'contactSheet':str(sheet),'visualReview':'pending','useInFinal':kind=='overview'})+ (a.out/'clips.json').write_text(json.dumps({'selectionSha256':src['selectionSha256'],'mpnMarking':src['mpnMarking'],'clips':clips,'finalMaximumSeconds':120,'notes':'Review both contact sheets and playback before registration; only the overview belongs in the final cut by default.'},indent=2));print(json.dumps(clips,indent=2))+@@ -0,0 +1,32 @@+# Component library tours++Review the library early, before placement, in the actual EDA selected by the user. Match symbols, footprints and model paths in that EDA; a GLB gallery alone is not native library validation.++Use one reviewed manifest (`library.json`) to select every component's exact STEP and GLB hashes and its plain or optional MPN-marked variant. If marking is on, a missing marked asset must be resolved or the user must explicitly change that choice. Never silently show plain models in the tour when the board is expected to use marked ones. Preserve the original package frame, and fit marking along the longest usable top-face direction, with pin-1 clearance.++The additive `tools/library-tour.py` helper builds an interactive Adom 3D viewer from that manifest, then records two distinct clips: a two-second orbit per component and a five-second moving overview. Preview bodies are normalized in size for visibility; source CAD is unchanged. Keep the long walkthrough as a separate review artifact. The final video should include only the short overview by default and remain at most two minutes. Optional brief scrolling component wiki shots belong in a separate library tour when a large BOM would otherwise dominate the final cut.++`build --manifest library.json --out library-tour`, `serve --out library-tour --port 8878`, `record --out library-tour --port 8878`, then inspect both contact sheets and play the clips. Supply your run and thread to the helper. Hash/GLB validation and successful loading are not enough: reject blank, clipped, wrong-variant or motionless clips. `clips.json` deliberately leaves visual review pending. Register approved clips only after native EDA binding and visible review.++Release integration note: the published source snapshot lacks the installed release's compose and tour implementations (issue #16). This helper is independently runnable; wiring its approved overview into released compose must occur in the complete maintained release tree. Do not replace the installed production binary with the incomplete snapshot or claim that integration is already released.++The early plan now exposes `components → libraries → models → library-tour → placement`+as distinct visits. `libraries` covers native symbol/pad/model binding; `models`+checks the board's actual selected models; `library-tour` records their visual review.+An existing schematic's library repair is different from later requirements-driven+part selection. These are AI-owned steps, not new automated qualification gates.++When marking small components, verify the actual text bounds and surface: a+successful etcher job may still overflow a 0402 body or choose a raised terminal.+Fit to the upper body with pin-1/polarity and terminal clearance, compare both+orientations, preserve the source document's materials, and retain a separate+plain model. Never impose a minimum font size that pushes text outside the part.+Narrow annular contacts may only support tiny identification text; state that limit.+Keep generated lettering as a separate STEP layer when base redistribution rights+are unresolved; do not call an etched restricted model independently authored.++After opening or recreating a Hydrogen tour tab, retry a missing frame during the+bounded readiness wait. A frame-not-found result before mounting is not a renderer+failure. Actual loading/recording errors must still fail; verify decoded frames+and advancing playback time on the published wiki video.+
Comments
Log in to comment.
No comments yet.