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.
Files changed (110) hide show
  1. package/README.md +3 -1
  2. package/package.json +49 -10
  3. package/skill/SKILL.md +29 -19
  4. package/skill/THIRD_PARTY.md +3 -3
  5. package/skill/commands/launch-film.md +1 -1
  6. package/skill/reference/art-styles.md +70 -0
  7. package/skill/reference/backgrounds.md +1 -1
  8. package/skill/reference/brand-motion.md +62 -0
  9. package/skill/reference/clips.md +3 -3
  10. package/skill/reference/component-authoring.md +1 -1
  11. package/skill/reference/continuity.md +5 -5
  12. package/skill/reference/delivery-review.md +40 -0
  13. package/skill/reference/hebrew-rtl.md +3 -4
  14. package/skill/reference/{remotion-composition.md → hyperframes-composition.md} +14 -8
  15. package/skill/reference/kit.md +7 -3
  16. package/skill/reference/launch-film.md +13 -9
  17. package/skill/reference/motion-design.md +18 -16
  18. package/skill/reference/sound-design.md +34 -12
  19. package/skill/reference/studio-editing.md +55 -0
  20. package/skill/reference/styles.md +9 -6
  21. package/skill/reference/three-d.md +8 -7
  22. package/skill/reference/voice-sync.md +1 -1
  23. package/src/commands/assets.ts +5 -1
  24. package/src/contract/index.ts +1 -1
  25. package/src/hyperframes/Root.tsx +1 -0
  26. package/src/hyperframes/fonts.ts +54 -0
  27. package/src/hyperframes/frame.tsx +46 -0
  28. package/src/hyperframes/host.tsx +38 -0
  29. package/src/{remotion → hyperframes}/kit/Assemble3D.tsx +1 -1
  30. package/src/hyperframes/kit/BrandTransform3D.tsx +12 -0
  31. package/src/{remotion → hyperframes}/kit/BrowserFrame.tsx +1 -1
  32. package/src/{remotion → hyperframes}/kit/Camera.tsx +1 -1
  33. package/src/{remotion → hyperframes}/kit/Captions.tsx +1 -1
  34. package/src/{remotion → hyperframes}/kit/Card3D.tsx +2 -2
  35. package/src/{remotion → hyperframes}/kit/Carry.tsx +1 -1
  36. package/src/{remotion → hyperframes}/kit/ChapterFrame.tsx +1 -1
  37. package/src/{remotion → hyperframes}/kit/ClipLayer.tsx +1 -1
  38. package/src/{remotion → hyperframes}/kit/Counter.tsx +1 -1
  39. package/src/{remotion → hyperframes}/kit/CounterRoll.tsx +1 -1
  40. package/src/{remotion → hyperframes}/kit/Entrance.tsx +1 -1
  41. package/src/{remotion → hyperframes}/kit/FootageLayer.tsx +1 -1
  42. package/src/{remotion → hyperframes}/kit/GlassPanel.tsx +1 -1
  43. package/src/{remotion → hyperframes}/kit/Grounds.tsx +1 -1
  44. package/src/{remotion → hyperframes}/kit/Headline.tsx +1 -1
  45. package/src/{remotion → hyperframes}/kit/Hero3D.tsx +1 -1
  46. package/src/{remotion → hyperframes}/kit/HudOverlay.tsx +1 -1
  47. package/src/{remotion → hyperframes}/kit/ImageLayers.tsx +1 -1
  48. package/src/{remotion → hyperframes}/kit/KenBurnsImage.tsx +1 -1
  49. package/src/{remotion → hyperframes}/kit/KeyedClip.tsx +1 -1
  50. package/src/{remotion → hyperframes}/kit/Layers.tsx +1 -1
  51. package/src/{remotion → hyperframes}/kit/LowerThird.tsx +1 -1
  52. package/src/{remotion → hyperframes}/kit/Music.tsx +1 -1
  53. package/src/{remotion → hyperframes}/kit/NamedCursor.tsx +1 -1
  54. package/src/{remotion → hyperframes}/kit/Orbit3D.tsx +1 -1
  55. package/src/{remotion → hyperframes}/kit/Particles3D.tsx +1 -1
  56. package/src/{remotion → hyperframes}/kit/Place.tsx +1 -1
  57. package/src/{remotion → hyperframes}/kit/PromptBox.tsx +1 -1
  58. package/src/{remotion → hyperframes}/kit/Scene3D.tsx +2 -2
  59. package/src/{remotion → hyperframes}/kit/SceneFrame.tsx +1 -1
  60. package/src/{remotion → hyperframes}/kit/ScreenOverlay.tsx +1 -1
  61. package/src/{remotion → hyperframes}/kit/Sfx.tsx +1 -1
  62. package/src/{remotion → hyperframes}/kit/SoundCues.tsx +1 -1
  63. package/src/{remotion → hyperframes}/kit/TerminalLog.tsx +1 -1
  64. package/src/{remotion → hyperframes}/kit/Text3D.tsx +1 -1
  65. package/src/{remotion → hyperframes}/kit/TextOnImage.tsx +1 -1
  66. package/src/{remotion → hyperframes}/kit/TitleCard.tsx +1 -1
  67. package/src/{remotion → hyperframes}/kit/Voiceover.tsx +1 -1
  68. package/src/{remotion → hyperframes}/kit/Warp3D.tsx +1 -1
  69. package/src/hyperframes/kit/brand-transform.ts +25 -0
  70. package/src/{remotion → hyperframes}/kit/docs.ts +6 -2
  71. package/src/{remotion → hyperframes}/kit/index.ts +3 -0
  72. package/src/{remotion → hyperframes}/kit/sound-cues.ts +1 -1
  73. package/src/{remotion → hyperframes}/kit/sound-kinds.ts +14 -1
  74. package/src/{remotion → hyperframes}/kit/theme.ts +42 -40
  75. package/src/{remotion → hyperframes}/kit/ui-math.ts +1 -1
  76. package/src/hyperframes/math.ts +62 -0
  77. package/src/hyperframes/three.tsx +10 -0
  78. package/src/project/chromakey.ts +1 -1
  79. package/src/project/layers.ts +1 -1
  80. package/src/project/serve.ts +2 -2
  81. package/src/render/component-preview.ts +11 -55
  82. package/src/render/render.ts +62 -64
  83. package/src/render/serve.ts +31 -0
  84. package/src/render/static-check.ts +15 -4
  85. package/src/render/validate.ts +4 -4
  86. package/src/render/word-check.ts +1 -1
  87. package/src/render/worker.ts +71 -0
  88. package/src/testing/fixtures.ts +1 -1
  89. package/src/remotion/Root.tsx +0 -31
  90. /package/src/{remotion → hyperframes}/kit/Icon.tsx +0 -0
  91. /package/src/{remotion → hyperframes}/kit/beat.ts +0 -0
  92. /package/src/{remotion → hyperframes}/kit/bg-math.ts +0 -0
  93. /package/src/{remotion → hyperframes}/kit/brand-icons.ts +0 -0
  94. /package/src/{remotion → hyperframes}/kit/caption-groups.ts +0 -0
  95. /package/src/{remotion → hyperframes}/kit/caption-style.ts +0 -0
  96. /package/src/{remotion → hyperframes}/kit/image-layers-math.ts +0 -0
  97. /package/src/{remotion → hyperframes}/kit/inter-bold-typeface.ts +0 -0
  98. /package/src/{remotion → hyperframes}/kit/media.ts +0 -0
  99. /package/src/{remotion → hyperframes}/kit/motion-math.ts +0 -0
  100. /package/src/{remotion → hyperframes}/kit/music-math.ts +0 -0
  101. /package/src/{remotion → hyperframes}/kit/quiet-three.ts +0 -0
  102. /package/src/{remotion → hyperframes}/kit/sample-text.ts +0 -0
  103. /package/src/{remotion → hyperframes}/kit/scene3d-context.ts +0 -0
  104. /package/src/{remotion → hyperframes}/kit/seeded.ts +0 -0
  105. /package/src/{remotion → hyperframes}/kit/three-fx-math.ts +0 -0
  106. /package/src/{remotion → hyperframes}/kit/three-math.ts +0 -0
  107. /package/src/{remotion → hyperframes}/kit/transition-math.ts +0 -0
  108. /package/src/{remotion → hyperframes}/kit/ui-theme.ts +0 -0
  109. /package/src/{remotion → hyperframes}/kit/word-anchor.ts +0 -0
  110. /package/src/{remotion → hyperframes}/types.ts +0 -0
