Setting the file. One moment.
Check Timing · Embedded Captions · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page 14.42
Hershey Script1
scripts/check-timing.cjs
scripts/ check-timing.cjs
JavaScript · 173 lines · 6 KB
"15%"
: [
"fifteen"
,
"percent"
],
11 "1/3" : [ "one" , "third" ],
12 "2x" : [ "two" , "times" ],
13 };
14 const norm = ( s ) =>
15 String (s)
16 . replace ( / ^ [ .,?!"'] +| [ .,?!"'] +$ / g , "" )
17 . toLowerCase ();
18 const splitPacked = ( t ) =>
19 String (t)
20 . split ( /<br> | \s + / )
21 . filter (Boolean);
22
23 function estimateVerticalBand ( css , words , fw , fh ) {
24 const top = css. match ( /top: \s * (- ? [\d.] + )%/ );
25 if ( ! top) return null ;
26 const topPct = parseFloat (top[ 1 ]);
27 const sizeM = css. match ( /font-size: \s * calc \( \s * ( [\d.] + ) \s * \* \s * var \( --h \) \s * \) / );
28 const fontPx = sizeM ? parseFloat (sizeM[ 1 ]) * fh : 0.05 * fh;
29 const lhM = css. match ( /line-height: \s * ( [\d.] + )/ );
30 const lh = lhM ? parseFloat (lhM[ 1 ]) : 1.1 ;
31 const charW = /uppercase/ i . test (css) ? 0.72 : 0.6 ;
32 let lineCount = 1 ,
33 cur = 0 ;
34 const spaceW = fontPx * 0.3 ;
35 for ( const w of words) {
36 const parts = String (w.text). split ( /<br>/ );
37 parts. forEach (( part , i ) => {
38 const pw = part. length * fontPx * charW;
39 if (i > 0 ) {
40 lineCount ++ ;
41 cur = pw;
42 } else {
43 const test = cur + (cur > 0 ? spaceW : 0 ) + pw;
44 if (test > fw && cur > 0 ) {
45 lineCount ++ ;
46 cur = pw;
47 } else cur = test;
48 }
49 });
50 }
51 return [topPct, topPct + ( 100 * (lineCount * fontPx * lh)) / fh];
52 }
53
54 function check ( project , strict ) {
55 const plan = JSON . parse (fs. readFileSync (path. join (project, "plan.json" ), "utf8" ));
56 const t = JSON . parse (fs. readFileSync (path. join (project, "transcript.json" ), "utf8" ));
57 const seq = (t.words || [])
58 . filter (( w ) => w.type === "word" )
59 . map (( w ) => [ norm (w.text), w.start, w.end]);
60 const issues = [];
61 const fw = plan.width || 720 ,
62 fh = plan.height || 1290 ;
63 const groups = plan.groups || [];
64 const bands = groups. map (( g ) =>
65 g.plane
66 ? [g.id || "?" , g.in, g.out, null , true ]
67 : [
68 g.id || "?" ,
69 g.in,
70 g.out,
71 estimateVerticalBand (g.css || "" , g.words || [], fw, fh),
72 g.allow_overlap || false ,
73 ],
74 );
75 for ( let i = 0 ; i < bands. length ; i ++ ) {
76 const [ gi , ai , bi , bbi , oki ] = bands[i];
77 if ( ! bbi) continue ;
78 for ( let j = i + 1 ; j < bands. length ; j ++ ) {
79 const [ gj , aj , bj , bbj , okj ] = bands[j];
80 if ( ! bbj || oki || okj) continue ;
81 if (Math. min (bi, bj) - Math. max (ai, aj) <= 0.05 ) continue ;
82 if (Math. min (bbi[ 1 ], bbj[ 1 ]) - Math. max (bbi[ 0 ], bbj[ 0 ]) > 2.0 )
83 issues. push (
84 `[${ gi }↔${ gj }] groups overlap in time (${ Math . max ( ai , aj ). toFixed ( 2 ) }–${ Math . min ( bi , bj ). toFixed ( 2 ) }s) AND vertically (band ${ Math . max ( bbi [ 0 ], bbj [ 0 ]). toFixed ( 0 ) }%–${ Math . min ( bbi [ 1 ], bbj [ 1 ]). toFixed ( 0 ) }%) — reposition one or add "allow_overlap": true if deliberate.` ,
85 );
86 }
87 }
88 let ti = 0 ;
89 for ( const g of groups) {
90 const gid = g.id || "?" ,
91 gin = g.in,
92 gout = g.out;
93 const ws = (g.words || []). map (( w ) => w.start),
94 we = (g.words || []). map (( w ) => w.end);
95 if (ws. length && gin != null ) {
96 const e = Math. min ( ... ws);
97 if (e < gin - 0.01 )
98 issues. push (
99 `[${ gid }] group.in=${ gin . toFixed ( 2 ) } but earliest word starts at ${ e . toFixed ( 2 ) } — word delayed by ${ ( gin - e >= 0 ? "+" : "" ) + ( gin - e ). toFixed ( 2 ) }s. Lower group.in.` ,
100 );
101 }
102 if (we. length && gout != null ) {
103 const l = Math. max ( ... we);
104 if (l > gout + 0.01 )
105 issues. push (
106 `[${ gid }] group.out=${ gout . toFixed ( 2 ) } but latest word ends at ${ l . toFixed ( 2 ) } — word clipped. Raise group.out.` ,
107 );
108 }
109 for ( const w of g.words || []) {
110 // Exact pairing: a compiler that KNOWS the transcript index emits `ti` —
111 // duplicate words then pair by POSITION, not text-search (which grabs the
112 // first occurrence and reports a phantom drift). Sanity-check the text; on
113 // mismatch fall back to the text matcher below.
114 if (
115 Number. isInteger (w.ti) &&
116 seq[w.ti] &&
117 seq[w.ti][ 0 ] === norm ( splitPacked (w.text)[ 0 ] || "" )
118 ) {
119 const drift = w.start - seq[w.ti][ 1 ];
120 if (Math. abs (drift) > DRIFT_TOL )
121 issues. push (
122 `[${ gid }] '${ norm ( w . text ) }': plan=${ w . start . toFixed ( 3 ) } transcript=${ seq [ w . ti ][ 1 ]. toFixed ( 3 ) } drift ${ ( drift >= 0 ? "+" : "" ) + drift . toFixed ( 3 ) }s` ,
123 );
124 ti = w.ti + 1 ;
125 continue ;
126 }
127 const parts = splitPacked (w.text). map (norm);
128 parts. forEach (( part , pi ) => {
129 if ( ! part) return ;
130 if (part in CREATIVE_SUBS ) {
131 for ( const sub of CREATIVE_SUBS [part]) {
132 const j = seq. findIndex (( s , k ) => k >= ti && s[ 0 ] === sub);
133 if (j >= 0 ) ti = j + 1 ;
134 }
135 return ;
136 }
137 let found = seq. findIndex (( s , k ) => k >= ti && s[ 0 ] === part);
138 if (found < 0 ) found = seq. findIndex (( s ) => s[ 0 ] === part);
139 if (found < 0 ) {
140 issues. push ( `[${ gid }] '${ part }': NOT IN TRANSCRIPT (plan start=${ w . start . toFixed ( 3 ) })` );
141 return ;
142 }
143 const ts = seq[found][ 1 ];
144 ti = found + 1 ;
145 if (pi === 0 ) {
146 const drift = w.start - ts;
147 if (Math. abs (drift) > DRIFT_TOL )
148 issues. push (
149 `[${ gid }] '${ part }': plan=${ w . start . toFixed ( 3 ) } transcript=${ ts . toFixed ( 3 ) } drift ${ ( drift >= 0 ? "+" : "" ) + drift . toFixed ( 3 ) }s` ,
150 );
151 } else
152 issues. push (
153 `[${ gid }] '${ part }' packed with '${ parts [ 0 ] }': transcript=${ ts . toFixed ( 3 ) } but entry has no distinct timing — split into separate word entries` ,
154 );
155 });
156 }
157 }
158 const name = path. basename (project);
159 if ( ! issues. length ) {
160 console. log ( `${ name }: timing OK ✓ (${ seq . length } transcript words)` );
161 return 0 ;
162 }
163 console. log ( `${ name }: ${ issues . length } timing issue(s):` );
164 for ( const i of issues) console. log ( ` ${ i }` );
165 return strict ? 1 : 0 ;
166 }
167
168 const args = process.argv. slice ( 2 ). filter (( a ) => a !== "--strict" );
169 if ( ! args. length ) {
170 console. error ( "usage: check-timing.cjs <project-dir> [--strict]" );
171 process. exit ( 2 );
172 }
173 process. exit ( check (path. resolve (args[ 0 ]), process.argv. includes ( "--strict" )));