Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ pnpm --filter @hyperframes/core test:hyperframe-runtime-ci # Runtime contract t

## Packages

| Package | Description |
|---|---|
| `@hyperframes/core` | Types, HTML generation, runtime, linter |
| `@hyperframes/engine` | Seekable page-to-video capture engine |
| `@hyperframes/producer` | Full rendering pipeline (capture + encode) |
| `@hyperframes/studio` | Composition editor UI |
| `hyperframes` | CLI for creating, previewing, and rendering |
| Package | Description |
| ----------------------- | ------------------------------------------- |
| `@hyperframes/core` | Types, HTML generation, runtime, linter |
| `@hyperframes/engine` | Seekable page-to-video capture engine |
| `@hyperframes/producer` | Full rendering pipeline (capture + encode) |
| `@hyperframes/studio` | Composition editor UI |
| `hyperframes` | CLI for creating, previewing, and rendering |

## Releasing (Maintainers)

Expand Down
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,25 @@ npx hyperframes render # render to MP4
Define your video as HTML with data attributes:

```html
<div id="stage" data-composition-id="my-video"
data-start="0" data-width="1920" data-height="1080">
<video id="clip-1" data-start="0" data-duration="5"
data-track="0" src="intro.mp4" muted playsinline></video>
<img id="overlay" data-start="2" data-duration="3"
data-track="1" src="logo.png" />
<audio id="bg-music" data-start="0" data-duration="9"
data-track="2" data-volume="0.5" src="music.wav"></audio>
<div id="stage" data-composition-id="my-video" data-start="0" data-width="1920" data-height="1080">
<video
id="clip-1"
data-start="0"
data-duration="5"
data-track="0"
src="intro.mp4"
muted
playsinline
></video>
<img id="overlay" data-start="2" data-duration="3" data-track="1" src="logo.png" />
<audio
id="bg-music"
data-start="0"
data-duration="9"
data-track="2"
data-volume="0.5"
src="music.wav"
></audio>
</div>
```

Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ We will acknowledge receipt within 48 hours and aim to provide a fix or mitigati
## Supported Versions

| Version | Supported |
|---------|-----------|
| ------- | --------- |
| 0.x | Yes |