@@ -1,81 +1,79 @@
1
- import { mkdir, rm, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
- import { bundle } from "@remotion/bundler";
4
- import { ensureBrowser, renderMedia, renderStill, selectComposition } from "@remotion/renderer";
5
- import type { VideoProps } from "../remotion/types";
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, ROOT_FILE } from "./validate";
11
+ import { KIT_INDEX, PKG_ROOT } from "./validate";
8
12
 
9
- const ENTRY_SOURCE = `import { registerRoot } from "remotion";
10
- import { makeRoot } from "reelkit/root";
11
- import { Video } from "../../src/Video";
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", "remotion", "three", "@react-three/fiber", "@remotion/three"].map((n) => [n, packageDir(n)]));
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, ENTRY_SOURCE);
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
- return bundle({
37
- entryPoint: entry,
38
- enableCaching: false,
39
- webpackOverride: (config) => ({
40
- ...config,
41
- resolve: {
42
- ...config.resolve,
43
- alias: { ...(config.resolve?.alias as Record<string, string> | undefined), ...dependencyAliases(), "reelkit/kit": KIT_INDEX, "reelkit/root": ROOT_FILE },
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
- export async function disposeBundle(serveUrl: string): Promise<void> {
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
- await ensureBrowser({ onBrowserDownload: () => { onDownload(); return { version: null, onProgress: () => {} }; } });
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
- // Remotion's headless browser has no GPU, so three.js needs a software GL. "angle" is the setting that draws WebGL here; it is used only for a film that
62
- // has a 3D scene, so every 2D render keeps the browser's own defaults and looks exactly as before. This is the one place that says it.
63
- export const GL_BACKEND = "angle" as const;
64
- export type RenderOptions = { uses3D?: boolean };
65
- const chromium = (o?: RenderOptions) => (o?.uses3D ? { chromiumOptions: { gl: GL_BACKEND } } : {});
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
- export async function renderVideo(serveUrl: string, props: VideoProps, outPath: string, options?: RenderOptions) {
78
- await mkdir(dirname(outPath), { recursive: true });
79
- const composition = await selectComposition({ serveUrl, id: "Video", inputProps: asInput(props), ...chromium(options) });
80
- await renderMedia({ composition, serveUrl, codec: "h264", outputLocation: outPath, inputProps: asInput(props), ...chromium(options) });
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", "remotion", KIT]);
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 remotion)");
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, remotion, ${KIT}, or a component file that exists ("./Name").`
144
- : `${file}: import not allowed: "${s}". A component may only import react, remotion and ${KIT}.`,
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}.`);
@@ -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", "remotion", "kit", "index.ts");
8
- export const ROOT_FILE = join(PKG_ROOT, "src", "remotion", "Root.tsx");
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 remotionDir = packageDir("remotion");
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
- remotion: [remotionDir],
33
+ "reelkit/frame": [frameFile],
34
34
  },
35
35
  });
36
36
  const wanted = new Set(files);
@@ -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 "../remotion/kit/word-anchor";
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
+ }
@@ -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 "remotion";
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
 
@@ -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