mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 13:58:20 +00:00
Merge pull request #98 from heygen-com/feat/cli-smart-worker-default
feat(cli): smart default worker count based on CPU cores
This commit is contained in:
@@ -116,11 +116,57 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4 with the [
|
||||
| `--output` | path | `renders/<name>.mp4` | Output file path |
|
||||
| `--fps` | 24, 30, 60 | 30 | Frames per second |
|
||||
| `--quality` | draft, standard, high | standard | Encoding quality preset |
|
||||
| `--workers` | 1-8 | 4 | Parallel render workers |
|
||||
| `--workers` | 1-8 or `auto` | auto | Parallel render workers (see [Workers](#workers) below) |
|
||||
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, VAAPI) |
|
||||
| `--docker` | — | off | Use Docker for [deterministic rendering](/concepts/determinism) |
|
||||
| `--quiet` | — | off | Suppress verbose output |
|
||||
|
||||
## Workers
|
||||
|
||||
Each render worker launches a **separate Chrome browser process** to capture frames in parallel. More workers can speed up rendering, but each one consumes ~256 MB of RAM and significant CPU.
|
||||
|
||||
### Default behavior
|
||||
|
||||
By default, Hyperframes uses **half of your CPU cores, capped at 4**:
|
||||
|
||||
| Machine | CPU cores | Default workers |
|
||||
|---------|-----------|----------------|
|
||||
| MacBook Air (M1) | 8 | 4 |
|
||||
| MacBook Pro (M3) | 12 | 4 (capped) |
|
||||
| 4-core laptop | 4 | 2 |
|
||||
| 2-core VM | 2 | 1 |
|
||||
|
||||
This is intentionally conservative. Each worker spawns its own Chrome process, so the per-worker overhead is significant. Fewer workers avoids resource contention with FFmpeg encoding and your other applications.
|
||||
|
||||
### Choosing a worker count
|
||||
|
||||
```bash Terminal
|
||||
# Explicit worker count
|
||||
npx hyperframes render --workers 1 --output output.mp4
|
||||
|
||||
# Let Hyperframes pick based on your CPU
|
||||
npx hyperframes render --workers auto --output output.mp4
|
||||
|
||||
# Maximum parallelism (use with caution on laptops)
|
||||
npx hyperframes render --workers 8 --output output.mp4
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Start with the default. If renders feel slow and your system has headroom (check Activity Monitor / `htop`), try increasing `--workers`. If you see high memory pressure or fan noise, reduce it.
|
||||
</Tip>
|
||||
|
||||
### When to use 1 worker
|
||||
|
||||
- Short compositions (under 2 seconds / 60 frames) — parallelism overhead exceeds the benefit
|
||||
- Low-memory machines (4 GB or less)
|
||||
- Running renders alongside other heavy processes (video editing, large builds)
|
||||
|
||||
### When to increase workers
|
||||
|
||||
- Long compositions (30+ seconds) on a machine with 8+ cores and 16+ GB RAM
|
||||
- Dedicated render machines or CI runners
|
||||
- Docker mode on a well-provisioned host
|
||||
|
||||
## Tips
|
||||
|
||||
<Tip>
|
||||
@@ -128,7 +174,6 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4 with the [
|
||||
</Tip>
|
||||
|
||||
- Use `npx hyperframes benchmark` to find optimal settings for your system
|
||||
- 4 workers is usually the sweet spot for most compositions
|
||||
- Docker mode is slower but guarantees [identical output](/concepts/determinism) across platforms
|
||||
- For compositions with many frames, `--gpu` can significantly speed up local encoding
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineCommand } from "citty";
|
||||
import { existsSync, mkdirSync, statSync } from "node:fs";
|
||||
import { cpus } from "node:os";
|
||||
import { resolve, dirname, join } from "node:path";
|
||||
import { resolveProject } from "../utils/project.js";
|
||||
import { loadProducer } from "../utils/producer.js";
|
||||
@@ -12,6 +13,13 @@ const VALID_FPS = new Set([24, 30, 60]);
|
||||
const VALID_QUALITY = new Set(["draft", "standard", "high"]);
|
||||
const VALID_FORMAT = new Set(["mp4", "webm"]);
|
||||
|
||||
const CPU_CORE_COUNT = cpus().length;
|
||||
|
||||
/** Half of CPU cores, capped at 4. Each worker spawns a Chrome process (~256 MB). */
|
||||
function defaultWorkerCount(): number {
|
||||
return Math.max(1, Math.min(Math.floor(CPU_CORE_COUNT / 2), 4));
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
name: "render",
|
||||
@@ -24,19 +32,47 @@ Examples:
|
||||
hyperframes render --docker --output deterministic.mp4`,
|
||||
},
|
||||
args: {
|
||||
dir: { type: "positional", description: "Project directory", required: false },
|
||||
output: { type: "string", description: "Output path (default: renders/<name>.mp4)" },
|
||||
fps: { type: "string", description: "Frame rate: 24, 30, 60", default: "30" },
|
||||
quality: { type: "string", description: "Quality: draft, standard, high", default: "standard" },
|
||||
dir: {
|
||||
type: "positional",
|
||||
description: "Project directory",
|
||||
required: false,
|
||||
},
|
||||
output: {
|
||||
type: "string",
|
||||
description: "Output path (default: renders/<name>.mp4)",
|
||||
},
|
||||
fps: {
|
||||
type: "string",
|
||||
description: "Frame rate: 24, 30, 60",
|
||||
default: "30",
|
||||
},
|
||||
quality: {
|
||||
type: "string",
|
||||
description: "Quality: draft, standard, high",
|
||||
default: "standard",
|
||||
},
|
||||
format: {
|
||||
type: "string",
|
||||
description: "Output format: mp4, webm (WebM renders with transparency)",
|
||||
default: "mp4",
|
||||
},
|
||||
workers: { type: "string", description: "Parallel workers 1-8" },
|
||||
docker: { type: "boolean", description: "Use Docker for deterministic render", default: false },
|
||||
workers: {
|
||||
type: "string",
|
||||
description:
|
||||
"Parallel render workers (1-8 or 'auto'). Default: half your CPU cores, max 4. " +
|
||||
"Each worker launches a separate Chrome process.",
|
||||
},
|
||||
docker: {
|
||||
type: "boolean",
|
||||
description: "Use Docker for deterministic render",
|
||||
default: false,
|
||||
},
|
||||
gpu: { type: "boolean", description: "Use GPU encoding", default: false },
|
||||
quiet: { type: "boolean", description: "Suppress verbose output", default: false },
|
||||
quiet: {
|
||||
type: "boolean",
|
||||
description: "Suppress verbose output",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
async run({ args }) {
|
||||
// ── Resolve project ────────────────────────────────────────────────────
|
||||
@@ -68,10 +104,10 @@ Examples:
|
||||
|
||||
// ── Validate workers ──────────────────────────────────────────────────
|
||||
let workers: number | undefined;
|
||||
if (args.workers != null) {
|
||||
if (args.workers != null && args.workers !== "auto") {
|
||||
const parsed = parseInt(args.workers, 10);
|
||||
if (isNaN(parsed) || parsed < 1 || parsed > 8) {
|
||||
errorBox("Invalid workers", `Got "${args.workers}". Must be between 1 and 8.`);
|
||||
errorBox("Invalid workers", `Got "${args.workers}". Must be 1-8 or "auto".`);
|
||||
process.exit(1);
|
||||
}
|
||||
workers = parsed;
|
||||
@@ -85,18 +121,19 @@ Examples:
|
||||
: join(rendersDir, `${project.name}${ext}`);
|
||||
|
||||
// Ensure output directory exists
|
||||
const outputDir = dirname(outputPath);
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
mkdirSync(dirname(outputPath), { recursive: true });
|
||||
|
||||
const useDocker = args.docker ?? false;
|
||||
const useGpu = args.gpu ?? false;
|
||||
const quiet = args.quiet ?? false;
|
||||
|
||||
// ── Print render plan ─────────────────────────────────────────────────
|
||||
const workerCount = workers ?? 4;
|
||||
const workerCount = workers ?? defaultWorkerCount();
|
||||
if (!quiet) {
|
||||
const workerLabel =
|
||||
args.workers != null
|
||||
? `${workerCount} workers`
|
||||
: `${workerCount} workers (auto \u2014 half of ${CPU_CORE_COUNT} cores)`;
|
||||
console.log("");
|
||||
console.log(
|
||||
c.accent("\u25C6") +
|
||||
@@ -104,9 +141,7 @@ Examples:
|
||||
c.accent(project.name) +
|
||||
c.dim(" \u2192 " + outputPath),
|
||||
);
|
||||
console.log(
|
||||
c.dim(" " + fps + "fps \u00B7 " + quality + " \u00B7 " + workerCount + " workers"),
|
||||
);
|
||||
console.log(c.dim(" " + fps + "fps \u00B7 " + quality + " \u00B7 " + workerLabel));
|
||||
console.log("");
|
||||
}
|
||||
|
||||
@@ -159,7 +194,7 @@ Examples:
|
||||
fps,
|
||||
quality,
|
||||
format,
|
||||
workers,
|
||||
workers: workerCount,
|
||||
gpu: useGpu,
|
||||
quiet,
|
||||
});
|
||||
@@ -168,7 +203,7 @@ Examples:
|
||||
fps,
|
||||
quality,
|
||||
format,
|
||||
workers,
|
||||
workers: workerCount,
|
||||
gpu: useGpu,
|
||||
quiet,
|
||||
browserPath,
|
||||
@@ -181,7 +216,7 @@ interface RenderOptions {
|
||||
fps: 24 | 30 | 60;
|
||||
quality: "draft" | "standard" | "high";
|
||||
format: "mp4" | "webm";
|
||||
workers?: number;
|
||||
workers: number;
|
||||
gpu: boolean;
|
||||
quiet: boolean;
|
||||
browserPath?: string;
|
||||
@@ -205,7 +240,11 @@ async function renderDocker(
|
||||
});
|
||||
await producer.executeRenderJob(job, projectDir, outputPath);
|
||||
} catch (error: unknown) {
|
||||
trackRenderError({ fps: options.fps, quality: options.quality, docker: true });
|
||||
trackRenderError({
|
||||
fps: options.fps,
|
||||
quality: options.quality,
|
||||
docker: true,
|
||||
});
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
errorBox("Render failed", message, "Check Docker is running: docker info");
|
||||
process.exit(1);
|
||||
@@ -216,7 +255,7 @@ async function renderDocker(
|
||||
durationMs: elapsed,
|
||||
fps: options.fps,
|
||||
quality: options.quality,
|
||||
workers: options.workers ?? 4,
|
||||
workers: options.workers,
|
||||
docker: true,
|
||||
gpu: options.gpu,
|
||||
});
|
||||
@@ -256,7 +295,11 @@ async function renderLocal(
|
||||
try {
|
||||
await producer.executeRenderJob(job, projectDir, outputPath, onProgress);
|
||||
} catch (error: unknown) {
|
||||
trackRenderError({ fps: options.fps, quality: options.quality, docker: false });
|
||||
trackRenderError({
|
||||
fps: options.fps,
|
||||
quality: options.quality,
|
||||
docker: false,
|
||||
});
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
errorBox("Render failed", message, "Try --docker for containerized rendering");
|
||||
process.exit(1);
|
||||
@@ -267,7 +310,7 @@ async function renderLocal(
|
||||
durationMs: elapsed,
|
||||
fps: options.fps,
|
||||
quality: options.quality,
|
||||
workers: options.workers ?? 4,
|
||||
workers: options.workers,
|
||||
docker: false,
|
||||
gpu: options.gpu,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user