reelson 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +15 -0
  4. package/README.md +180 -0
  5. package/bin/reelson.js +649 -0
  6. package/bin/reelson.ts +676 -0
  7. package/bin/run.js +5 -0
  8. package/demo.config.example.json +51 -0
  9. package/docs/demo.webp +0 -0
  10. package/docs/prompting.md +72 -0
  11. package/docs/style-guide.md +117 -0
  12. package/examples/demo.config.json +18 -0
  13. package/examples/todo-add-item/scenario.ts +45 -0
  14. package/examples/todo-add-item/video.json +35 -0
  15. package/music/README.md +7 -0
  16. package/package.json +68 -0
  17. package/skills/reelson-compose/SKILL.md +226 -0
  18. package/skills/reelson-compose/schemas/video.schema.json +172 -0
  19. package/skills/reelson-compose/scripts/build.js +424 -0
  20. package/skills/reelson-compose/scripts/build.ts +532 -0
  21. package/skills/reelson-compose/scripts/captions.js +24 -0
  22. package/skills/reelson-compose/scripts/captions.ts +40 -0
  23. package/skills/reelson-compose/scripts/check.js +80 -0
  24. package/skills/reelson-compose/scripts/check.ts +93 -0
  25. package/skills/reelson-compose/scripts/composition.js +163 -0
  26. package/skills/reelson-compose/scripts/composition.ts +241 -0
  27. package/skills/reelson-compose/scripts/hyperframes.js +138 -0
  28. package/skills/reelson-compose/scripts/hyperframes.ts +157 -0
  29. package/skills/reelson-compose/scripts/portrait.js +150 -0
  30. package/skills/reelson-compose/scripts/portrait.ts +218 -0
  31. package/skills/reelson-compose/scripts/project.js +238 -0
  32. package/skills/reelson-compose/scripts/project.ts +324 -0
  33. package/skills/reelson-compose/scripts/studio.js +330 -0
  34. package/skills/reelson-compose/scripts/studio.ts +394 -0
  35. package/skills/reelson-compose/scripts/timeline.js +271 -0
  36. package/skills/reelson-compose/scripts/timeline.ts +514 -0
  37. package/skills/reelson-compose/scripts/tts.js +303 -0
  38. package/skills/reelson-compose/scripts/tts.ts +371 -0
  39. package/skills/reelson-compose/scripts/verify.js +190 -0
  40. package/skills/reelson-compose/scripts/verify.ts +209 -0
  41. package/skills/reelson-compose/scripts/voice.js +178 -0
  42. package/skills/reelson-compose/scripts/voice.ts +224 -0
  43. package/skills/reelson-compose/scripts/zooms.js +259 -0
  44. package/skills/reelson-compose/scripts/zooms.ts +333 -0
  45. package/skills/reelson-compose/sections/README.md +5 -0
  46. package/skills/reelson-compose/sections/intro/minimal/section.css +27 -0
  47. package/skills/reelson-compose/sections/intro/minimal/section.html +12 -0
  48. package/skills/reelson-compose/sections/intro/minimal/section.js +16 -0
  49. package/skills/reelson-compose/sections/intro/minimal/section.json +9 -0
  50. package/skills/reelson-compose/sections/intro/poster/section.css +27 -0
  51. package/skills/reelson-compose/sections/intro/poster/section.html +14 -0
  52. package/skills/reelson-compose/sections/intro/poster/section.js +28 -0
  53. package/skills/reelson-compose/sections/intro/poster/section.json +9 -0
  54. package/skills/reelson-compose/sections/intro/split/section.css +30 -0
  55. package/skills/reelson-compose/sections/intro/split/section.html +12 -0
  56. package/skills/reelson-compose/sections/intro/split/section.js +19 -0
  57. package/skills/reelson-compose/sections/intro/split/section.json +9 -0
  58. package/skills/reelson-compose/sections/outro/compact/section.css +23 -0
  59. package/skills/reelson-compose/sections/outro/compact/section.html +10 -0
  60. package/skills/reelson-compose/sections/outro/compact/section.js +11 -0
  61. package/skills/reelson-compose/sections/outro/compact/section.json +8 -0
  62. package/skills/reelson-compose/sections/outro/endcard/section.css +25 -0
  63. package/skills/reelson-compose/sections/outro/endcard/section.html +13 -0
  64. package/skills/reelson-compose/sections/outro/endcard/section.js +13 -0
  65. package/skills/reelson-compose/sections/outro/endcard/section.json +8 -0
  66. package/skills/reelson-compose/sections/outro/wordmark/section.css +11 -0
  67. package/skills/reelson-compose/sections/outro/wordmark/section.html +5 -0
  68. package/skills/reelson-compose/sections/outro/wordmark/section.js +12 -0
  69. package/skills/reelson-compose/sections/outro/wordmark/section.json +8 -0
  70. package/skills/reelson-compose/sections/recap/compact/section.css +26 -0
  71. package/skills/reelson-compose/sections/recap/compact/section.html +8 -0
  72. package/skills/reelson-compose/sections/recap/compact/section.js +23 -0
  73. package/skills/reelson-compose/sections/recap/compact/section.json +11 -0
  74. package/skills/reelson-compose/sections/recap/steps/section.css +36 -0
  75. package/skills/reelson-compose/sections/recap/steps/section.html +8 -0
  76. package/skills/reelson-compose/sections/recap/steps/section.js +27 -0
  77. package/skills/reelson-compose/sections/recap/steps/section.json +11 -0
  78. package/skills/reelson-compose/studio/index.html +81 -0
  79. package/skills/reelson-compose/studio/studio.css +142 -0
  80. package/skills/reelson-compose/studio/studio.js +1031 -0
  81. package/skills/reelson-compose/templates/README.md +193 -0
  82. package/skills/reelson-compose/templates/classic/assets/vendor/Inter-OFL.txt +93 -0
  83. package/skills/reelson-compose/templates/classic/assets/vendor/gsap.min.js +11 -0
  84. package/skills/reelson-compose/templates/classic/assets/vendor/inter-latin-ext-wght-normal.woff2 +0 -0
  85. package/skills/reelson-compose/templates/classic/assets/vendor/inter-latin-wght-normal.woff2 +0 -0
  86. package/skills/reelson-compose/templates/classic/stage.html +447 -0
  87. package/skills/reelson-compose/templates/classic/template.json +5 -0
  88. package/skills/reelson-record/SKILL.md +220 -0
  89. package/skills/reelson-record/schemas/demo.config.schema.json +269 -0
  90. package/skills/reelson-record/scripts/capture.js +142 -0
  91. package/skills/reelson-record/scripts/capture.ts +190 -0
  92. package/skills/reelson-record/scripts/config.js +148 -0
  93. package/skills/reelson-record/scripts/config.ts +264 -0
  94. package/skills/reelson-record/scripts/cursor-overlay.js +111 -0
  95. package/skills/reelson-record/scripts/cursor-overlay.ts +122 -0
  96. package/skills/reelson-record/scripts/doctor.js +128 -0
  97. package/skills/reelson-record/scripts/doctor.ts +147 -0
  98. package/skills/reelson-record/scripts/languages.js +29 -0
  99. package/skills/reelson-record/scripts/languages.ts +41 -0
  100. package/skills/reelson-record/scripts/record.js +365 -0
  101. package/skills/reelson-record/scripts/record.ts +451 -0
  102. package/skills/reelson-record/scripts/scenario.js +356 -0
  103. package/skills/reelson-record/scripts/scenario.ts +570 -0
  104. package/skills/reelson-record/scripts/validate.js +143 -0
  105. package/skills/reelson-record/scripts/validate.ts +179 -0
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Name pools per UI language (demo.config.json `language`). Add a language by
3
+ * adding a key; unknown languages fall back to English.
4
+ */
5
+ const PERSONAS = {
6
+ en: {
7
+ phonePrefix: '+1 555 ',
8
+ people: [
9
+ { firstName: 'Emma', lastName: 'Carter', company: 'Carter Consulting' },
10
+ { firstName: 'Liam', lastName: 'Bennett', company: 'Bennett Design Co.' },
11
+ { firstName: 'Olivia', lastName: 'Hayes', company: 'Hayes Logistics' },
12
+ { firstName: 'Noah', lastName: 'Foster', company: 'Foster Construction' },
13
+ { firstName: 'Ava', lastName: 'Mitchell', company: 'Mitchell Health' },
14
+ { firstName: 'Lucas', lastName: 'Reed', company: 'Reed Tech' },
15
+ { firstName: 'Mia', lastName: 'Collins', company: 'Collins Events' },
16
+ { firstName: 'Ethan', lastName: 'Brooks', company: 'Brooks Farms' },
17
+ ],
18
+ },
19
+ ro: {
20
+ phonePrefix: '07',
21
+ people: [
22
+ { firstName: 'Andrei', lastName: 'Popescu', company: 'Popescu Consulting SRL' },
23
+ { firstName: 'Ioana', lastName: 'Ionescu', company: 'Ionescu Design SRL' },
24
+ { firstName: 'Mihai', lastName: 'Dumitrescu', company: 'Dumitrescu Construct SRL' },
25
+ { firstName: 'Elena', lastName: 'Stan', company: 'Stan Logistic SRL' },
26
+ { firstName: 'Alexandru', lastName: 'Radu', company: 'Radu Tech SRL' },
27
+ { firstName: 'Maria', lastName: 'Constantin', company: 'Constantin Medical SRL' },
28
+ { firstName: 'Cristian', lastName: 'Moldovan', company: 'Moldovan Agro SRL' },
29
+ { firstName: 'Ana', lastName: 'Georgescu', company: 'Georgescu Events SRL' },
30
+ ],
31
+ },
32
+ };
33
+ export function persona(seed = Date.now(), domain = 'example.com', language = 'en') {
34
+ const pool = PERSONAS[language.split('-')[0]] ?? PERSONAS.en;
35
+ const base = pool.people[seed % pool.people.length];
36
+ const strip = (v) => v
37
+ .normalize('NFD')
38
+ .replace(/[\u0300-\u036f]/g, '')
39
+ .toLowerCase();
40
+ const suffix = (seed % 89) + 10;
41
+ const digits = String(seed).padStart(8, '0').slice(-8);
42
+ return {
43
+ ...base,
44
+ fullName: `${base.firstName} ${base.lastName}`,
45
+ email: `${strip(base.firstName)}.${strip(base.lastName)}${suffix}@${domain}`,
46
+ phone: pool.phonePrefix === '07'
47
+ ? `07${digits}`
48
+ : `${pool.phonePrefix}${digits.slice(0, 3)} ${digits.slice(3, 7)}`,
49
+ company: base.company,
50
+ };
51
+ }
52
+ export function createDemo(first, seed = 1, personaDefaults = {}, hooks = {}) {
53
+ // The page every action uses; demo.popup / demo.switchTo move it, a close moves it back.
54
+ let page = first;
55
+ const openers = [];
56
+ const startedAt = Date.now();
57
+ const markers = [];
58
+ const clicks = [];
59
+ const focus = [];
60
+ const cuts = [];
61
+ const transitions = [];
62
+ const elapsedSeconds = () => (Date.now() - startedAt) / 1000;
63
+ const stamp = () => Number(elapsedSeconds().toFixed(2));
64
+ // Deterministic per scenario, so a re-record moves and types the same way.
65
+ const rng = mulberry32(seed);
66
+ const viewport = page.viewportSize() ?? { width: 1440, height: 900 };
67
+ // A believable place for a hand to rest: somewhere in the middle third, never a corner.
68
+ const restingPoint = () => ({
69
+ x: Math.round(viewport.width * (0.38 + rng() * 0.24)),
70
+ y: Math.round(viewport.height * (0.38 + rng() * 0.24)),
71
+ });
72
+ let pos = restingPoint();
73
+ /**
74
+ * Human-like glide: a gently curved path (quadratic bezier with a random
75
+ * perpendicular bow), ease-in-out speed, a little tremor that dies out as
76
+ * the cursor settles, and a landing point slightly off the exact centre.
77
+ */
78
+ const glide = async (to) => {
79
+ const from = pos;
80
+ const dx = to.x - from.x;
81
+ const dy = to.y - from.y;
82
+ const dist = Math.hypot(dx, dy);
83
+ if (dist < 2) {
84
+ return;
85
+ }
86
+ const bow = (0.1 + rng() * 0.18) * (rng() < 0.5 ? -1 : 1);
87
+ const cx = from.x + dx / 2 - dy * bow;
88
+ const cy = from.y + dy / 2 + dx * bow;
89
+ const steps = Math.round(Math.min(42, Math.max(14, dist / 16)));
90
+ const durationMs = Math.min(720, Math.max(260, 200 + dist * 0.55));
91
+ for (let i = 1; i <= steps; i++) {
92
+ const u = i / steps;
93
+ const e = u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
94
+ const tremor = (1 - e) * 1.2;
95
+ const x = (1 - e) * (1 - e) * from.x +
96
+ 2 * (1 - e) * e * cx +
97
+ e * e * to.x +
98
+ (rng() - 0.5) * tremor;
99
+ const y = (1 - e) * (1 - e) * from.y +
100
+ 2 * (1 - e) * e * cy +
101
+ e * e * to.y +
102
+ (rng() - 0.5) * tremor;
103
+ await page.mouse.move(x, y);
104
+ await page.waitForTimeout(durationMs / steps);
105
+ }
106
+ pos = to;
107
+ };
108
+ const rest = async () => {
109
+ await glide(restingPoint());
110
+ await page.mouse.move(pos.x, pos.y);
111
+ };
112
+ const moveTo = async (target) => {
113
+ if (await revealSmoothly(target.first())) {
114
+ // Let the viewer see where the scroll landed before the cursor sets off.
115
+ await page.waitForTimeout(250);
116
+ }
117
+ await target.first().scrollIntoViewIfNeeded();
118
+ const box = await target.first().boundingBox();
119
+ if (!box) {
120
+ throw new Error(`moveTo: target has no bounding box: ${target}`);
121
+ }
122
+ focus.push({ at: stamp(), box: rounded(box), area: rounded(await areaAround(target.first(), box)) });
123
+ // Land near, not exactly on, the centre — people don't hit the middle.
124
+ const jx = (rng() - 0.5) * Math.min(24, box.width * 0.3);
125
+ const jy = (rng() - 0.5) * Math.min(10, box.height * 0.3);
126
+ await glide({
127
+ x: box.x + box.width / 2 + jx,
128
+ y: box.y + box.height / 2 + jy,
129
+ });
130
+ await page.waitForTimeout(120 + rng() * 120);
131
+ };
132
+ /**
133
+ * An element out of sight is scrolled into view the way a person would (its scrolling
134
+ * container glides, eased, over 0.5–1.4 s by distance), not in one jump — so the viewer
135
+ * can follow a menu or a list moving. Returns whether it scrolled.
136
+ */
137
+ const revealSmoothly = async (el) => el.evaluate(async (node) => {
138
+ const r = node.getBoundingClientRect();
139
+ const x = Math.min(Math.max(r.left + r.width / 2, 0), innerWidth - 1);
140
+ const y = Math.min(Math.max(r.top + r.height / 2, 0), innerHeight - 1);
141
+ const hit = document.elementFromPoint(x, y);
142
+ const inView = r.top >= 0 && r.bottom <= innerHeight && !!hit && (node.contains(hit) || hit.contains(node));
143
+ if (inView) {
144
+ return false;
145
+ }
146
+ // The nearest ancestor that scrolls vertically, else the page.
147
+ let box = node.parentElement;
148
+ while (box && !(/(auto|scroll)/.test(getComputedStyle(box).overflowY) && box.scrollHeight > box.clientHeight)) {
149
+ box = box.parentElement;
150
+ }
151
+ const scroller = (box ?? document.scrollingElement ?? document.documentElement);
152
+ const isPage = !box;
153
+ const view = isPage ? { top: 0, height: innerHeight } : { top: scroller.getBoundingClientRect().top, height: scroller.clientHeight };
154
+ const start = scroller.scrollTop;
155
+ const max = scroller.scrollHeight - scroller.clientHeight;
156
+ const target = Math.min(Math.max(start + (r.top + r.height / 2) - (view.top + view.height / 2), 0), max);
157
+ const distance = target - start;
158
+ if (Math.abs(distance) < 2) {
159
+ return false;
160
+ }
161
+ const duration = Math.min(1400, Math.max(500, 400 + Math.abs(distance) * 0.8));
162
+ const began = performance.now();
163
+ await new Promise((done) => {
164
+ const step = (now) => {
165
+ const t = Math.min(1, (now - began) / duration);
166
+ const eased = t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
167
+ scroller.scrollTop = start + distance * eased;
168
+ if (t < 1)
169
+ requestAnimationFrame(step);
170
+ else
171
+ done();
172
+ };
173
+ requestAnimationFrame(step);
174
+ });
175
+ return true;
176
+ });
177
+ /**
178
+ * Clicks where the cursor landed, not at the element's exact centre (Playwright's
179
+ * default would snap the cursor there). If the element moved since the glide, the
180
+ * point is kept inside it.
181
+ */
182
+ const clickWhereLanded = async (target) => {
183
+ const el = target.first();
184
+ const box = await el.boundingBox();
185
+ if (!box) {
186
+ throw new Error(`click: target has no bounding box: ${target}`);
187
+ }
188
+ const inside = (v, size) => Math.min(Math.max(v, Math.min(2, size / 2)), size - Math.min(2, size / 2));
189
+ const position = { x: inside(pos.x - box.x, box.width), y: inside(pos.y - box.y, box.height) };
190
+ pos = { x: box.x + position.x, y: box.y + position.y };
191
+ await el.click({ position });
192
+ };
193
+ /**
194
+ * Human-like typing: uneven per-key delay, a beat after spaces and
195
+ * punctuation, the odd longer hesitation — but never slow overall.
196
+ */
197
+ const typeNaturally = async (text) => {
198
+ for (const ch of text) {
199
+ await page.keyboard.type(ch);
200
+ let delay = 38 + rng() * 60;
201
+ if (ch === ' ') {
202
+ delay += 50 + rng() * 80;
203
+ }
204
+ else if ('.,@-_'.includes(ch)) {
205
+ delay += 60 + rng() * 60;
206
+ }
207
+ if (rng() < 0.05) {
208
+ delay += 120 + rng() * 160;
209
+ }
210
+ await page.waitForTimeout(delay);
211
+ }
212
+ };
213
+ const switchTo = async (next) => {
214
+ if (next === page) {
215
+ return;
216
+ }
217
+ openers.push(page);
218
+ page = next;
219
+ await next.waitForLoadState('domcontentloaded').catch(() => { });
220
+ await next.bringToFront().catch(() => { });
221
+ // The new page's mouse starts where the cursor is, so the video shows no jump.
222
+ await next.mouse.move(pos.x, pos.y);
223
+ hooks.onSwitch?.(next);
224
+ next.once('close', () => {
225
+ if (page === next) {
226
+ page = openers.pop() ?? first;
227
+ hooks.onSwitch?.(page);
228
+ }
229
+ });
230
+ };
231
+ return {
232
+ get page() {
233
+ return page;
234
+ },
235
+ markers,
236
+ clicks,
237
+ focus,
238
+ persona: (personaSeed, domain) => persona(personaSeed, domain ?? personaDefaults.domain, personaDefaults.language),
239
+ cuts,
240
+ cut: async (fn, opts = {}) => {
241
+ const from = elapsedSeconds() + (opts.keepMs ?? 600) / 1000;
242
+ const result = await fn();
243
+ const to = elapsedSeconds();
244
+ if (to > from) {
245
+ cuts.push({
246
+ from: Number(from.toFixed(2)),
247
+ to: Number(to.toFixed(2)),
248
+ });
249
+ }
250
+ return result;
251
+ },
252
+ transitions,
253
+ transition: async (fn, card) => {
254
+ const from = elapsedSeconds();
255
+ const result = await fn();
256
+ // Re-assert the cursor on the new page so the first kept frame has it.
257
+ await page.mouse.move(pos.x, pos.y);
258
+ await page.waitForTimeout(300);
259
+ const to = elapsedSeconds();
260
+ cuts.push({
261
+ from: Number(from.toFixed(2)),
262
+ to: Number(to.toFixed(2)),
263
+ });
264
+ transitions.push({ ...card, at: Number(from.toFixed(2)) });
265
+ return result;
266
+ },
267
+ elapsedSeconds,
268
+ startedAt,
269
+ marker: (label) => {
270
+ markers.push({ label, at: Number(elapsedSeconds().toFixed(2)) });
271
+ },
272
+ pause: (ms) => page.waitForTimeout(ms),
273
+ goto: async (path) => {
274
+ // Park the cursor mid-screen BEFORE leaving: the overlay carries the position
275
+ // into the next page, and the trimmed video usually starts right after this.
276
+ await rest();
277
+ await page.goto(path, { waitUntil: 'networkidle' });
278
+ // Re-assert on the new document (a new origin has no saved overlay position).
279
+ await page.mouse.move(pos.x, pos.y);
280
+ await page.waitForTimeout(600);
281
+ },
282
+ rest,
283
+ moveTo,
284
+ click: async (target, opts = {}) => {
285
+ const move = stamp();
286
+ await moveTo(target);
287
+ const at = stamp();
288
+ await clickWhereLanded(target);
289
+ clicks.push({ move, at, x: Math.round(pos.x), y: Math.round(pos.y), kind: 'click' });
290
+ await page.waitForTimeout(opts.settleMs ?? 700);
291
+ },
292
+ type: async (target, text) => {
293
+ const move = stamp();
294
+ await moveTo(target);
295
+ const at = stamp();
296
+ await clickWhereLanded(target);
297
+ const click = { move, at, x: Math.round(pos.x), y: Math.round(pos.y), kind: 'type' };
298
+ clicks.push(click);
299
+ await typeNaturally(text);
300
+ click.until = stamp();
301
+ await page.waitForTimeout(300 + rng() * 200);
302
+ },
303
+ switchTo,
304
+ mobile: hooks.mobile ?? false,
305
+ square: hooks.square ?? false,
306
+ popup: async (action) => {
307
+ const [opened] = await Promise.all([page.context().waitForEvent('page'), action()]);
308
+ await switchTo(opened);
309
+ return opened;
310
+ },
311
+ scroll: async (deltaY, opts = {}) => {
312
+ const step = opts.stepPx ?? 80;
313
+ const steps = Math.max(1, Math.round(Math.abs(deltaY) / step));
314
+ const sign = Math.sign(deltaY);
315
+ for (let i = 0; i < steps; i++) {
316
+ await page.mouse.wheel(0, sign * step);
317
+ await page.waitForTimeout(40);
318
+ }
319
+ await page.waitForTimeout(300);
320
+ },
321
+ };
322
+ }
323
+ /** Small seeded PRNG so recordings are reproducible. */
324
+ function mulberry32(seed) {
325
+ let a = seed >>> 0;
326
+ return () => {
327
+ a = (a + 0x6d2b79f5) >>> 0;
328
+ let t = a;
329
+ t = Math.imul(t ^ (t >>> 15), t | 1);
330
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
331
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
332
+ };
333
+ }
334
+ const rounded = (b) => ({ x: Math.round(b.x), y: Math.round(b.y), width: Math.round(b.width), height: Math.round(b.height) });
335
+ /**
336
+ * The compact block around an element: the largest ancestor that is still about one row
337
+ * (at most 3x the element's height, or 180 px) and narrower than the viewport — e.g. a form
338
+ * field with its label and hint, a table row, a toolbar. Falls back to the element itself.
339
+ */
340
+ async function areaAround(element, box) {
341
+ return element
342
+ .evaluate((node, own) => {
343
+ const maxHeight = Math.max(own.height * 3, 180);
344
+ const maxWidth = window.innerWidth * 0.9;
345
+ let best = own;
346
+ for (let el = node.parentElement; el && el !== document.body; el = el.parentElement) {
347
+ const r = el.getBoundingClientRect();
348
+ if (r.height > maxHeight || r.width > maxWidth)
349
+ break;
350
+ if (r.width >= best.width && r.height >= best.height)
351
+ best = { x: r.x, y: r.y, width: r.width, height: r.height };
352
+ }
353
+ return best;
354
+ }, box)
355
+ .catch(() => box);
356
+ }