mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
initial code (#2)
* feat: initial code port from hyperframes-internal Port all OSS-ready packages from the internal monorepo: - @hyperframes/core — shared types, HTML generation, GSAP utilities, runtime - @hyperframes/cli — CLI for creating, previewing, and rendering compositions - @hyperframes/engine — framework-agnostic rendering engine (BeginFrame + FFmpeg) - @hyperframes/producer — video rendering pipeline (Puppeteer + FFmpeg) - @hyperframes/ui-player — browser-based video player component - @hyperframes/studio — composition editor (React frontend + Hono backend) Includes regression test suite with Docker-based test harness. All HeyGen-internal references, deployment infrastructure, and proprietary assets have been removed. Package names migrated from @app/* to @hyperframes/*. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: scrub internal codenames and stale references from OSS port - Replace static.heygen.ai runtime URLs in test fixtures - Remove internal CDN publish script (publish-hyperframe-runtime.ts) - Replace sandbox-studio, sandbox-interceptor, __magicEditRuntime with neutral names (studio, hyperframe-runtime, __hyperframeRuntime) - Fix stale Vault API / localhost references in docs - Remove broken deprecated_studio link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove remaining internal codenames and stale references - Delete stale producer README.md and PIPELINE.md (referenced nonexistent files) - Replace "Cerberus" codename with "HyperFrames" in test design reviews - Replace magic-edit postMessage identifiers with hf-preview/hf-parent - Rename debug-magic-edit-timeline.ts to debug-timeline.ts - Replace "Motion Cut" with "HyperFrames" in Timeline comments - Fix studio/CLI references to nonexistent archive package (use local data/projects/ dir, stub render proxy) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
10621e7903
commit
9f8e5ba5a1
@@ -0,0 +1,226 @@
|
||||
import { defineCommand } from "citty";
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import { resolve, join } from "node:path";
|
||||
import { resolveProject } from "../utils/project.js";
|
||||
import { loadProducer } from "../utils/producer.js";
|
||||
import { c } from "../ui/colors.js";
|
||||
import { formatBytes, formatDuration, errorBox } from "../ui/format.js";
|
||||
import * as clack from "@clack/prompts";
|
||||
|
||||
interface BenchmarkConfig {
|
||||
label: string;
|
||||
fps: 24 | 30 | 60;
|
||||
quality: "draft" | "standard" | "high";
|
||||
workers: number;
|
||||
}
|
||||
|
||||
interface RunResult {
|
||||
elapsedMs: number;
|
||||
fileSize: number | null;
|
||||
}
|
||||
|
||||
interface ConfigResult {
|
||||
config: BenchmarkConfig;
|
||||
runs: RunResult[];
|
||||
failures: number;
|
||||
avgTime: number | null;
|
||||
avgSize: number | null;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIGS: BenchmarkConfig[] = [
|
||||
{ label: "30fps \u00B7 draft \u00B7 2w", fps: 30, quality: "draft", workers: 2 },
|
||||
{ label: "30fps \u00B7 standard \u00B7 2w", fps: 30, quality: "standard", workers: 2 },
|
||||
{ label: "30fps \u00B7 high \u00B7 2w", fps: 30, quality: "high", workers: 2 },
|
||||
{ label: "30fps \u00B7 standard \u00B7 4w", fps: 30, quality: "standard", workers: 4 },
|
||||
{ label: "60fps \u00B7 standard \u00B7 4w", fps: 60, quality: "standard", workers: 4 },
|
||||
];
|
||||
|
||||
export default defineCommand({
|
||||
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" },
|
||||
json: { type: "boolean", description: "Output results as JSON", default: false },
|
||||
},
|
||||
async run({ args }) {
|
||||
// ── Resolve project ──────────────────────────────────────────────────
|
||||
const project = resolveProject(args.dir);
|
||||
|
||||
// ── Parse runs ───────────────────────────────────────────────────────
|
||||
const runsPerConfig = parseInt(args.runs ?? "3", 10);
|
||||
if (isNaN(runsPerConfig) || runsPerConfig < 1 || runsPerConfig > 20) {
|
||||
errorBox("Invalid runs", `Got "${args.runs ?? "3"}". Must be between 1 and 20.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const jsonOutput = args.json ?? false;
|
||||
|
||||
// ── Temp output for benchmark renders ────────────────────────────────
|
||||
const benchDir = resolve("renders", ".benchmark");
|
||||
|
||||
// ── Load producer ────────────────────────────────────────────────────
|
||||
let producer: Awaited<ReturnType<typeof loadProducer>> | null = null;
|
||||
try {
|
||||
producer = await loadProducer();
|
||||
} catch {
|
||||
if (jsonOutput) {
|
||||
console.log(JSON.stringify({ error: "Producer module not available. Is the project built?" }));
|
||||
} else {
|
||||
errorBox(
|
||||
"Producer module not available",
|
||||
"The rendering pipeline could not be loaded.",
|
||||
"Ensure @hyperframes/producer is built and linked.",
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Print header ─────────────────────────────────────────────────────
|
||||
if (!jsonOutput) {
|
||||
console.log("");
|
||||
console.log(
|
||||
c.accent("\u25C6") +
|
||||
" Benchmarking " +
|
||||
c.accent(project.name) +
|
||||
c.dim(` (${runsPerConfig} runs each)`),
|
||||
);
|
||||
console.log("");
|
||||
}
|
||||
|
||||
// ── Run benchmarks ───────────────────────────────────────────────────
|
||||
const results: ConfigResult[] = [];
|
||||
|
||||
for (const config of DEFAULT_CONFIGS) {
|
||||
const runs: RunResult[] = [];
|
||||
let failures = 0;
|
||||
|
||||
const s = !jsonOutput ? clack.spinner() : undefined;
|
||||
s?.start(`Benchmarking ${config.label}...`);
|
||||
|
||||
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`);
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
await producer.renderComposition(project.dir, {
|
||||
output: outputPath,
|
||||
fps: config.fps,
|
||||
quality: config.quality,
|
||||
workers: config.workers,
|
||||
quiet: true,
|
||||
});
|
||||
const elapsedMs = Date.now() - startTime;
|
||||
|
||||
let fileSize: number | null = null;
|
||||
if (existsSync(outputPath)) {
|
||||
const stat = statSync(outputPath);
|
||||
fileSize = stat.size;
|
||||
}
|
||||
|
||||
runs.push({ elapsedMs, fileSize });
|
||||
} catch {
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
s?.stop(`${config.label} — ${runs.length} runs${failures > 0 ? `, ${failures} failed` : ""}`);
|
||||
|
||||
const successfulRuns = runs.filter((r) => r.elapsedMs > 0);
|
||||
const avgTime =
|
||||
successfulRuns.length > 0
|
||||
? successfulRuns.reduce((sum, r) => sum + r.elapsedMs, 0) / successfulRuns.length
|
||||
: null;
|
||||
const sizesWithValues = runs.map((r) => r.fileSize).filter((s): s is number => s != null);
|
||||
const avgSize =
|
||||
sizesWithValues.length > 0
|
||||
? sizesWithValues.reduce((sum, s) => sum + s, 0) / sizesWithValues.length
|
||||
: null;
|
||||
|
||||
results.push({ config, runs, failures, avgTime, avgSize });
|
||||
}
|
||||
|
||||
// ── Output results ───────────────────────────────────────────────────
|
||||
if (jsonOutput) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
results.map((r) => ({
|
||||
config: r.config.label,
|
||||
fps: r.config.fps,
|
||||
quality: r.config.quality,
|
||||
workers: r.config.workers,
|
||||
avgTimeMs: r.avgTime,
|
||||
avgSizeBytes: r.avgSize,
|
||||
failures: r.failures,
|
||||
runs: r.runs,
|
||||
})),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Table output ─────────────────────────────────────────────────────
|
||||
const configColWidth = 26;
|
||||
const timeColWidth = 10;
|
||||
const sizeColWidth = 10;
|
||||
|
||||
const header =
|
||||
" " +
|
||||
c.bold("Config".padEnd(configColWidth)) +
|
||||
c.bold("Time".padEnd(timeColWidth)) +
|
||||
c.bold("Size".padEnd(sizeColWidth));
|
||||
const separator = " " + c.dim("\u2500".repeat(configColWidth + timeColWidth + sizeColWidth));
|
||||
|
||||
console.log(header);
|
||||
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)`) : "";
|
||||
|
||||
console.log(
|
||||
" " +
|
||||
result.config.label.padEnd(configColWidth) +
|
||||
timeStr.padEnd(timeColWidth) +
|
||||
sizeStr.padEnd(sizeColWidth) +
|
||||
failStr,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Summary ──────────────────────────────────────────────────────────
|
||||
const successfulResults = results.filter((r) => r.avgTime != null);
|
||||
if (successfulResults.length > 0) {
|
||||
let fastest = successfulResults[0];
|
||||
for (const r of successfulResults) {
|
||||
if (fastest == null || r.avgTime == null) continue;
|
||||
if (fastest.avgTime == null || r.avgTime < fastest.avgTime) {
|
||||
fastest = r;
|
||||
}
|
||||
}
|
||||
|
||||
if (fastest?.avgTime != null) {
|
||||
console.log("");
|
||||
console.log(
|
||||
c.success("\u25C7") +
|
||||
" Fastest: " +
|
||||
c.accent(fastest.config.label) +
|
||||
c.dim(` (${formatDuration(fastest.avgTime)})`),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log("");
|
||||
console.log(
|
||||
c.error("\u2717") +
|
||||
" All configurations failed. Ensure the rendering pipeline is set up.",
|
||||
);
|
||||
}
|
||||
|
||||
console.log("");
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user