Setting the file. One moment.
Lib Dna · Embedded Captions · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page Hershey Script1
Script Make Cinematic
scripts/ lib-dna.cjs
JavaScript · 201 lines · 7 KB
15 */
16 const path = require ( "path" );
17 const fs = require ( "fs" );
18
19 const SKILL_ROOT = path. resolve (__dirname, ".." );
20 const DNA_DIR = path. join ( SKILL_ROOT , "dna" );
21
22 // legacy template names → DNA (back-compat for existing plan.json files)
23 const LEGACY = { "cinematic-cream" : "cream" };
24
25 function list () {
26 try {
27 return fs
28 . readdirSync ( DNA_DIR )
29 . filter (( f ) => f. endsWith ( ".json" ))
30 . map (( f ) => f. replace ( / \. json $ / , "" ))
31 . sort ();
32 } catch {
33 return [];
34 }
35 }
36
37 function load ( name ) {
38 const p = path. join ( DNA_DIR , `${ name }.json` );
39 if ( ! fs. existsSync (p)) {
40 throw new Error ( `unknown DNA "${ name }". Available: ${ list (). join ( ", " ) }` );
41 }
42 return JSON . parse (fs. readFileSync (p, "utf8" ));
43 }
44
45 function readJson ( p ) {
46 try {
47 return JSON . parse (fs. readFileSync (p, "utf8" ));
48 } catch {
49 return null ;
50 }
51 }
52
53 // ── audio impact: where does this window's loudness rank within the clip? ──────
54 // envelope.json = { hop, rms: [linear 0..1 per hop] } (audio-envelope.cjs)
55 function heroImpact ( project , t0 , t1 ) {
56 const env = readJson (path. join (project, "envelope.json" ));
57 if ( ! env || ! Array. isArray (env.rms) || ! env.rms. length ) return 0.6 ; // neutral default
58 const hop = env.hop || 0.05 ;
59 const i0 = Math. max ( 0 , Math. floor (t0 / hop)),
60 i1 = Math. min (env.rms. length , Math. ceil (t1 / hop));
61 if (i1 <= i0) return 0.6 ;
62 const span = i1 - i0;
63 const mean = ( arr , a , b ) => {
64 let s = 0 ;
65 for ( let i = a; i < b; i ++ ) s += arr[i];
66 return s / (b - a);
67 };
68 const target = mean (env.rms, i0, i1);
69 // percentile of this window vs all same-length windows across the clip
70 let below = 0 ,
71 total = 0 ;
72 for ( let s = 0 ; s + span <= env.rms. length ; s += Math. max ( 1 , Math. floor (span / 2 ))) {
73 total ++ ;
74 if ( mean (env.rms, s, s + span) <= target) below ++ ;
75 }
76 return total ? below / total : 0.6 ;
77 }
78
79 // ── scene-aware token resolution ───────────────────────────────────────────────
80 function resolveTokens ( dna , project , opts ) {
81 const sz = readJson (path. join (project, "safe-zones.json" )) || {};
82 const palette = sz.palette || {};
83 const optics = sz.optics || {};
84 const lighting = sz.lighting || {};
85
86 // accent: "scene" → sampled suggestion, else literal.
87 // accentMode "counter" (loud registers): the accent must FIGHT the scene's
88 // temperature, not harmonize with it — a sampled warm sienna inside a tungsten room
89 // camouflages (the warm-on-warm failure a blind review caught across three DNAs).
90 // Counter-pole = rotate the sampled hue 180° and push saturation/lightness hot.
91 let accent =
92 dna.palette.accent === "scene"
93 ? palette.accentSuggestion || dna.palette.accent_fallback || "#e3c06a"
94 : dna.palette.accent;
95 if (
96 dna.palette.accentMode === "counter" &&
97 dna.palette.accent === "scene" &&
98 / ^ #( [0-9a-f] {6} ) $ / i . test (accent || "" )
99 ) {
100 const n = parseInt (accent. slice ( 1 ), 16 );
101 let r = (n >> 16 ) / 255 ,
102 g2 = ((n >> 8 ) & 255 ) / 255 ,
103 b = (n & 255 ) / 255 ;
104 const mx = Math. max (r, g2, b),
105 mn = Math. min (r, g2, b),
106 d = mx - mn;
107 let h = 0 ;
108 if (d > 0 ) {
109 h = mx === r ? ((g2 - b) / d) % 6 : mx === g2 ? (b - r) / d + 2 : (r - g2) / d + 4 ;
110 h *= 60 ;
111 if (h < 0 ) h += 360 ;
112 }
113 h = (h + 180 ) % 360 ;
114 const s = 0.85 ,
115 v = 0.92 ; // hot, acid, readable
116 const c = v * s,
117 x = c * ( 1 - Math. abs (((h / 60 ) % 2 ) - 1 )),
118 m = v - c;
119 const [ r2 , g3 , b2 ] =
120 h < 60
121 ? [c, x, 0 ]
122 : h < 120
123 ? [x, c, 0 ]
124 : h < 180
125 ? [ 0 , c, x]
126 : h < 240
127 ? [ 0 , x, c]
128 : h < 300
129 ? [x, 0 , c]
130 : [c, 0 , x];
131 const f = ( q ) =>
132 Math. round ((q + m) * 255 )
133 . toString ( 16 )
134 . padStart ( 2 , "0" );
135 accent = `#${ f ( r2 ) }${ f ( g3 ) }${ f ( b2 ) }` ;
136 }
137
138 const dark = dna.palette.scheme === "dark-on-light" ;
139
140 // contact shadow along the measured light direction + a soft ambient.
141 // light-on-dark also gets the warm bloom layer when the DNA asks for it.
142 let shadowParts = [];
143 if (dna.optics && dna.optics.contactShadow) {
144 const sd = lighting.shadow || { dx: 0 , dy: 3 };
145 shadowParts. push ( `${ sd . dx }px ${ sd . dy + 1 }px 10px rgba(0,0,0,${ dark ? 0.22 : 0.32 })` );
146 }
147 shadowParts. push (dark ? "0 1px 14px rgba(0,0,0,0.10)" : "0 2px 22px rgba(0,0,0,0.28)" );
148 if (dna.optics && dna.optics.warmBloom) shadowParts. push ( "0 0 26px rgba(255,214,160,0.18)" );
149 const textShadow = shadowParts. join ( ", " );
150
151 // depth match: embed (bg) text picks up the scene's bokeh; fg/rail stay sharp
152 const blurPx = (dna.optics && dna.optics.depthMatch && optics.suggestedTextBlurPx) || 0 ;
153 const baseFilter = dna.filter && dna.filter !== "none" ? dna.filter : "" ;
154 const filterBg = `${ baseFilter }${ blurPx ? ` blur(${ blurPx }px)` : ""}` . trim () || "none" ;
155 const filterFg = baseFilter || "none" ;
156
157 // hero orchestration + loudness coupling — one config PER hero (scarcity is per
158 // beat/block, not per clip; each hero's amplitude follows ITS spoken loudness)
159 const heroGroups = (opts && (opts.heroGroups || (opts.heroGroup ? [opts.heroGroup] : []))) || [];
160 const heroes = heroGroups. map (( hg ) => {
161 const impact = heroImpact (project, hg.in, Math. min (hg.in + 0.7 , hg.out));
162 const amp = + (( 0.75 + impact * 0.5 ) * (hg.minor ? 0.7 : 1 )). toFixed ( 3 ); // 0.75…1.25, minors damped
163 return {
164 // spread the DNA's full hero block (entrance/perLetter/glow/breathe/ripple/loom/
165 // letterBlur/sheen/echoes/… — new fx fields flow through with no plumbing),
166 // then the per-hero computed values
167 ... dna.hero,
168 entrance: dna.hero.entrance || "emergence" ,
169 dimOthers: dna.hero.dimOthers != null ? dna.hero.dimOthers : 0.45 ,
170 id: hg.id,
171 minor: hg.minor === true ,
172 in: hg.in,
173 out: hg.out,
174 amp,
175 heroColor: dna.palette.heroColor === "accent" ? accent : null ,
176 };
177 });
178 const hero = heroes[ 0 ] || null ; // back-compat single accessor
179
180 return {
181 fontFamily: dna.font.family,
182 capColor: dna.palette.cap_color,
183 blend: dna.palette.blend,
184 accent,
185 textShadow,
186 filterBg,
187 filterFg,
188 blurPx,
189 motion: dna.motion,
190 hero,
191 heroes,
192 heroCss: (dna.hero && dna.hero.css) || "" ,
193 heroCase: (dna.hero && dna.hero.case) || "none" ,
194 heroTracking: (dna.hero && dna.hero.tracking) || "0" ,
195 dnaCss: dna.css || "" ,
196 register: dna.register,
197 name: dna.name,
198 };
199 }
200
201 module . exports = { LEGACY, list, load, resolveTokens, heroImpact };