main
John Lauer Document concept registration and six-clip narrated demo storyboard pending review 4c0572c 1d ago
"""Build separately reviewable narrated clips; do not concatenate before storyboard review."""
from pathlib import Path
import subprocess,json,concurrent.futures
P=Path(__file__).resolve().parent
raw=lambda stamp:str(P/'raw'/f'window-10488978-20261007-{stamp}.mp4')
def duration(f):return float(subprocess.check_output(['ffprobe','-v','error','-show_entries','format=duration','-of','default=nw=1:nk=1',str(f)]))
scenes=[
 ('01','Start with the buck molecule','151404',[(15,55,1)],'Buck wiki and requirements; idle sections removed.'),
 ('02','Choose a commercial instrument','153501',[(4,49,1)],'Compare listed prices and CAD availability on the published selection page.'),
 ('03','Inspect the carrier concept','153044',[(0,20,2),(26,60,1)],'Orbit at 2x for the first ten seconds, then inspect the exploded carrier at normal speed.'),
 ('04','Set a constant-current load','153637',[(0,42,1)],'Simulation: enable 0.1 A, step to 0.5 A and 1 A, then return to 0.1 A.'),
 ('05','Check limits and export data','153637',[(46,90,1)],'Simulation: reject 1.5 A, turn OFF, export CSV, then inspect/reset the model.'),
 ('06','Publish the reusable equipment package','153350',[(0,47,1)],'Public wiki: inspect package contents, launch instructions, budget and qualification status.')]

def build(scene):
 n,title,stamp,segments,description=scene;audio=P/'narration'/f'{n}.mp3';target=duration(audio)+.6
 chains=[]
 for i,(start,end,speed) in enumerate(segments):chains.append(f'[0:v]trim=start={start}:end={end},setpts=(PTS-STARTPTS)/{speed}[v{i}]')
 chains.append(''.join(f'[v{i}]' for i in range(len(segments)))+f'concat=n={len(segments)}:v=1:a=0[join]')
 label=title.replace("'",'')
 vf=f"[join]scale=1920:1000:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:55:color=0x0d1117,setsar=1,fps=30,drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf:text='{n} / {label}':x=36:y=15:fontsize=28:fontcolor=white"
 if n=='03':vf+=",drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf:text='2x playback':x=w-tw-36:y=16:fontsize=25:fontcolor=0x00b8b1:enable='lt(t,10)'"
 vf+=f',tpad=stop_mode=clone:stop_duration=1,trim=duration={target}[v]';chains.append(vf)
 chains.append(f'[1:a]loudnorm=I=-16:TP=-1.5:LRA=11,apad,atrim=duration={target}[a]')
 out=P/'clips'/f'{n}.mp4'
 cmd=['ffmpeg','-nostdin','-y','-v','error','-threads','2','-i',raw(stamp),'-i',str(audio),'-filter_complex_threads','2','-filter_complex',';'.join(chains),'-map','[v]','-map','[a]','-c:v','libx264','-threads','2','-preset','veryfast','-crf','22','-pix_fmt','yuv420p','-g','30','-keyint_min','30','-c:a','aac','-b:a','160k','-movflags','+faststart',str(out)]
 subprocess.run(cmd,check=True,stdin=subprocess.DEVNULL)
 print(n,duration(out),flush=True)
 return {'id':n,'title':title,'description':description,'file':f'clips/{n}.mp4','narration':f'narration/{n}.txt','duration':duration(out),'source':f'raw/window-10488978-20261007-{stamp}.mp4','segments':[{'start':s,'end':e,'speed':v} for s,e,v in segments]}
if __name__=='__main__':
 with concurrent.futures.ThreadPoolExecutor(max_workers=2) as ex:clips=list(ex.map(build,scenes))
 (P/'storyboard.json').write_text(json.dumps({'status':'awaiting per-clip review; not concatenated','duration_seconds':sum(c['duration'] for c in clips),'clips':clips},indent=2))