main
ae007 Project page: AFS_Adom rev A (Rice Eclipse AFS clean-room AI redesign): highlight reel and narrated process video with… cbe460e 7d ago
"""AFS_Adom project reel: title card, routing build-up (gen/buildup.py), then real recorded step clips from the aiflow run
(pours filling, each power net lit, silkscreen going on, Adom Fields current/heat, the finished board in the KiCad 3D
viewer), end card. Each segment normalised to 1920x1080 30 fps with a caption bar; chapters written for ::adom-video."""
import json, os, subprocess
from PIL import Image, ImageDraw, ImageFont
S = '/tmp/claude-1001/-home-adom-project/8819fb5f-c2c6-4524-9528-99428cd6b88e/scratchpad'; R = S + '/AFS_Adom/aiflow/run/'; O = S + '/reel/'
FONT = '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'; FONT2 = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'
def card(name, lines, secs):
    im = Image.new('RGB', (1920, 1080), (10, 12, 18)); d = ImageDraw.Draw(im); y = 360
    for text, size, col in lines:
        f = ImageFont.truetype(FONT if size >= 50 else FONT2, size); w = d.textlength(text, font=f); d.text(((1920 - w) / 2, y), text, font=f, fill=col); y += size + 28
    im.save(O + name + '.png')
    subprocess.run(['ffmpeg', '-loglevel', 'error', '-y', '-loop', '1', '-t', str(secs), '-framerate', '30', '-i', O + name + '.png', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '20', O + name + '.mp4'], check=True)
def esc(t): return t.replace(':', '\\:').replace("'", "’")
def clip(name, src, start, dur, speed, title, sub, crop=None):
    vf = []
    if crop: vf.append('crop=%s' % crop)
    vf += ['setpts=PTS/%g' % speed, 'scale=1920:1080:force_original_aspect_ratio=decrease', 'pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=0x0a0c12', 'fps=30',
           "drawbox=x=0:y=0:w=iw:h=150:[email protected]:t=fill",
           "drawtext=fontfile=%s:text='%s':x=60:y=34:fontsize=46:fontcolor=white" % (FONT, esc(title)),
           "drawtext=fontfile=%s:text='%s':x=60:y=96:fontsize=28:fontcolor=0xb4bec8" % (FONT2, esc(sub))]
    subprocess.run(['ffmpeg', '-loglevel', 'error', '-y', '-ss', str(start), '-t', str(dur), '-i', R + src, '-an', '-vf', ','.join(vf), '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '21', O + name + '.mp4'], check=True)
def dur(p): return float(subprocess.run(['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', p], capture_output=True, text=True).stdout)
card('seg0_title', [('AFS_Adom', 96, (255, 255, 255)), ('Active fin stabilization flight computer', 44, (220, 225, 235)),
                    ('RP2350B  +  4 sensors on SPI  +  8 servos  +  2S LiPo  |  4-layer, 92 x 68 mm Adom molecule', 30, (170, 180, 190)),
                    ('designed, routed, analysed and exported by an AI through KiCad', 30, (120, 200, 150))], 4)
clip('seg2_pours', 'window-2165406-20261016-033209-action.mp4', 0, 12.5, 1.0, 'Copper pours land live in KiCad', 'GND on both faces, the 2 mm servo rail, VSYS, 5 V and 3.3 V patches, each DRC-checked before it lands')
clip('seg3_nets', 'window-2165406-20261016-141020-action.mp4', 0, 6.0, 0.8, 'Every power net, lit as a whole', '+3V3, +5V, servo rail (VBAT), battery input, VSYS, GND, the RP2350 core rail')
clip('seg4_silk', 'window-2165406-20261016-141115-action.mp4', 0, 34.2, 2.5, 'Silkscreen goes on', '294 labels at PCBWay’s 0.8 mm minimum, contacts and header pins named on both faces')
clip('seg5_fields', 'window-6686400-20261016-135340-10x.mp4', 0, 58.4, 2.0, 'Current and heat, solved on the real copper', 'Adom Fields: every loaded net under 60 A/mm2, hottest part +27 C in still air')
clip('seg6_3d', 'window-2949872-20261016-172334.mp4', 0, 24.9, 1.4, 'The finished board', 'KiCad 3D viewer, driven by menu commands only: top, tilt, orbit, close-up, edge, bottom')
card('seg7_end', [('AFS_Adom rev A', 64, (255, 255, 255)), ('0 DRC errors  |  0 unconnected  |  493 / 493 pins match the schematic', 34, (220, 225, 235)),
                  ('Gerbers, drill, pick-and-place, BOM, STEP and the full design notes on this page', 30, (170, 180, 190))], 4)
order = ['seg0_title', 'seg1_buildup', 'seg2_pours', 'seg3_nets', 'seg4_silk', 'seg5_fields', 'seg6_3d', 'seg7_end']
titles = ['AFS_Adom', 'Routing builds up', 'Pours land live', 'Power nets', 'Silkscreen', 'Current and heat (Adom Fields)', 'The finished board in 3D', 'Results']
open(O + 'list.txt', 'w').write(''.join("file '%s%s.mp4'\n" % (O, n) for n in order))
subprocess.run(['ffmpeg', '-loglevel', 'error', '-y', '-f', 'concat', '-safe', '0', '-i', O + 'list.txt', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '22', '-movflags', '+faststart', O + 'afs-adom-reel.mp4'], check=True)
t = 0.0; chapters = []
for n, ti in zip(order, titles):
    chapters.append({'t': round(t, 1), 'title': ti}); t += dur(O + n + '.mp4')
subprocess.run(['ffmpeg', '-loglevel', 'error', '-y', '-ss', '16', '-i', O + 'afs-adom-reel.mp4', '-frames:v', '1', '-q:v', '3', O + 'afs-adom-reel-poster.jpg'], check=True)
json.dump(chapters, open(O + 'chapters.json', 'w'), indent=1); print('reel', round(t, 1), 's', chapters)