Setting the file. One moment.
Check Rail Climax · Embedded Captions · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page 14.42
Hershey Script1
Lines
233 scripts/ check-rail-climax.cjs
JavaScript · 233 lines · 8 KB
15 * determine (no rail.html, no puppeteer, timeline never registers, no climax)
16 * exits 0 — infra problems never block a render.
17 */
18 const path = require ( "path" );
19 const fs = require ( "fs" );
20 const os = require ( "os" );
21
22 const HF_ROOTS = [
23 process.env. HYPERFRAMES_ROOT ,
24 path. resolve (__dirname, "../../.." ),
25 path. join (os. homedir (), "Downloads" , "hyperframes" ),
26 ]. filter (Boolean);
27
28 function findInBun ( root , pkg , sub ) {
29 const cands = [path. join (root, "node_modules" , pkg)];
30 const bunDir = path. join (root, "node_modules" , ".bun" );
31 try {
32 if (fs. existsSync (bunDir))
33 for ( const d of fs. readdirSync (bunDir))
34 if (d. startsWith (pkg + "@" )) cands. push (path. join (bunDir, d, "node_modules" , pkg));
35 } catch {
36 /* ignore */
37 }
38 for ( const c of cands) {
39 const p = sub ? path. join (c, sub) : c;
40 if (fs. existsSync (p)) return p;
41 }
42 return null ;
43 }
44
45 let puppeteer = null ,
46 gsapSource = null ;
47 for ( const root of HF_ROOTS ) {
48 if ( ! puppeteer) {
49 const p = findInBun (root, "puppeteer" );
50 if (p) {
51 try {
52 puppeteer = require (p);
53 } catch {}
54 }
55 }
56 if ( ! gsapSource) {
57 const g = findInBun (root, "gsap" , path. join ( "dist" , "gsap.min.js" ));
58 if (g) gsapSource = fs. readFileSync (g, "utf8" );
59 }
60 }
61
62 const norm = ( s ) =>
63 String (s)
64 . toLowerCase ()
65 . replace ( / [ ^ a-z0-9] / g , "" );
66 const toks = ( s ) =>
67 String (s)
68 . split ( / \s + / )
69 . map (norm)
70 . filter (( t ) => t. length >= 2 );
71
72 function ok ( msg ) {
73 if (msg) console. log (msg);
74 process. exit ( 0 );
75 } // can't-determine / pass → never blocks
76 function fail ( msg ) {
77 console. error (msg);
78 process. exit ( 2 );
79 }
80
81 async function newPage ( browser , W , H ) {
82 const page = await browser. newPage ();
83 await page. setViewport ({ width: W , height: H , deviceScaleFactor: 1 });
84 if (gsapSource) {
85 await page. evaluateOnNewDocument (gsapSource);
86 await page. setRequestInterception ( true );
87 page. on ( "request" , ( req ) => {
88 const u = req. url ();
89 if (req. resourceType () === "script" && /gsap/ i . test (u) && / ^ https ? :/ i . test (u)) req. abort ();
90 else req. continue ();
91 });
92 }
93 return page;
94 }
95 async function load ( page , file ) {
96 await page. goto ( `file://${ file }` , { waitUntil: "load" , timeout: 15000 });
97 const start = Date. now ();
98 while (Date. now () - start < 12000 ) {
99 if ( await page. evaluate (() => !! (window.__timelines && window.__timelines.main))) {
100 try {
101 await page. evaluate ( async () => {
102 await document.fonts.ready;
103 });
104 } catch {}
105 return true ;
106 }
107 await new Promise (( r ) => setTimeout (r, 150 ));
108 }
109 return false ;
110 }
111 // visible text of a selector at time t, by seeking the page's main timeline.
112 // Visibility must be EFFECTIVE (own opacity × every ancestor's): a rail line that
113 // fades out as a CONTAINER leaves its word spans at opacity:1 — checking only the
114 // span's own style false-positives on text that is actually invisible.
115 async function visibleAt ( page , t , selector , childSel ) {
116 return page. evaluate (
117 ( t , selector , childSel ) => {
118 const tl = window.__timelines.main;
119 tl. seek (t);
120 void document.body.offsetHeight;
121 const eff = ( el ) => {
122 let o = 1 ,
123 n = el;
124 while (n && n.nodeType === 1 ) {
125 const cs = getComputedStyle (n);
126 if (cs.display === "none" || cs.visibility === "hidden" ) return 0 ;
127 o *= + cs.opacity;
128 n = n.parentElement;
129 }
130 return o;
131 };
132 const out = [];
133 for ( const el of document. querySelectorAll (selector)) {
134 if ( eff (el) < 0.05 ) continue ;
135 const kids = childSel ? [ ... el. querySelectorAll (childSel)] : [];
136 if (kids. length ) {
137 for ( const k of kids) {
138 if ( eff (k) > 0.05 ) out. push (k.textContent);
139 }
140 } else if ((el.textContent || "" ). trim ()) out. push (el.textContent);
141 }
142 return out. join ( " " ). trim ();
143 },
144 t,
145 selector,
146 childSel,
147 );
148 }
149
150 async function main () {
151 const project = path. resolve (process.argv[ 2 ] || "" );
152 if ( ! process.argv[ 2 ]) ok ( "[rail-climax] usage: check-rail-climax.cjs <project-dir>" );
153 const indexPath = path. join (project, "index.html" );
154 const railPath = path. join (project, "rail.html" );
155 if ( ! fs. existsSync (railPath) || ! fs. existsSync (indexPath))
156 ok ( "[rail-climax] no rail.html+index.html — not Standard, skipping" );
157 if ( ! puppeteer) ok ( "[rail-climax] puppeteer unavailable — skipping (set HYPERFRAMES_ROOT)" );
158
159 const exe =
160 process.platform === "darwin"
161 ? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
162 : "/usr/bin/google-chrome" ;
163 let browser;
164 try {
165 browser = await puppeteer. launch ({
166 headless: "new" ,
167 executablePath: fs. existsSync (exe) ? exe : undefined ,
168 args: [ "--disable-web-security" , "--allow-file-access-from-files" , "--disable-dev-shm-usage" ],
169 });
170 } catch (e) {
171 ok ( `[rail-climax] could not launch Chromium — skipping (${ e . message })` );
172 }
173
174 try {
175 // index.html → climax window + climax word tokens
176 const ip = await newPage (browser, 1920 , 1080 );
177 if ( ! ( await load (ip, indexPath)))
178 ok ( "[rail-climax] index timeline never registered — skipping" );
179 const dur =
180 + ( await ip. evaluate (() => {
181 const r = document. querySelector ( "#root" ) || document. querySelector ( "[data-duration]" );
182 return (r && r.dataset && r.dataset.duration) || 0 ;
183 })) || 30 ;
184 const STEP = 0.1 ;
185 const climaxTokens = new Set ();
186 let winIn = null ,
187 winOut = null ;
188 for ( let t = 0 ; t <= dur + 0.001 ; t += STEP ) {
189 const txt = await visibleAt (ip, + t. toFixed ( 2 ), ".climax" , "span,.w" );
190 if (txt) {
191 toks (txt). forEach (( w ) => climaxTokens. add (w));
192 if (winIn === null ) winIn = t;
193 winOut = t;
194 }
195 }
196 if ( ! climaxTokens.size || winIn === null )
197 ok ( "[rail-climax] no visible climax found in index.html — skipping" );
198
199 // rail.html → words visible during [winIn, winOut]
200 const rp = await newPage (browser, 1920 , 1080 );
201 if ( ! ( await load (rp, railPath))) ok ( "[rail-climax] rail timeline never registered — skipping" );
202 const hits = new Map (); // token -> first t seen
203 for ( let t = Math. max ( 0 , winIn - STEP ); t <= winOut + 0.001 ; t += STEP ) {
204 const railToks = new Set ( toks ( await visibleAt (rp, + t. toFixed ( 2 ), ".w" , null )));
205 for ( const w of climaxTokens) if (railToks. has (w) && ! hits. has (w)) hits. set (w, + t. toFixed ( 2 ));
206 }
207
208 if (hits.size) {
209 const list = [ ... hits. entries ()]
210 . map (([ w , t ]) => `"${ w }" (rail shows it at t=${ t }s)` )
211 . join ( ", " );
212 fail (
213 `[rail-climax] ✗ DUPLICATE PROMOTED WORD: the climax word(s) ${ [ ... climaxTokens ]. map (( w ) => `"${ w }"` ). join ( ", " ) } ` +
214 `are on screen during the climax window [${ winIn . toFixed ( 2 ) }–${ winOut . toFixed ( 2 ) }s], ` +
215 `and the rail ALSO reveals: ${ list }. \n ` +
216 ` The promoted word must be handed off, never duplicated — see PIPELINE.md "Rail ↔ climax hand-off": \n ` +
217 ` freeze the rail before the promoted word, let the climax carry it, hold the climax across the rail's \n ` +
218 ` page-flip, and exit it at the end of the thought. The rail must never reveal the promoted word.` ,
219 );
220 }
221 console. log (
222 `[rail-climax] ✓ ok — promoted word(s) ${ [ ... climaxTokens ]. map (( w ) => `"${ w }"` ). join ( ", " ) } not duplicated in the rail (window ${ winIn . toFixed ( 2 ) }–${ winOut . toFixed ( 2 ) }s)` ,
223 );
224 } finally {
225 await Promise . race ([browser. close (). catch (() => {}), new Promise (( r ) => setTimeout (r, 8000 ))]);
226 }
227 }
228 main ()
229 . then (() => process. exit ( 0 ))
230 . catch (( e ) => {
231 console. error ( `[rail-climax] (skipped — ${ e . message })` );
232 process. exit ( 0 );
233 });