mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-08-31 02:41:44 +00:00
Merge a65ed53e65 into 3337cc8990
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
name: macOS treated-media real-pixel gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "scripts/issue3329-macos-real-pixel.mjs"
|
||||
- ".github/workflows/issue3329-macos-real-pixel.yml"
|
||||
- "packages/core/src/runtime/colorGrading.ts"
|
||||
- "packages/engine/src/services/frameCapture.ts"
|
||||
- "packages/engine/src/services/screenshotService.ts"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
macos-real-pixels:
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
lfs: true
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install FFmpeg
|
||||
run: brew install ffmpeg
|
||||
- name: Install dependencies
|
||||
run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
- name: Build local CLI render stack
|
||||
run: |
|
||||
bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
|
||||
bun run --cwd packages/core build
|
||||
bun run --filter '@hyperframes/{engine,producer,studio}' build
|
||||
bun run --filter @hyperframes/cli build
|
||||
- name: Run real-pixel boundary matrix
|
||||
env:
|
||||
HF_ISSUE3329_ARTIFACT_DIR: ${{ github.workspace }}/.artifacts/issue3329-macos
|
||||
run: node scripts/issue3329-macos-real-pixel.mjs
|
||||
- name: Upload boundary evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: issue3329-macos-real-pixels-${{ runner.arch }}
|
||||
path: .artifacts/issue3329-macos
|
||||
if-no-files-found: error
|
||||
include-hidden-files: true
|
||||
@@ -0,0 +1,55 @@
|
||||
import { existsSync, realpathSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { basename, dirname, isAbsolute, parse, relative, resolve, sep } from "node:path";
|
||||
|
||||
function rejectTraversal(raw) {
|
||||
if (raw.split(/[\\/]+/).includes("..")) {
|
||||
throw new Error("Artifact root must not contain traversal segments");
|
||||
}
|
||||
}
|
||||
|
||||
function isForbiddenRoot(target, repo, artifactSpace) {
|
||||
return new Set([parse(target).root, resolve(homedir()), repo, artifactSpace]).has(target);
|
||||
}
|
||||
|
||||
function isDedicatedDescendant(target, artifactSpace) {
|
||||
const rel = relative(artifactSpace, target);
|
||||
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
||||
}
|
||||
|
||||
function canonicalizeTarget(target) {
|
||||
const suffix = [];
|
||||
let ancestor = target;
|
||||
while (!existsSync(ancestor)) {
|
||||
const parent = dirname(ancestor);
|
||||
if (parent === ancestor) {
|
||||
throw new Error("Artifact root has no canonical ancestor");
|
||||
}
|
||||
suffix.unshift(basename(ancestor));
|
||||
ancestor = parent;
|
||||
}
|
||||
return resolve(realpathSync(ancestor), ...suffix);
|
||||
}
|
||||
|
||||
function rejectUnsafeTarget(target, repo, artifactSpace) {
|
||||
if (
|
||||
isForbiddenRoot(target, repo, artifactSpace) ||
|
||||
!isDedicatedDescendant(artifactSpace, repo) ||
|
||||
!isDedicatedDescendant(target, artifactSpace)
|
||||
) {
|
||||
throw new Error(
|
||||
"Artifact root must be a dedicated descendant of the repository artifact space",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateArtifactRoot(requestedPath, repoRoot) {
|
||||
const raw = String(requestedPath ?? "");
|
||||
rejectTraversal(raw);
|
||||
const repo = realpathSync(resolve(repoRoot));
|
||||
const artifactSpace = canonicalizeTarget(resolve(repo, ".artifacts"));
|
||||
const requestedTarget = resolve(repo, raw);
|
||||
const target = canonicalizeTarget(requestedTarget);
|
||||
rejectUnsafeTarget(target, repo, artifactSpace);
|
||||
return requestedTarget;
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { copyFileSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { validateArtifactRoot } from "./issue3329-artifact-safety.mjs";
|
||||
|
||||
const repo = process.cwd();
|
||||
const artifactRoot = validateArtifactRoot(
|
||||
process.env.HF_ISSUE3329_ARTIFACT_DIR ?? join(".artifacts", "issue3329-macos"),
|
||||
repo,
|
||||
);
|
||||
const projectsRoot = join(artifactRoot, "projects");
|
||||
const outputsRoot = join(artifactRoot, "outputs");
|
||||
rmSync(artifactRoot, { recursive: true, force: true });
|
||||
mkdirSync(projectsRoot, { recursive: true });
|
||||
mkdirSync(outputsRoot, { recursive: true });
|
||||
|
||||
function run(binary, args, options = {}) {
|
||||
const result = spawnSync(binary, args, {
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
timeout: 180_000,
|
||||
...options,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`${binary} ${args.join(" ")} failed (${result.status})\n${result.stdout}\n${result.stderr}`,
|
||||
);
|
||||
}
|
||||
return { stdout: result.stdout.trim(), stderr: result.stderr.trim() };
|
||||
}
|
||||
|
||||
function ffmpeg(args) {
|
||||
return run("ffmpeg", ["-hide_banner", "-loglevel", "error", ...args]);
|
||||
}
|
||||
|
||||
function cli(args) {
|
||||
return run("bun", ["packages/cli/src/cli.ts", ...args], {
|
||||
env: { ...process.env, HYPERFRAMES_NO_TELEMETRY: "1" },
|
||||
});
|
||||
}
|
||||
|
||||
function sha256(path) {
|
||||
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
||||
}
|
||||
|
||||
function firstFile(dir, suffix) {
|
||||
const name = readdirSync(dir).find((entry) => entry.endsWith(suffix));
|
||||
if (!name) throw new Error(`No ${suffix} output under ${dir}`);
|
||||
return join(dir, name);
|
||||
}
|
||||
|
||||
function pixelStats(path) {
|
||||
const result = spawnSync(
|
||||
"ffmpeg",
|
||||
[
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-i",
|
||||
path,
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
"scale=160:90",
|
||||
"-pix_fmt",
|
||||
"rgba",
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-",
|
||||
],
|
||||
{ encoding: null },
|
||||
);
|
||||
if (result.status !== 0) throw new Error(`Pixel decode failed for ${path}`);
|
||||
const pixels = result.stdout;
|
||||
let sum = 0;
|
||||
let sumSq = 0;
|
||||
let alphaMin = 255;
|
||||
let alphaMax = 0;
|
||||
const count = pixels.length / 4;
|
||||
for (let offset = 0; offset + 3 < pixels.length; offset += 4) {
|
||||
const luma =
|
||||
0.2126 * pixels[offset] + 0.7152 * pixels[offset + 1] + 0.0722 * pixels[offset + 2];
|
||||
sum += luma;
|
||||
sumSq += luma * luma;
|
||||
alphaMin = Math.min(alphaMin, pixels[offset + 3]);
|
||||
alphaMax = Math.max(alphaMax, pixels[offset + 3]);
|
||||
}
|
||||
const mean = sum / count;
|
||||
const variance = Math.max(0, sumSq / count - mean * mean);
|
||||
const stddev = Math.sqrt(variance);
|
||||
return {
|
||||
mean: Number(mean.toFixed(3)),
|
||||
stddev: Number(stddev.toFixed(3)),
|
||||
alphaMin,
|
||||
alphaMax,
|
||||
solid: stddev < 2,
|
||||
};
|
||||
}
|
||||
|
||||
function ffprobe(path) {
|
||||
const output = execFileSync(
|
||||
"ffprobe",
|
||||
[
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"stream=codec_name,codec_tag_string,pix_fmt,color_space,color_transfer,color_primaries",
|
||||
"-of",
|
||||
"json",
|
||||
"--",
|
||||
path,
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
return JSON.parse(output).streams?.[0] ?? {};
|
||||
}
|
||||
|
||||
const sourceDir = join(artifactRoot, "source");
|
||||
mkdirSync(sourceDir, { recursive: true });
|
||||
const framePath = join(sourceDir, "frame.png");
|
||||
const videoPath = join(sourceDir, "frame.mp4");
|
||||
ffmpeg(["-f", "lavfi", "-i", "testsrc2=size=160x90:rate=1", "-frames:v", "1", "-y", framePath]);
|
||||
ffmpeg([
|
||||
"-loop",
|
||||
"1",
|
||||
"-i",
|
||||
framePath,
|
||||
"-t",
|
||||
"1",
|
||||
"-r",
|
||||
"30",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-y",
|
||||
videoPath,
|
||||
]);
|
||||
|
||||
const gradings = {
|
||||
ungraded: null,
|
||||
adjustment: { version: 2, colorSpace: "rec709", adjust: { saturation: -0.3 } },
|
||||
kuwahara: { version: 2, colorSpace: "rec709", effects: { kuwahara: 0.8 } },
|
||||
};
|
||||
const ledger = {
|
||||
environment: {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
runnerOs: process.env.RUNNER_OS ?? null,
|
||||
uname: run("uname", ["-a"]).stdout,
|
||||
osVersion: process.platform === "darwin" ? run("sw_vers", []).stdout : null,
|
||||
ffmpeg: run("ffmpeg", ["-version"]).stdout.split("\n")[0],
|
||||
node: process.version,
|
||||
bun: run("bun", ["--version"]).stdout,
|
||||
eligibleMacArm64: process.platform === "darwin" && process.arch === "arm64",
|
||||
},
|
||||
source: { frame: sha256(framePath), video: sha256(videoPath) },
|
||||
rows: [],
|
||||
};
|
||||
|
||||
const requestedCases = new Set(
|
||||
(process.env.HF_ISSUE3329_CASES ?? "")
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
for (const mediaKind of ["img", "video"]) {
|
||||
for (const [gradingName, grading] of Object.entries(gradings)) {
|
||||
const caseName = `${mediaKind}-${gradingName}`;
|
||||
if (requestedCases.size > 0 && !requestedCases.has(caseName)) continue;
|
||||
const projectDir = join(projectsRoot, caseName);
|
||||
mkdirSync(projectDir, { recursive: true });
|
||||
const sourceName = mediaKind === "img" ? "frame.png" : "frame.mp4";
|
||||
copyFileSync(mediaKind === "img" ? framePath : videoPath, join(projectDir, sourceName));
|
||||
const gradingAttr = grading ? ` data-color-grading='${JSON.stringify(grading)}'` : "";
|
||||
const media =
|
||||
mediaKind === "img"
|
||||
? `<img id="media" src="${sourceName}"${gradingAttr} style="position:absolute;inset:0;width:160px;height:90px;object-fit:fill">`
|
||||
: `<video id="media" src="${sourceName}" data-start="0" data-duration="1" muted playsinline${gradingAttr} style="position:absolute;inset:0;width:160px;height:90px;object-fit:fill"></video>`;
|
||||
writeFileSync(
|
||||
join(projectDir, "index.html"),
|
||||
`<!doctype html><html><body style="margin:0;background:#123"><div id="root" data-composition-id="${caseName}" data-root="true" data-start="0" data-duration="1" data-width="160" data-height="90" data-fps="1">${media}</div></body></html>`,
|
||||
);
|
||||
|
||||
const caseOut = join(outputsRoot, caseName);
|
||||
const runtimeDir = join(caseOut, "runtime");
|
||||
const rawDir = join(caseOut, "raw");
|
||||
mkdirSync(runtimeDir, { recursive: true });
|
||||
cli([
|
||||
"snapshot",
|
||||
projectDir,
|
||||
"--at",
|
||||
"0.5",
|
||||
"--no-end",
|
||||
"--output",
|
||||
runtimeDir,
|
||||
"--describe",
|
||||
"false",
|
||||
]);
|
||||
cli([
|
||||
"render",
|
||||
projectDir,
|
||||
"--format",
|
||||
"png-sequence",
|
||||
"--output",
|
||||
rawDir,
|
||||
"--fps",
|
||||
"1",
|
||||
"--workers",
|
||||
"1",
|
||||
"--quiet",
|
||||
]);
|
||||
const h264Path = join(caseOut, "h264.mp4");
|
||||
const proresPath = join(caseOut, "prores.mov");
|
||||
cli([
|
||||
"render",
|
||||
projectDir,
|
||||
"--format",
|
||||
"mp4",
|
||||
"--output",
|
||||
h264Path,
|
||||
"--fps",
|
||||
"1",
|
||||
"--workers",
|
||||
"1",
|
||||
"--quiet",
|
||||
]);
|
||||
cli([
|
||||
"render",
|
||||
projectDir,
|
||||
"--format",
|
||||
"mov",
|
||||
"--output",
|
||||
proresPath,
|
||||
"--fps",
|
||||
"1",
|
||||
"--workers",
|
||||
"1",
|
||||
"--quiet",
|
||||
]);
|
||||
const runtimePath = firstFile(runtimeDir, ".png");
|
||||
const rawPath = firstFile(rawDir, ".png");
|
||||
const hevcPath = join(caseOut, "hevc.mp4");
|
||||
ffmpeg([
|
||||
"-loop",
|
||||
"1",
|
||||
"-i",
|
||||
rawPath,
|
||||
"-t",
|
||||
"1",
|
||||
"-r",
|
||||
"1",
|
||||
"-c:v",
|
||||
"libx265",
|
||||
"-tag:v",
|
||||
"hvc1",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-y",
|
||||
hevcPath,
|
||||
]);
|
||||
|
||||
const boundaries = {};
|
||||
for (const [name, path] of Object.entries({
|
||||
runtime: runtimePath,
|
||||
raw: rawPath,
|
||||
h264: h264Path,
|
||||
prores: proresPath,
|
||||
hevc: hevcPath,
|
||||
})) {
|
||||
boundaries[name] = {
|
||||
path,
|
||||
sha256: sha256(path),
|
||||
bytes: readFileSync(path).length,
|
||||
pixels: pixelStats(path),
|
||||
...(name === "h264" || name === "prores" || name === "hevc"
|
||||
? { stream: ffprobe(path) }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
const firstCorrupt = ["runtime", "raw", "h264", "prores", "hevc"].find(
|
||||
(name) => boundaries[name].pixels.solid,
|
||||
);
|
||||
ledger.rows.push({
|
||||
caseName,
|
||||
mediaKind,
|
||||
gradingName,
|
||||
firstCorrupt: firstCorrupt ?? null,
|
||||
boundaries,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const controls = new Map(
|
||||
ledger.rows.filter((row) => row.gradingName === "ungraded").map((row) => [row.mediaKind, row]),
|
||||
);
|
||||
const infrastructureFailures = ledger.rows
|
||||
.filter((row) => row.gradingName === "ungraded")
|
||||
.flatMap((row) =>
|
||||
Object.entries(row.boundaries)
|
||||
.filter(([, value]) => value.pixels.solid)
|
||||
.map(([boundary]) => `${row.caseName}@${boundary}`),
|
||||
);
|
||||
const productRed = ledger.rows
|
||||
.filter((row) => row.gradingName !== "ungraded")
|
||||
.flatMap((row) => {
|
||||
const control = controls.get(row.mediaKind);
|
||||
if (!control) return [];
|
||||
return Object.entries(row.boundaries)
|
||||
.filter(
|
||||
([boundary, value]) =>
|
||||
value.pixels.solid && control.boundaries[boundary]?.pixels.solid === false,
|
||||
)
|
||||
.map(([boundary]) => `${row.caseName}@${boundary}`);
|
||||
});
|
||||
ledger.classification = !ledger.environment.eligibleMacArm64
|
||||
? "ineligible_environment"
|
||||
: infrastructureFailures.length > 0
|
||||
? "fixture_or_capture_infrastructure_failure"
|
||||
: productRed.length > 0
|
||||
? "product_red"
|
||||
: "no_reproduction";
|
||||
ledger.infrastructureFailures = infrastructureFailures;
|
||||
ledger.productRed = productRed;
|
||||
writeFileSync(join(artifactRoot, "ledger.json"), `${JSON.stringify(ledger, null, 2)}\n`);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
artifactRoot,
|
||||
environment: ledger.environment,
|
||||
rows: ledger.rows.map(({ caseName, firstCorrupt, boundaries }) => ({
|
||||
caseName,
|
||||
firstCorrupt,
|
||||
stddev: Object.fromEntries(
|
||||
Object.entries(boundaries).map(([name, value]) => [name, value.pixels.stddev]),
|
||||
),
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
if (!ledger.environment.eligibleMacArm64) {
|
||||
throw new Error(
|
||||
`INELIGIBLE_ENVIRONMENT: expected darwin/arm64, got ${process.platform}/${process.arch}`,
|
||||
);
|
||||
}
|
||||
if (infrastructureFailures.length > 0) {
|
||||
throw new Error(
|
||||
`INFRASTRUCTURE_FAILURE: ungraded controls are solid at ${infrastructureFailures.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (productRed.length > 0) {
|
||||
throw new Error(
|
||||
`PRODUCT_RED: graded output is solid while its matching control is non-solid at ${productRed.join(", ")}`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
} from "node:fs";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { dirname, join, parse, resolve } from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repo = realpathSync(resolve(scriptsDir, ".."));
|
||||
|
||||
let safetyModule = null;
|
||||
try {
|
||||
safetyModule = await import("./issue3329-artifact-safety.mjs");
|
||||
} catch {
|
||||
// RED: the safety owner does not exist yet.
|
||||
}
|
||||
|
||||
test("cleanup accepts only a dedicated descendant of the repo artifact space", () => {
|
||||
assert.ok(safetyModule, "artifact cleanup safety module must exist");
|
||||
const safe = join(repo, ".artifacts", "issue3329-macos", "run-1");
|
||||
assert.equal(safetyModule.validateArtifactRoot(safe, repo), safe);
|
||||
});
|
||||
|
||||
test("cleanup rejects broad, traversing, home, and arbitrary external targets", () => {
|
||||
assert.ok(safetyModule, "artifact cleanup safety module must exist");
|
||||
const outside = mkdtempSync(join(tmpdir(), "hf-issue3329-external-"));
|
||||
for (const target of [
|
||||
parse(repo).root,
|
||||
homedir(),
|
||||
repo,
|
||||
join(repo, ".artifacts"),
|
||||
join(repo, ".artifacts", "issue3329-macos", "..", ".."),
|
||||
outside,
|
||||
]) {
|
||||
assert.throws(() => safetyModule.validateArtifactRoot(target, repo), /artifact root/i);
|
||||
}
|
||||
});
|
||||
|
||||
test("cleanup rejects a descendant that escapes through a symlink", () => {
|
||||
assert.ok(safetyModule, "artifact cleanup safety module must exist");
|
||||
const artifactSpace = join(repo, ".artifacts");
|
||||
const outside = mkdtempSync(join(tmpdir(), "hf-issue3329-symlink-external-"));
|
||||
const escape = join(artifactSpace, `escape-${process.pid}-${Date.now()}`);
|
||||
mkdirSync(artifactSpace, { recursive: true });
|
||||
symlinkSync(outside, escape, "dir");
|
||||
try {
|
||||
assert.throws(
|
||||
() => safetyModule.validateArtifactRoot(join(escape, "run"), repo),
|
||||
/artifact root/i,
|
||||
);
|
||||
} finally {
|
||||
unlinkSync(escape);
|
||||
rmSync(outside, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("ffprobe terminates option parsing before its controlled input path", () => {
|
||||
const source = readFileSync(join(scriptsDir, "issue3329-macos-real-pixel.mjs"), "utf8");
|
||||
assert.match(
|
||||
source,
|
||||
/"-of",\s*"json",\s*"--",\s*path/,
|
||||
"ffprobe input must follow the -- option terminator",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user