## Scope
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
"knip": "knip",
"prepare": "test -d .git && lefthook install || true"
},
"pnpm": {
"onlyBuiltDependencies": ["lefthook"]
},
"devDependencies": {
"@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0",
Expand All @@ -31,5 +28,10 @@
"oxlint": "^1.56.0",
"tsx": "^4.21.0",
"typescript": "^5.0.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"lefthook"
]
}
}
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"name": "hyperframes",
"version": "0.1.1",
"description": "HyperFrames CLI — create, preview, and render HTML video compositions",
"type": "module",
"bin": {
"hyperframes": "./dist/cli.js"
},
"files": [
"dist"
],
"type": "module",
"scripts": {
"dev": "tsx src/cli.ts",
"build": "pnpm build:studio && tsup && pnpm build:runtime && pnpm build:copy",
Expand All @@ -30,9 +30,9 @@
"puppeteer-core": "^24.39.1"
},
"devDependencies": {
"@hyperframes/core": "workspace:*",
"@clack/prompts": "^1.1.0",
"@hono/node-server": "^1.0.0",
"@hyperframes/core": "workspace:*",
"@hyperframes/engine": "workspace:*",
"@hyperframes/producer": "workspace:*",
"@types/adm-zip": "^0.5.7",
Expand Down
28 changes: 6 additions & 22 deletions packages/cli/src/browser/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import { execSync } from "node:child_process";
import { existsSync, rmSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import {
Browser,
detectBrowserPlatform,
getInstalledBrowsers,
install,
} from "@puppeteer/browsers";
import { Browser, detectBrowserPlatform, getInstalledBrowsers, install } from "@puppeteer/browsers";

const CHROME_VERSION = "131.0.6778.85";
const CACHE_DIR = join(homedir(), ".cache", "hyperframes", "chrome");
Expand All @@ -18,11 +13,7 @@ export function setBrowserPath(path: string): void {
_browserPathOverride = path;
}

export type BrowserSource =
| "env"
| "cache"
| "system"
| "download";
export type BrowserSource = "env" | "cache" | "system" | "download";

export interface BrowserResult {
executablePath: string;
Expand Down Expand Up @@ -76,9 +67,7 @@ async function findFromCache(): Promise<BrowserResult | undefined> {
}

const installed = await getInstalledBrowsers({ cacheDir: CACHE_DIR });
const match = installed.find(
(b) => b.browser === Browser.CHROMEHEADLESSSHELL,
);
const match = installed.find((b) => b.browser === Browser.CHROMEHEADLESSSHELL);
if (match) {
return { executablePath: match.executablePath, source: "cache" };
}
Expand All @@ -93,8 +82,7 @@ function findFromSystem(): BrowserResult | undefined {
}
}

const fromWhich =
whichBinary("google-chrome") ?? whichBinary("chromium");
const fromWhich = whichBinary("google-chrome") ?? whichBinary("chromium");
if (fromWhich) {
return { executablePath: fromWhich, source: "system" };
}
Expand Down Expand Up @@ -122,17 +110,13 @@ export async function findBrowser(): Promise<BrowserResult | undefined> {
* Find or download a browser.
* Resolution: env var -> cached download -> system Chrome -> auto-download.
*/
export async function ensureBrowser(
options?: EnsureBrowserOptions,
): Promise<BrowserResult> {
export async function ensureBrowser(options?: EnsureBrowserOptions): Promise<BrowserResult> {
const existing = await findBrowser();
if (existing) return existing;

const platform = detectBrowserPlatform();
if (!platform) {
throw new Error(
`Unsupported platform: ${process.platform} ${process.arch}`,
);
throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`);
}

const installed = await install({
Expand Down
26 changes: 15 additions & 11 deletions packages/cli/src/commands/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ const DEFAULT_CONFIGS: BenchmarkConfig[] = [
];

export default defineCommand({
meta: { name: "benchmark", description: "Run multiple render configurations and compare results" },
meta: {
name: "benchmark",
description: "Run multiple render configurations and compare results",
},
args: {
dir: { type: "positional", description: "Project directory", required: false },
runs: { type: "string", description: "Number of runs per config", default: "3" },
Expand Down Expand Up @@ -64,7 +67,9 @@ export default defineCommand({
producer = await loadProducer();
} catch {
if (jsonOutput) {
console.log(JSON.stringify({ error: "Producer module not available. Is the project built?" }));
console.log(
JSON.stringify({ error: "Producer module not available. Is the project built?" }),
);
} else {
errorBox(
"Producer module not available",
Expand Down Expand Up @@ -99,7 +104,10 @@ export default defineCommand({

for (let i = 0; i < runsPerConfig; i++) {
s?.message(`${config.label} — run ${i + 1}/${runsPerConfig}`);
const outputPath = join(benchDir, `${config.label.replace(/[^a-zA-Z0-9]/g, "_")}_run${i}.mp4`);
const outputPath = join(
benchDir,
`${config.label.replace(/[^a-zA-Z0-9]/g, "_")}_run${i}.mp4`,
);

try {
const startTime = Date.now();
Expand Down Expand Up @@ -176,12 +184,9 @@ export default defineCommand({
console.log(separator);

for (const result of results) {
const timeStr =
result.avgTime != null ? formatDuration(result.avgTime) : c.dim("failed");
const sizeStr =
result.avgSize != null ? formatBytes(result.avgSize) : c.dim("n/a");
const failStr =
result.failures > 0 ? c.warn(` (${result.failures} failed)`) : "";
const timeStr = result.avgTime != null ? formatDuration(result.avgTime) : c.dim("failed");
const sizeStr = result.avgSize != null ? formatBytes(result.avgSize) : c.dim("n/a");
const failStr = result.failures > 0 ? c.warn(` (${result.failures} failed)`) : "";

console.log(
" " +
Expand Down Expand Up @@ -215,8 +220,7 @@ export default defineCommand({
} else {
console.log("");
console.log(
c.error("\u2717") +
" All configurations failed. Ensure the rendering pipeline is set up.",
c.error("\u2717") + " All configurations failed. Ensure the rendering pipeline is set up.",
);
}

Expand Down
18 changes: 8 additions & 10 deletions packages/cli/src/commands/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ async function runEnsure(): Promise<void> {
s.stop("No browser found — downloading");

const downloadSpinner = clack.spinner();
downloadSpinner.start(
`Downloading Chrome Headless Shell ${c.dim("v" + CHROME_VERSION)}...`,
);
downloadSpinner.start(`Downloading Chrome Headless Shell ${c.dim("v" + CHROME_VERSION)}...`);

let lastPct = -1;
const result = await ensureBrowser({
Expand Down Expand Up @@ -66,9 +64,7 @@ async function runPath(): Promise<void> {
const ensured = await ensureBrowser();
process.stdout.write(ensured.executablePath + "\n");
} catch (err: unknown) {
console.error(
err instanceof Error ? err.message : "Failed to find browser",
);
console.error(err instanceof Error ? err.message : "Failed to find browser");
process.exit(1);
}
return;
Expand All @@ -81,9 +77,7 @@ function runClear(): void {

const removed = clearBrowser();
if (removed) {
clack.outro(
c.success("Removed cached browser from ") + c.dim(CACHE_DIR),
);
clack.outro(c.success("Removed cached browser from ") + c.dim(CACHE_DIR));
} else {
clack.outro(c.dim("No cached browser to remove."));
}
Expand All @@ -92,7 +86,11 @@ function runClear(): void {
export default defineCommand({
meta: { name: "browser", description: "Manage the Chrome browser used for rendering" },
args: {
subcommand: { type: "positional", description: "Subcommand: ensure, path, clear", required: false },
subcommand: {
type: "positional",
description: "Subcommand: ensure, path, clear",
required: false,
},
},
async run({ args }) {
const subcommand = args.subcommand;
Expand Down
15 changes: 6 additions & 9 deletions packages/cli/src/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { defineCommand } from "citty";
import { spawn } from "node:child_process";
import {
existsSync,
lstatSync,
symlinkSync,
unlinkSync,
readlinkSync,
mkdirSync,
} from "node:fs";
import { existsSync, lstatSync, symlinkSync, unlinkSync, readlinkSync, mkdirSync } from "node:fs";
import { resolve, dirname, basename, join } from "node:path";
import { fileURLToPath } from "node:url";
import * as clack from "@clack/prompts";
Expand Down Expand Up @@ -181,6 +174,10 @@ async function runDevMode(dir: string): Promise<void> {
* TODO: Migrate to use @hyperframes/studio's built-in Vite server for published CLI.
*/
async function runEmbeddedMode(_dir: string, _port: number): Promise<void> {
console.error(c.error("Embedded mode not yet available. Run from the monorepo root with: hyperframes dev <dir>"));
console.error(
c.error(
"Embedded mode not yet available. Run from the monorepo root with: hyperframes dev <dir>",
),
);
process.exit(1);
}
9 changes: 5 additions & 4 deletions packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ function checkFFmpeg(): CheckResult {
const path = findFFmpeg();
if (path) {
try {
const version = execSync("ffmpeg -version", { encoding: "utf-8", timeout: 5000 })
.split("\n")[0] ?? "";
const version =
execSync("ffmpeg -version", { encoding: "utf-8", timeout: 5000 }).split("\n")[0] ?? "";
return { ok: true, detail: version.trim() };
} catch {
return { ok: true, detail: path };
Expand Down Expand Up @@ -88,7 +88,6 @@ function checkNode(): CheckResult {
return { ok: true, detail: `${process.version} (${process.platform} ${process.arch})` };
}


export default defineCommand({
meta: { name: "doctor", description: "Check system dependencies and environment" },
args: {},
Expand All @@ -112,7 +111,9 @@ export default defineCommand({
const result = await check.run();
const icon = result.ok ? c.success("\u2713") : c.error("\u2717");
const name = check.name.padEnd(16);
console.log(` ${icon} ${c.bold(name)} ${result.ok ? c.dim(result.detail) : c.error(result.detail)}`);
console.log(
` ${icon} ${c.bold(name)} ${result.ok ? c.dim(result.detail) : c.error(result.detail)}`,
);
if (!result.ok && result.hint) {
console.log(` ${" ".repeat(19)}${c.accent(result.hint)}`);
}
Expand Down
31 changes: 18 additions & 13 deletions packages/cli/src/commands/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ export default defineCommand({
(max, el) => Math.max(max, el.startTime + el.duration),
0,
);
const resolution =
parsed.resolution === "portrait" ? "1080x1920" : "1920x1080";
const resolution = parsed.resolution === "portrait" ? "1080x1920" : "1920x1080";
const size = totalSize(project.dir);

const typeCounts: Record<string, number> = {};
Expand All @@ -51,17 +50,23 @@ export default defineCommand({
.join(", ");

if (args.json) {
console.log(JSON.stringify({
name: project.name,
resolution: parsed.resolution,
width: parsed.resolution === "portrait" ? 1080 : 1920,
height: parsed.resolution === "portrait" ? 1920 : 1080,
duration: maxEnd,
elements: parsed.elements.length,
tracks: tracks.size,
types: typeCounts,
size,
}, null, 2));
console.log(
JSON.stringify(
{
name: project.name,
resolution: parsed.resolution,
width: parsed.resolution === "portrait" ? 1080 : 1920,
height: parsed.resolution === "portrait" ? 1920 : 1080,
duration: maxEnd,
elements: parsed.elements.length,
tracks: tracks.size,
types: typeCounts,
size,
},
null,
2,
),
);
return;
}

Expand Down
Loading
Loading