reelkit-cli 0.8.0 → 0.10.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/README.md +3 -1
- package/package.json +49 -10
- package/skill/SKILL.md +29 -19
- package/skill/THIRD_PARTY.md +3 -3
- package/skill/commands/launch-film.md +1 -1
- package/skill/reference/art-styles.md +70 -0
- package/skill/reference/backgrounds.md +1 -1
- package/skill/reference/brand-motion.md +62 -0
- package/skill/reference/clips.md +3 -3
- package/skill/reference/component-authoring.md +1 -1
- package/skill/reference/continuity.md +5 -5
- package/skill/reference/delivery-review.md +40 -0
- package/skill/reference/hebrew-rtl.md +3 -4
- package/skill/reference/{remotion-composition.md → hyperframes-composition.md} +14 -8
- package/skill/reference/kit.md +7 -3
- package/skill/reference/launch-film.md +13 -9
- package/skill/reference/motion-design.md +18 -16
- package/skill/reference/sound-design.md +34 -12
- package/skill/reference/studio-editing.md +55 -0
- package/skill/reference/styles.md +9 -6
- package/skill/reference/three-d.md +8 -7
- package/skill/reference/voice-sync.md +1 -1
- package/src/commands/assets.ts +5 -1
- package/src/contract/index.ts +1 -1
- package/src/hyperframes/Root.tsx +1 -0
- package/src/hyperframes/fonts.ts +54 -0
- package/src/hyperframes/frame.tsx +46 -0
- package/src/hyperframes/host.tsx +38 -0
- package/src/{remotion → hyperframes}/kit/Assemble3D.tsx +1 -1
- package/src/hyperframes/kit/BrandTransform3D.tsx +12 -0
- package/src/{remotion → hyperframes}/kit/BrowserFrame.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Camera.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Captions.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Card3D.tsx +2 -2
- package/src/{remotion → hyperframes}/kit/Carry.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/ChapterFrame.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/ClipLayer.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Counter.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/CounterRoll.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Entrance.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/FootageLayer.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/GlassPanel.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Grounds.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Headline.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Hero3D.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/HudOverlay.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/ImageLayers.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/KenBurnsImage.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/KeyedClip.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Layers.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/LowerThird.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Music.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/NamedCursor.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Orbit3D.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Particles3D.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Place.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/PromptBox.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Scene3D.tsx +2 -2
- package/src/{remotion → hyperframes}/kit/SceneFrame.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/ScreenOverlay.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Sfx.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/SoundCues.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/TerminalLog.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Text3D.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/TextOnImage.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/TitleCard.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Voiceover.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Warp3D.tsx +1 -1
- package/src/hyperframes/kit/brand-transform.ts +25 -0
- package/src/{remotion → hyperframes}/kit/docs.ts +6 -2
- package/src/{remotion → hyperframes}/kit/index.ts +3 -0
- package/src/{remotion → hyperframes}/kit/sound-cues.ts +1 -1
- package/src/{remotion → hyperframes}/kit/sound-kinds.ts +14 -1
- package/src/{remotion → hyperframes}/kit/theme.ts +42 -40
- package/src/{remotion → hyperframes}/kit/ui-math.ts +1 -1
- package/src/hyperframes/math.ts +62 -0
- package/src/hyperframes/three.tsx +10 -0
- package/src/project/chromakey.ts +1 -1
- package/src/project/layers.ts +1 -1
- package/src/project/serve.ts +2 -2
- package/src/render/component-preview.ts +11 -55
- package/src/render/render.ts +62 -64
- package/src/render/serve.ts +31 -0
- package/src/render/static-check.ts +15 -4
- package/src/render/validate.ts +4 -4
- package/src/render/word-check.ts +1 -1
- package/src/render/worker.ts +71 -0
- package/src/testing/fixtures.ts +1 -1
- package/src/remotion/Root.tsx +0 -31
- /package/src/{remotion → hyperframes}/kit/Icon.tsx +0 -0
- /package/src/{remotion → hyperframes}/kit/beat.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/bg-math.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/brand-icons.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/caption-groups.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/caption-style.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/image-layers-math.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/inter-bold-typeface.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/media.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/motion-math.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/music-math.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/quiet-three.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/sample-text.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/scene3d-context.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/seeded.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/three-fx-math.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/three-math.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/transition-math.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/ui-theme.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/word-anchor.ts +0 -0
- /package/src/{remotion → hyperframes}/types.ts +0 -0
package/src/render/render.ts
CHANGED
|
@@ -1,81 +1,79 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { cp, mkdir, mkdtemp, rm, writeFile, readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join, basename } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { build } from "esbuild";
|
|
8
|
+
import type { VideoProps } from "../hyperframes/types";
|
|
9
|
+
import { fontFamilies } from "../hyperframes/fonts";
|
|
6
10
|
import { packageDir } from "./deps";
|
|
7
|
-
import { KIT_INDEX,
|
|
11
|
+
import { KIT_INDEX, PKG_ROOT } from "./validate";
|
|
8
12
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
registerRoot(makeRoot(Video));
|
|
14
|
-
`;
|
|
15
|
-
|
|
16
|
-
// The project folder has no node_modules. Each dependency is pinned to the one copy installed with this package, so the
|
|
17
|
-
// kit, Root and the user's composition all share a single react and remotion (a second react breaks hooks), and the 3D layer a single three
|
|
18
|
-
// (two copies of three break instanceof checks). User code still may not import them: only the kit does.
|
|
13
|
+
const run = promisify(execFile);
|
|
14
|
+
const HOST = join(PKG_ROOT, "src/hyperframes/host.tsx");
|
|
15
|
+
const FRAME = join(PKG_ROOT, "src/hyperframes/frame.tsx");
|
|
19
16
|
export function dependencyAliases(): Record<string, string> {
|
|
20
|
-
return Object.fromEntries(["react", "react-dom", "
|
|
17
|
+
return Object.fromEntries(["react", "react-dom", "three", "@react-three/fiber"].map((name) => [name, packageDir(name)]));
|
|
21
18
|
}
|
|
22
|
-
|
|
23
|
-
// Writes the bundler's entry file into the project. The composition itself stays where the user's Claude wrote it.
|
|
24
19
|
export async function writeEntry(projectDir: string): Promise<string> {
|
|
25
20
|
const dir = join(projectDir, ".reelkit", "build");
|
|
26
|
-
await rm(dir, { recursive: true, force: true });
|
|
27
|
-
await mkdir(dir, { recursive: true });
|
|
21
|
+
await rm(dir, { recursive: true, force: true }); await mkdir(dir, { recursive: true });
|
|
28
22
|
const entry = join(dir, "index.ts");
|
|
29
|
-
await writeFile(entry,
|
|
23
|
+
await writeFile(entry, 'import { mountVideo } from "reelkit/host";\nimport { Video } from "../../src/Video";\nmountVideo(Video);\n');
|
|
30
24
|
return entry;
|
|
31
25
|
}
|
|
32
|
-
|
|
33
|
-
// Webpack's on-disk cache would only grow, by tens of megabytes per render. It is switched off, and each bundle
|
|
34
|
-
// is deleted by disposeBundle once it has been used.
|
|
26
|
+
// A self-contained HyperFrames page using the package's copies of React and Three.
|
|
35
27
|
export async function bundleProject(entry: string): Promise<string> {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
}
|
|
46
|
-
|
|
28
|
+
const dir = await mkdtemp(join(tmpdir(), "reelkit-hyperframes-"));
|
|
29
|
+
try {
|
|
30
|
+
await build({ entryPoints: [entry], bundle: true, outfile: join(dir, "composition.js"), platform: "browser", format: "iife", jsx: "automatic", logLevel: "silent", loader: { ".woff2": "file", ".woff": "file", ".ttf": "file" }, define: { "process.env.NODE_ENV": '"production"' }, alias: { ...dependencyAliases(), "reelkit/kit": KIT_INDEX, "reelkit/frame": FRAME, "reelkit/host": HOST } });
|
|
31
|
+
await mkdir(join(dir, "fonts"));
|
|
32
|
+
for (const slug of Object.values(fontFamilies)) {
|
|
33
|
+
const source = packageDir(`@fontsource/${slug}`);
|
|
34
|
+
await cp(join(source, "files"), join(dir, "fonts", "files"), { recursive: true });
|
|
35
|
+
const sheets = (await readdir(source)).filter((name) => /^\d+(?:-italic)?\.css$/.test(name));
|
|
36
|
+
await writeFile(join(dir, "fonts", `${slug}.css`), (await Promise.all(sheets.map((name) => readFile(join(source, name), "utf8")))).join("\n"));
|
|
37
|
+
}
|
|
38
|
+
await writeFile(join(dir, "index.html"), '<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;overflow:hidden}#stage{position:relative;overflow:hidden}</style></head><body><div id="stage"></div><script src="props.js"></script><script src="composition.js"></script></body></html>');
|
|
39
|
+
return dir;
|
|
40
|
+
} catch (error) { await rm(dir, { recursive: true, force: true }); throw error; }
|
|
47
41
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// Only ever remove a folder that is one of Remotion's own temporary bundles.
|
|
51
|
-
if (/remotion-webpack-bundle-/.test(serveUrl)) await rm(serveUrl, { recursive: true, force: true });
|
|
42
|
+
export async function disposeBundle(dir: string): Promise<void> {
|
|
43
|
+
if (dirname(dir) === tmpdir() && basename(dir).startsWith("reelkit-hyperframes-")) await rm(dir, { recursive: true, force: true });
|
|
52
44
|
}
|
|
53
|
-
|
|
54
|
-
// Makes sure Remotion's headless browser is installed. onDownload is called once, only when a download is about to begin.
|
|
55
|
-
// Remotion's own progress bar is replaced because it writes to stdout, which --json keeps for the result alone.
|
|
56
45
|
export type EnsureBrowser = (onDownload: () => void) => Promise<void>;
|
|
46
|
+
export const GL_BACKEND = "software" as const;
|
|
47
|
+
export type RenderOptions = { uses3D?: boolean; silent?: boolean; crf?: number };
|
|
48
|
+
export function browserPath(): string | undefined {
|
|
49
|
+
if (process.env.REELKIT_CHROME_PATH) return process.env.REELKIT_CHROME_PATH;
|
|
50
|
+
const paths = process.platform === "darwin" ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium"] : process.platform === "linux" ? ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome"] : [];
|
|
51
|
+
return paths.find(existsSync);
|
|
52
|
+
}
|
|
57
53
|
export const ensureRenderBrowser: EnsureBrowser = async (onDownload) => {
|
|
58
|
-
|
|
54
|
+
if (browserPath()) return;
|
|
55
|
+
const { resolveHeadlessShellPath } = await import("@hyperframes/engine");
|
|
56
|
+
const cached = resolveHeadlessShellPath({}) ?? resolveHeadlessShellPath({ chromePath: undefined });
|
|
57
|
+
if (cached) { process.env.REELKIT_CHROME_PATH = cached; return; }
|
|
58
|
+
const { install, Browser, resolveBuildId, detectBrowserPlatform, getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
59
|
+
const installed = (await getInstalledBrowsers({ cacheDir: join(PKG_ROOT, ".browser") })).find((b) => b.browser === Browser.CHROMEHEADLESSSHELL && existsSync(b.executablePath));
|
|
60
|
+
if (installed) { process.env.REELKIT_CHROME_PATH = installed.executablePath; return; }
|
|
61
|
+
const platform = detectBrowserPlatform(); if (!platform) throw new Error("No supported Chrome platform. Set REELKIT_CHROME_PATH.");
|
|
62
|
+
onDownload();
|
|
63
|
+
const browser = await install({ browser: Browser.CHROMEHEADLESSSHELL, buildId: await resolveBuildId(Browser.CHROMEHEADLESSSHELL, platform, "stable"), cacheDir: join(PKG_ROOT, ".browser"), platform });
|
|
64
|
+
process.env.REELKIT_CHROME_PATH = browser.executablePath;
|
|
59
65
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const asInput = (props: VideoProps) => props as unknown as Record<string, unknown>;
|
|
68
|
-
|
|
69
|
-
export async function renderStills(serveUrl: string, props: VideoProps, frames: number[], outDir: string, options?: RenderOptions) {
|
|
70
|
-
await mkdir(outDir, { recursive: true });
|
|
71
|
-
const composition = await selectComposition({ serveUrl, id: "Video", inputProps: asInput(props), ...chromium(options) });
|
|
72
|
-
for (const frame of frames) {
|
|
73
|
-
await renderStill({ composition, serveUrl, frame, output: join(outDir, `still-${frame}.png`), inputProps: asInput(props), ...chromium(options) });
|
|
74
|
-
}
|
|
66
|
+
async function render(dir: string, props: VideoProps, mode: "stills" | "video", output: string, frames: number[], options?: RenderOptions) {
|
|
67
|
+
await ensureRenderBrowser(() => {});
|
|
68
|
+
await writeFile(join(dir, "props.js"), `window.__reelkitProps=${JSON.stringify(props).replace(/</g, "\\u003c")};`);
|
|
69
|
+
// Isolate engine diagnostics from the CLI's JSON stdout.
|
|
70
|
+
const request = join(dir, "render-request.json");
|
|
71
|
+
await writeFile(request, JSON.stringify({ dir, props, mode, output, frames, options, chromePath: browserPath() }));
|
|
72
|
+
await run(process.execPath, ["--import", join(packageDir("tsx"), "dist/loader.mjs"), join(PKG_ROOT, "src/render/worker.ts"), request], { maxBuffer: 16 * 1024 * 1024 });
|
|
75
73
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await
|
|
74
|
+
export async function renderStills(dir: string, props: VideoProps, frames: number[], outDir: string, options?: RenderOptions) {
|
|
75
|
+
await mkdir(outDir, { recursive: true }); await render(dir, props, "stills", outDir, frames, options);
|
|
76
|
+
}
|
|
77
|
+
export async function renderVideo(dir: string, props: VideoProps, outPath: string, options?: RenderOptions) {
|
|
78
|
+
await mkdir(dirname(outPath), { recursive: true }); await render(dir, props, "video", outPath, [], options);
|
|
81
79
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createReadStream, statSync } from "node:fs";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import type { AddressInfo } from "node:net";
|
|
5
|
+
import { extname, resolve, sep } from "node:path";
|
|
6
|
+
|
|
7
|
+
const TYPES: Record<string, string> = {
|
|
8
|
+
".html": "text/html; charset=utf-8", ".js": "application/javascript", ".css": "text/css",
|
|
9
|
+
".json": "application/json", ".woff2": "font/woff2", ".woff": "font/woff", ".ttf": "font/ttf",
|
|
10
|
+
".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Serve the generated composition only to localhost, under an unguessable per-render prefix.
|
|
14
|
+
// The generic HyperFrames file server listens on all interfaces; project copy and media URLs stay private here.
|
|
15
|
+
export async function serveComposition(dir: string): Promise<{ url: string; close(): Promise<void> }> {
|
|
16
|
+
const root = resolve(dir), prefix = `/${randomBytes(16).toString("hex")}/`;
|
|
17
|
+
const server = createServer((req, res) => {
|
|
18
|
+
let path: string;
|
|
19
|
+
try { path = decodeURIComponent((req.url ?? "").split("?")[0]!); } catch { res.writeHead(400).end(); return; }
|
|
20
|
+
if (!path.startsWith(prefix)) { res.writeHead(404).end(); return; }
|
|
21
|
+
const file = resolve(root, path.slice(prefix.length) || "index.html");
|
|
22
|
+
try {
|
|
23
|
+
if (!file.startsWith(root + sep) || !statSync(file).isFile()) { res.writeHead(404).end(); return; }
|
|
24
|
+
res.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream", "cache-control": "no-store" });
|
|
25
|
+
const stream = createReadStream(file); stream.on("error", () => res.destroy()); stream.pipe(res);
|
|
26
|
+
} catch { res.writeHead(404).end(); }
|
|
27
|
+
});
|
|
28
|
+
await new Promise<void>((ready, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", ready); });
|
|
29
|
+
const port = (server.address() as AddressInfo).port;
|
|
30
|
+
return { url: `http://127.0.0.1:${port}${prefix}`, close: () => new Promise<void>((ready) => { server.closeAllConnections(); server.close(() => ready()); }) };
|
|
31
|
+
}
|
|
@@ -7,8 +7,19 @@ export const MAIN_FILE = "Video.tsx";
|
|
|
7
7
|
export const FILE_NAME = /^[A-Z][A-Za-z0-9]*\.tsx$/;
|
|
8
8
|
export const componentName = (file: string) => file.replace(/\.tsx$/, "");
|
|
9
9
|
|
|
10
|
+
// Upgrade previously published library components without touching on-screen copy or comments.
|
|
11
|
+
export function migrateFrameImports(source: string): string {
|
|
12
|
+
const parsed = sourceFile(source);
|
|
13
|
+
const edits: { start: number; end: number }[] = [];
|
|
14
|
+
for (const node of parsed.statements) {
|
|
15
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "remotion") edits.push({ start: node.moduleSpecifier.getStart(parsed), end: node.moduleSpecifier.getEnd() });
|
|
16
|
+
}
|
|
17
|
+
for (const edit of edits.reverse()) source = source.slice(0, edit.start) + '"reelkit/frame"' + source.slice(edit.end);
|
|
18
|
+
return source;
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
export const KIT = "reelkit/kit";
|
|
11
|
-
const BASE_IMPORTS = new Set(["react", "
|
|
22
|
+
const BASE_IMPORTS = new Set(["react", "reelkit/frame", KIT]);
|
|
12
23
|
|
|
13
24
|
// This check keeps honest compositions deterministic and self-contained. It is NOT a security boundary:
|
|
14
25
|
// code in src/ runs on the user's machine when it is bundled and rendered, and anyone determined can get around a list of names.
|
|
@@ -77,7 +88,7 @@ function inspectSource(source: string, file?: string): Finding {
|
|
|
77
88
|
return Boolean(x && ts.isIdentifier(x) && x.text === name && !declared(x));
|
|
78
89
|
};
|
|
79
90
|
const flagMember = (object: ts.Expression | undefined, member: string | undefined) => {
|
|
80
|
-
if (isGlobal(object, "Math") && member === "random") forbidden.add("Math.random (use random(seed) from
|
|
91
|
+
if (isGlobal(object, "Math") && member === "random") forbidden.add("Math.random (use random(seed) from reelkit/frame)");
|
|
81
92
|
if (isGlobal(object, "Date") && member === "now") forbidden.add("the clock (Date)");
|
|
82
93
|
};
|
|
83
94
|
// The key a destructuring element reads: `random` in `{ random }` and in `{ random: r }`, "random" in `{ ["random"]: r }`.
|
|
@@ -140,8 +151,8 @@ export function staticCheck(source: string, file: string, siblings: string[]): s
|
|
|
140
151
|
if (BASE_IMPORTS.has(s) || allowedSiblings.has(s)) continue;
|
|
141
152
|
errors.push(
|
|
142
153
|
isMain
|
|
143
|
-
? `${file}: import not allowed: "${s}". Use react,
|
|
144
|
-
: `${file}: import not allowed: "${s}". A component may only import react,
|
|
154
|
+
? `${file}: import not allowed: "${s}". Use react, reelkit/frame, ${KIT}, or a component file that exists ("./Name").`
|
|
155
|
+
: `${file}: import not allowed: "${s}". A component may only import react, reelkit/frame and ${KIT}.`,
|
|
145
156
|
);
|
|
146
157
|
}
|
|
147
158
|
for (const name of found.forbidden) errors.push(`${file}: not allowed: ${name}.`);
|
package/src/render/validate.ts
CHANGED
|
@@ -4,15 +4,15 @@ import ts from "typescript";
|
|
|
4
4
|
import { packageDir } from "./deps";
|
|
5
5
|
|
|
6
6
|
export const PKG_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
|
7
|
-
export const KIT_INDEX = join(PKG_ROOT, "src", "
|
|
8
|
-
export const ROOT_FILE = join(PKG_ROOT, "src", "
|
|
7
|
+
export const KIT_INDEX = join(PKG_ROOT, "src", "hyperframes", "kit", "index.ts");
|
|
8
|
+
export const ROOT_FILE = join(PKG_ROOT, "src", "hyperframes", "Root.tsx");
|
|
9
9
|
|
|
10
10
|
// The static check lives in its own module, which a server can bundle; everything it exports is available from here as before.
|
|
11
11
|
export { FILE_NAME, MAIN_FILE, componentName, importSpecifiers, staticCheck } from "./static-check";
|
|
12
12
|
import { KIT, sourceFile, unwrap } from "./static-check";
|
|
13
13
|
|
|
14
14
|
const typesReact = packageDir("@types/react");
|
|
15
|
-
export const
|
|
15
|
+
export const frameFile = join(PKG_ROOT, "src", "hyperframes", "frame.tsx");
|
|
16
16
|
|
|
17
17
|
export function typecheck(files: string[]): string[] {
|
|
18
18
|
const program = ts.createProgram(files, {
|
|
@@ -30,7 +30,7 @@ export function typecheck(files: string[]): string[] {
|
|
|
30
30
|
[KIT]: [KIT_INDEX],
|
|
31
31
|
react: [typesReact],
|
|
32
32
|
"react/*": [join(typesReact, "*")],
|
|
33
|
-
|
|
33
|
+
"reelkit/frame": [frameFile],
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
const wanted = new Set(files);
|
package/src/render/word-check.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// the word is in that scene's narration. Pure text in, findings out (the TypeScript syntax tree, never regular expressions on code), so `check`
|
|
3
3
|
// and `preview` can both use it.
|
|
4
4
|
import ts from "typescript";
|
|
5
|
-
import { normalizeWord, wordFrame, type WordScene } from "../
|
|
5
|
+
import { normalizeWord, wordFrame, type WordScene } from "../hyperframes/kit/word-anchor";
|
|
6
6
|
import { sourceFile, unwrap } from "./static-check";
|
|
7
7
|
|
|
8
8
|
export const WORD_HELPERS = ["wordFrame", "wordFrames", "onWord", "onWordBeat"] as const;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Internal subprocess: HyperFrames owns capture, encoding and audio mixing.
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { cp, mkdir, mkdtemp, rm, writeFile, readFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, extname } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { createCaptureSession, initializeSession, captureFrame, closeCaptureSession, encodeFramesFromDir, processCompositionAudio, muxVideoWithAudio, applyFaststart, type AudioElement } from "@hyperframes/engine";
|
|
8
|
+
import { serveComposition } from "./serve";
|
|
9
|
+
import type { VideoProps } from "../hyperframes/types";
|
|
10
|
+
import type { AudioRecord } from "../hyperframes/frame";
|
|
11
|
+
import type { RenderOptions } from "./render";
|
|
12
|
+
|
|
13
|
+
const run = promisify(execFile);
|
|
14
|
+
const input = JSON.parse(await readFile(process.argv[2]!, "utf8")) as { dir: string; props: VideoProps; mode: "stills" | "video"; output: string; frames: number[]; options?: RenderOptions; chromePath?: string };
|
|
15
|
+
const m = input.props.manifest, fps = { num: m.fps, den: 1 };
|
|
16
|
+
const work = await mkdtemp(join(tmpdir(), "reelkit-capture-"));
|
|
17
|
+
const server = await serveComposition(input.dir);
|
|
18
|
+
let session: Awaited<ReturnType<typeof createCaptureSession>> | undefined;
|
|
19
|
+
try {
|
|
20
|
+
session = await createCaptureSession(server.url.replace(/\/$/, ""), work, { width: m.width, height: m.height, fps, format: "png", compositionDurationSeconds: m.totalFrames / m.fps }, async (page) => {
|
|
21
|
+
await page.evaluate(async () => { await (window as unknown as { __reelkitReady: Promise<void> }).__reelkitReady; });
|
|
22
|
+
}, { chromePath: input.chromePath, forceScreenshot: true, useDrawElement: false, staticFrameDedup: false, enableBrowserPool: false, browserGpuMode: input.options?.uses3D ? "software" : "auto" });
|
|
23
|
+
await initializeSession(session);
|
|
24
|
+
await session.page.evaluate(async () => { await (window as unknown as { __reelkitReady: Promise<void> }).__reelkitReady; });
|
|
25
|
+
const frames = input.mode === "stills" ? input.frames : Array.from({ length: m.totalFrames }, (_, i) => i);
|
|
26
|
+
for (const frame of frames) {
|
|
27
|
+
if (!Number.isInteger(frame) || frame < 0 || frame >= m.totalFrames) throw new Error(`Frame ${frame} is outside the composition.`);
|
|
28
|
+
const captured = await captureFrame(session, frame, frame / m.fps);
|
|
29
|
+
if (input.mode === "stills") await cp(captured.path, join(input.output, `still-${frame}.png`));
|
|
30
|
+
}
|
|
31
|
+
if (input.mode === "video") {
|
|
32
|
+
const silent = join(work, "silent.mp4");
|
|
33
|
+
const encoded = await encodeFramesFromDir(work, "frame_%06d.png", silent, { fps, width: m.width, height: m.height, codec: "h264", pixelFormat: "yuv420p", quality: input.options?.crf ?? 18, preset: "fast" });
|
|
34
|
+
if (!encoded.success) throw new Error(encoded.error ?? "HyperFrames encoding failed.");
|
|
35
|
+
const records = input.options?.silent ? [] : await session.page.evaluate(() => [...(window as unknown as { __reelkitAudio: Map<string, AudioRecord> }).__reelkitAudio.values()]);
|
|
36
|
+
const audio: AudioElement[] = [];
|
|
37
|
+
const localSources = new Map<string, string>();
|
|
38
|
+
for (const record of records) {
|
|
39
|
+
let src = localSources.get(record.src);
|
|
40
|
+
if (!src) {
|
|
41
|
+
const response = await fetch(record.src); if (!response.ok) throw new Error(`Audio source returned ${response.status}.`);
|
|
42
|
+
src = join(work, `source-${localSources.size}${extname(new URL(record.src).pathname) || ".media"}`);
|
|
43
|
+
await writeFile(src, new Uint8Array(await response.arrayBuffer())); localSources.set(record.src, src);
|
|
44
|
+
}
|
|
45
|
+
const info = JSON.parse((await run("ffprobe", ["-v", "error", "-show_format", "-of", "json", src])).stdout) as { format: { duration: string } };
|
|
46
|
+
let end = Math.min(record.end, record.start + Number(info.format.duration) - record.mediaStart);
|
|
47
|
+
if (record.loop) {
|
|
48
|
+
const looped = join(work, `loop-${audio.length}.wav`);
|
|
49
|
+
await run("ffmpeg", ["-y", "-v", "error", "-stream_loop", "-1", "-i", src, "-t", String(record.end - record.start + record.mediaStart), "-vn", looped]);
|
|
50
|
+
src = looped; end = record.end;
|
|
51
|
+
}
|
|
52
|
+
if (end <= record.start) continue;
|
|
53
|
+
const keys = [...new Map(record.volumeKeyframes.map((k) => [k.time, k])).values()].sort((a, b) => a.time - b.time);
|
|
54
|
+
audio.push({ ...record, src, end, volumeKeyframes: keys.length ? keys : undefined });
|
|
55
|
+
}
|
|
56
|
+
if (audio.length) {
|
|
57
|
+
const mixed = join(work, "audio.m4a");
|
|
58
|
+
const result = await processCompositionAudio(audio, work, join(work, "audio-work"), mixed, m.totalFrames / m.fps);
|
|
59
|
+
if (!result.success) throw new Error(result.error ?? "HyperFrames audio mixing failed.");
|
|
60
|
+
const muxed = await muxVideoWithAudio(silent, mixed, input.output, undefined, { audioCodec: "aac" });
|
|
61
|
+
if (!muxed.success) throw new Error(muxed.error ?? "HyperFrames audio mux failed.");
|
|
62
|
+
} else {
|
|
63
|
+
await mkdir(join(work, "faststart"));
|
|
64
|
+
const result = await applyFaststart(silent, input.output);
|
|
65
|
+
if (!result.success) throw new Error(result.error ?? "Faststart failed.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} finally {
|
|
69
|
+
if (session) await closeCaptureSession(session);
|
|
70
|
+
await server.close(); await rm(work, { recursive: true, force: true });
|
|
71
|
+
}
|
package/src/testing/fixtures.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const PIXEL_PNG = new Uint8Array(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAE
|
|
|
12
12
|
|
|
13
13
|
// A known-good composition, used as the worked example for composition authors.
|
|
14
14
|
export const FIXTURE_COMPOSITION = `import React from "react";
|
|
15
|
-
import { AbsoluteFill, useVideoConfig } from "
|
|
15
|
+
import { AbsoluteFill, useVideoConfig } from "reelkit/frame";
|
|
16
16
|
import { BgMesh, Captions, FootageLayer, fonts, Grade, Grain, KenBurnsImage, palettes, SceneFrame, Vignette, Voiceover, WordReveal } from "reelkit/kit";
|
|
17
17
|
import type { VideoProps } from "reelkit/kit";
|
|
18
18
|
|
package/src/remotion/Root.tsx
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Composition } from "remotion";
|
|
3
|
-
import { ManifestContext, UrlsContext } from "./kit/media";
|
|
4
|
-
import type { VideoProps } from "./types";
|
|
5
|
-
|
|
6
|
-
const EMPTY: VideoProps = { manifest: { fps: 30, width: 1080, height: 1920, totalFrames: 1, scenes: [] }, urls: {} };
|
|
7
|
-
|
|
8
|
-
export const makeRoot = (Video: React.FC<VideoProps>): React.FC => {
|
|
9
|
-
const WithMedia: React.FC<VideoProps> = (props) => (
|
|
10
|
-
<UrlsContext.Provider value={props.urls}>
|
|
11
|
-
<ManifestContext.Provider value={props.manifest}>
|
|
12
|
-
<Video {...props} />
|
|
13
|
-
</ManifestContext.Provider>
|
|
14
|
-
</UrlsContext.Provider>
|
|
15
|
-
);
|
|
16
|
-
return () => (
|
|
17
|
-
<Composition
|
|
18
|
-
id="Video"
|
|
19
|
-
component={WithMedia as unknown as React.FC<Record<string, unknown>>}
|
|
20
|
-
durationInFrames={1}
|
|
21
|
-
fps={30}
|
|
22
|
-
width={1080}
|
|
23
|
-
height={1920}
|
|
24
|
-
defaultProps={EMPTY as unknown as Record<string, unknown>}
|
|
25
|
-
calculateMetadata={({ props }) => {
|
|
26
|
-
const m = (props as unknown as VideoProps).manifest;
|
|
27
|
-
return { durationInFrames: m.totalFrames, fps: m.fps, width: m.width, height: m.height };
|
|
28
|
-
}}
|
|
29
|
-
/>
|
|
30
|
-
);
|
|
31
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|