retake-demos 0.13.2 → 0.14.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.
- package/.env.example +4 -6
- package/AGENTS.md +69 -19
- package/README.md +203 -19
- package/assets/voice-samples/aiden.mp4 +0 -0
- package/assets/voice-samples/edge.mp4 +0 -0
- package/assets/voice-samples/kokoro.mp4 +0 -0
- package/dist/action-proof.js +66 -0
- package/dist/browser-home.js +64 -0
- package/dist/cards.js +103 -5
- package/dist/cli.js +937 -49
- package/dist/describe.js +3 -3
- package/dist/dryrun.js +56 -13
- package/dist/ext/golden.js +241 -0
- package/dist/hold.js +75 -0
- package/dist/logins.js +194 -0
- package/dist/manifest.js +239 -25
- package/dist/media-bin.js +24 -0
- package/dist/operator/run.js +82 -7
- package/dist/operator/tools.js +310 -42
- package/dist/pace.js +35 -3
- package/dist/paths.js +4 -1
- package/dist/presets.js +8 -0
- package/dist/preview.js +197 -0
- package/dist/record.js +320 -39
- package/dist/recording-env.js +201 -0
- package/dist/render.js +502 -88
- package/dist/routine.js +839 -0
- package/dist/schedule.js +169 -0
- package/dist/setup.js +725 -0
- package/dist/signin.js +46 -0
- package/dist/ui/chat.html +920 -11
- package/dist/ui/server.js +340 -23
- package/dist/voice.js +147 -4
- package/docs/stitch.md +38 -0
- package/llms.txt +0 -1
- package/package.json +7 -3
- package/scripts/patch-testreel.mjs +87 -5
- package/site/releases.json +15 -0
- package/skill/SKILL.md +86 -50
- package/demos/example.yaml +0 -43
- package/dist/ui/guide.html +0 -132
package/dist/cards.js
CHANGED
|
@@ -9,13 +9,69 @@
|
|
|
9
9
|
*
|
|
10
10
|
* All of it is render-time: change a title, `retake render`, seconds.
|
|
11
11
|
*/
|
|
12
|
+
import { launchChromium } from "./browser-home.js";
|
|
12
13
|
import fs from "node:fs";
|
|
13
14
|
import os from "node:os";
|
|
14
15
|
import path from "node:path";
|
|
15
|
-
import { chromium } from "playwright";
|
|
16
16
|
import { execFileSync } from "node:child_process";
|
|
17
17
|
const AMBER = "#e8b04b";
|
|
18
18
|
const ease = "t<0?0:t>1?1:t*t*(3-2*t)"; // smoothstep, in the page
|
|
19
|
+
/** A file as a data: URI, so the card page needs nothing from disk or network. */
|
|
20
|
+
function dataUri(file) {
|
|
21
|
+
const ext = path.extname(file).toLowerCase();
|
|
22
|
+
const mime = ext === ".svg" ? "image/svg+xml" : ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : ext === ".woff2" ? "font/woff2" : ext === ".woff" ? "font/woff" : ext === ".ttf" ? "font/ttf" : ext === ".otf" ? "font/otf" : "application/octet-stream";
|
|
23
|
+
return `data:${mime};base64,${fs.readFileSync(path.resolve(file)).toString("base64")}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A card that looks like the product rather than like Retake: the mark above
|
|
27
|
+
* the title, the brand's own typeface, its ground and ink, and — if asked —
|
|
28
|
+
* two soft circles in opposite corners. Built to reproduce a north-star card,
|
|
29
|
+
* which before this could only be made outside Retake and recoloured in.
|
|
30
|
+
*
|
|
31
|
+
* Same contract as the plain card: `__seek(ms)` lays out that exact moment,
|
|
32
|
+
* `__still` names the window where nothing moves (the poster comes from it).
|
|
33
|
+
*/
|
|
34
|
+
function brandedCardHtml(w, h, c, title, subtitle, ms) {
|
|
35
|
+
// Measured off a north-star card at 1920×1080: a 143px mark, the title
|
|
36
|
+
// 66px below it, two lines of ~92px type spanning 192px, wrapping at ~1300px.
|
|
37
|
+
const titleSize = Math.round(Math.min(w * 0.048, h * 0.085));
|
|
38
|
+
const subSize = Math.round(titleSize * 0.4);
|
|
39
|
+
const mark = Math.round(h * 0.133);
|
|
40
|
+
const font = c.font ? `@font-face{font-family:"BrandCard";src:url(${dataUri(c.font)});font-weight:100 900;font-display:block}` : "";
|
|
41
|
+
const family = c.font ? `"BrandCard",Georgia,serif` : `-apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif`;
|
|
42
|
+
const logo = c.logo ? `<img class="mark" src="${dataUri(c.logo)}" alt="">` : "";
|
|
43
|
+
const circles = c.ornament === "circles"
|
|
44
|
+
? `<i class="orb a"></i><i class="orb b"></i>` : "";
|
|
45
|
+
return `<!doctype html><meta charset="utf-8"><style>
|
|
46
|
+
${font}
|
|
47
|
+
html,body{margin:0;width:${w}px;height:${h}px;overflow:hidden;background:${c.background}}
|
|
48
|
+
.orb{position:absolute;border-radius:50%;background:${c.orb ?? `color-mix(in srgb, ${c.ink} 6%, ${c.background})`}}
|
|
49
|
+
.orb.a{width:${Math.round(h * 0.6)}px;height:${Math.round(h * 0.6)}px;left:${Math.round(-h * 0.25)}px;top:${Math.round(-h * 0.3)}px}
|
|
50
|
+
.orb.b{width:${Math.round(h * 0.7)}px;height:${Math.round(h * 0.7)}px;right:${Math.round(-h * 0.3)}px;bottom:${Math.round(-h * 0.33)}px}
|
|
51
|
+
.wrap{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:${Math.round(h * 0.06)}px}
|
|
52
|
+
.mark{width:${mark}px;height:${mark}px;border-radius:${Math.round(mark * 0.22)}px;box-shadow:0 ${Math.round(mark * 0.12)}px ${Math.round(mark * 0.3)}px rgba(0,0,0,.10);opacity:0;object-fit:cover}
|
|
53
|
+
.text{display:flex;flex-direction:column;align-items:center;gap:${Math.round(subSize * 0.8)}px}
|
|
54
|
+
.title{font-family:${family};font-size:${titleSize}px;font-weight:${c.weight};line-height:1.1;letter-spacing:-.022em;color:${c.ink};text-align:center;max-width:${Math.round(w * 0.68)}px;opacity:0;font-variation-settings:"opsz" 144}
|
|
55
|
+
.sub{font-family:${family};font-size:${subSize}px;color:${c.ink};opacity:0;text-align:center;max-width:${Math.round(w * 0.6)}px}
|
|
56
|
+
</style>
|
|
57
|
+
${circles}
|
|
58
|
+
<div class="wrap">${logo}<div class="text"><div class="title"></div><div class="sub"></div></div></div>
|
|
59
|
+
<script>
|
|
60
|
+
const M = document.querySelector(".mark"), T = document.querySelector(".title"), S = document.querySelector(".sub");
|
|
61
|
+
T.textContent = ${JSON.stringify(title)}; S.textContent = ${JSON.stringify(subtitle ?? "")};
|
|
62
|
+
if (!S.textContent) S.remove();
|
|
63
|
+
const DUR = ${ms}, FADE = Math.min(460, DUR * 0.24), OUT = Math.min(320, DUR * 0.18);
|
|
64
|
+
const ez = (t) => (${ease});
|
|
65
|
+
window.__still = [FADE * 1.6, DUR - OUT];
|
|
66
|
+
window.__seek = (ms) => {
|
|
67
|
+
const a = ez(ms / FADE), b = ez((ms - FADE * 0.35) / FADE), o = ez((DUR - ms) / OUT);
|
|
68
|
+
if (M) { M.style.opacity = Math.min(a, o); M.style.transform = "translateY(" + (1 - a) * 10 + "px)"; }
|
|
69
|
+
T.style.opacity = Math.min(b, o); T.style.transform = "translateY(" + (1 - b) * 12 + "px)";
|
|
70
|
+
if (S.isConnected) { S.style.opacity = Math.min(b, o); }
|
|
71
|
+
return true;
|
|
72
|
+
};
|
|
73
|
+
</script>`;
|
|
74
|
+
}
|
|
19
75
|
function cardHtml(w, h, theme, title, subtitle, ms) {
|
|
20
76
|
const titleSize = Math.round(Math.min(w * 0.055, h * 0.14));
|
|
21
77
|
const subSize = Math.round(titleSize * 0.4);
|
|
@@ -88,7 +144,7 @@ function calloutHtml(w, h, box, label, ms, fontSize, ink) {
|
|
|
88
144
|
* `__still` gets every frame shot — the slow path is the safe default. */
|
|
89
145
|
async function renderFrames(html, w, h, ms, fps, transparent) {
|
|
90
146
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "retake-frames-"));
|
|
91
|
-
const browser = await
|
|
147
|
+
const browser = await launchChromium();
|
|
92
148
|
try {
|
|
93
149
|
const page = await browser.newPage({ viewport: { width: w, height: h } });
|
|
94
150
|
await page.setContent(html, { waitUntil: "load" });
|
|
@@ -120,8 +176,9 @@ async function renderFrames(html, w, h, ms, fps, transparent) {
|
|
|
120
176
|
}
|
|
121
177
|
/** A title card as an H.264 segment at the final canvas size, plus (for the
|
|
122
178
|
intro) the settled middle frame as cover.png — the poster. */
|
|
123
|
-
export async function renderCard(ffmpeg, outDir, kind, spec, w, h, fps, theme) {
|
|
124
|
-
const
|
|
179
|
+
export async function renderCard(ffmpeg, outDir, kind, spec, w, h, fps, theme, style) {
|
|
180
|
+
const html = style ? brandedCardHtml(w, h, style, spec.title, spec.subtitle, spec.ms) : cardHtml(w, h, theme, spec.title, spec.subtitle, spec.ms);
|
|
181
|
+
const dir = await renderFrames(html, w, h, spec.ms, fps, false);
|
|
125
182
|
const mp4 = path.join(outDir, `.card-${kind}.mp4`);
|
|
126
183
|
execFileSync(ffmpeg, ["-y", "-loglevel", "error", "-framerate", String(fps), "-i", path.join(dir, "f%05d.png"), "-c:v", "libx264", "-crf", "17", "-preset", "veryfast", "-pix_fmt", "yuv420p", mp4], { stdio: "inherit" });
|
|
127
184
|
let cover;
|
|
@@ -160,7 +217,48 @@ export async function renderTitledCover(ffmpeg, frame, out, spec, w, h, theme) {
|
|
|
160
217
|
</style>
|
|
161
218
|
<div class="shot"></div><div class="veil"></div>
|
|
162
219
|
<div class="txt"><p class="t">${escapeHtml(spec.title)}</p><div class="r"></div>${spec.subtitle ? `<p class="s">${escapeHtml(spec.subtitle)}</p>` : ""}</div>`;
|
|
163
|
-
const browser = await
|
|
220
|
+
const browser = await launchChromium();
|
|
221
|
+
try {
|
|
222
|
+
const page = await browser.newPage({ viewport: { width: w, height: h } });
|
|
223
|
+
await page.setContent(html, { waitUntil: "load" });
|
|
224
|
+
await page.screenshot({ path: out });
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await browser.close();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* A poster that stops a scroll: the strongest frame of the video, the
|
|
233
|
+
* brand's colour sweeping in from the left behind big type, the product's
|
|
234
|
+
* mark on a light badge so it reads on any frame, and who made it. The bare
|
|
235
|
+
* frame (the old thumbnail) is honest but anonymous; this one says whose it
|
|
236
|
+
* is and what it is before anyone presses play.
|
|
237
|
+
*/
|
|
238
|
+
export async function renderPoster(frame, out, spec, w, h, brand) {
|
|
239
|
+
const b64 = fs.readFileSync(frame).toString("base64");
|
|
240
|
+
const rgb = (hex) => { const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim()); const n = m ? parseInt(m[1], 16) : 0; return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`; };
|
|
241
|
+
const m = rgb(brand.matte);
|
|
242
|
+
const face = brand.font ? `@font-face{font-family:poster;src:url(${dataUri(brand.font)})}` : "";
|
|
243
|
+
const family = `${brand.font ? "poster," : ""}-apple-system,BlinkMacSystemFont,"SF Pro Display","Helvetica Neue",Arial,sans-serif`;
|
|
244
|
+
const titleSize = Math.round(h * (spec.title.length > 16 ? 0.088 : 0.108));
|
|
245
|
+
const html = `<!doctype html><meta charset="utf-8"><style>${face}
|
|
246
|
+
html,body{margin:0;width:${w}px;height:${h}px;overflow:hidden;background:${brand.matte};font-family:${family}}
|
|
247
|
+
.shot{position:absolute;inset:0;background:url(data:image/png;base64,${b64}) center/cover no-repeat;transform:scale(1.04);transform-origin:70% 50%}
|
|
248
|
+
.veil{position:absolute;inset:0;background:linear-gradient(90deg,rgba(${m},.97) 0%,rgba(${m},.93) 30%,rgba(${m},.6) 50%,rgba(${m},0) 70%)}
|
|
249
|
+
.foot{position:absolute;left:0;right:0;bottom:0;height:${Math.round(h * 0.22)}px;background:linear-gradient(0deg,rgba(${m},.55),rgba(${m},0))}
|
|
250
|
+
.badge{position:absolute;left:${Math.round(w * 0.055)}px;top:${Math.round(h * 0.075)}px;background:${brand.ground};border-radius:${Math.round(h * 0.022)}px;padding:${Math.round(h * 0.018)}px ${Math.round(h * 0.028)}px;box-shadow:0 ${Math.round(h * 0.01)}px ${Math.round(h * 0.035)}px rgba(0,0,0,.28)}
|
|
251
|
+
.badge img{display:block;height:${Math.round(h * 0.052)}px;width:auto}
|
|
252
|
+
.txt{position:absolute;left:${Math.round(w * 0.055)}px;bottom:${Math.round(h * 0.12)}px;width:${Math.round(w * 0.46)}px;color:${brand.ground}}
|
|
253
|
+
.k{display:inline-block;font-size:${Math.round(h * 0.024)}px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;padding:${Math.round(h * 0.009)}px ${Math.round(h * 0.016)}px;border:${Math.max(2, Math.round(h * 0.0022))}px solid rgba(255,255,255,.55);border-radius:999px;margin-bottom:${Math.round(h * 0.03)}px}
|
|
254
|
+
.t{font-size:${titleSize}px;line-height:.98;font-weight:800;letter-spacing:-.035em;margin:0;text-shadow:0 ${Math.round(h * 0.004)}px ${Math.round(h * 0.02)}px rgba(0,0,0,.25)}
|
|
255
|
+
.bar{width:${Math.round(h * 0.085)}px;height:${Math.max(4, Math.round(h * 0.008))}px;border-radius:99px;background:${brand.ground};opacity:.9;margin:${Math.round(h * 0.034)}px 0 ${Math.round(h * 0.026)}px}
|
|
256
|
+
.s{font-size:${Math.round(h * 0.04)}px;font-weight:600;margin:0;opacity:.95}
|
|
257
|
+
</style>
|
|
258
|
+
<div class="shot"></div><div class="veil"></div><div class="foot"></div>
|
|
259
|
+
${brand.logo ? `<div class="badge"><img src="${dataUri(brand.logo)}" alt=""></div>` : ""}
|
|
260
|
+
<div class="txt">${spec.kicker ? `<div class="k">${escapeHtml(spec.kicker)}</div>` : ""}<p class="t">${escapeHtml(spec.title)}</p>${spec.subtitle ? `<div class="bar"></div><p class="s">${escapeHtml(spec.subtitle)}</p>` : ""}</div>`;
|
|
261
|
+
const browser = await launchChromium();
|
|
164
262
|
try {
|
|
165
263
|
const page = await browser.newPage({ viewport: { width: w, height: h } });
|
|
166
264
|
await page.setContent(html, { waitUntil: "load" });
|