mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-09 03:16:38 +00:00
fix(cli): address media treatment review findings
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
analyzeMediaGrade,
|
||||
type MediaTreatmentAnalysis,
|
||||
} from "../../../../skills/media-use/scripts/lib/grade-analyzer.mjs";
|
||||
} from "@hyperframes/core/media-grade-analyzer";
|
||||
import { findFFmpeg, findFFprobe, getFFmpegInstallHint } from "../browser/ffmpeg.js";
|
||||
|
||||
interface CliMediaTreatmentAnalysis extends Omit<MediaTreatmentAnalysis, "adjust"> {
|
||||
suggestedPatch: { adjust: Record<string, number> };
|
||||
suggestedPatch: { adjust: MediaTreatmentAnalysis["adjust"] };
|
||||
}
|
||||
|
||||
export function analyzeMediaTreatment(mediaPath: string): CliMediaTreatmentAnalysis {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { runCommand } from "citty";
|
||||
@@ -210,21 +210,47 @@ describe("applyMediaTreatmentToHtml", () => {
|
||||
});
|
||||
|
||||
it("resolves nested composition media through the shared project-root contract", () => {
|
||||
const project = "/tmp/hf-project";
|
||||
const project = mkdtempSync(join(tmpdir(), "hf-media-treatment-assets-"));
|
||||
mkdirSync(join(project, "capture"), { recursive: true });
|
||||
mkdirSync(join(project, "assets"), { recursive: true });
|
||||
writeFileSync(join(project, "capture", "talking-head.mp4"), "");
|
||||
writeFileSync(join(project, "assets", "photo.webp"), "");
|
||||
writeFileSync(join(project, "assets", "My Clip.mp4"), "");
|
||||
|
||||
expect(
|
||||
resolveMediaTreatmentSource(
|
||||
project,
|
||||
"compositions/scene.html",
|
||||
"../capture/talking-head.mp4?v=1#frame",
|
||||
),
|
||||
).toBe(join(project, "capture/talking-head.mp4"));
|
||||
expect(
|
||||
resolveMediaTreatmentSource(project, "compositions/scene.html", "assets/photo.webp"),
|
||||
).toBe(join(project, "assets/photo.webp"));
|
||||
expect(() =>
|
||||
resolveMediaTreatmentSource(project, "compositions/scene.html", "https://example.com/a.mp4"),
|
||||
).toThrow(/local project asset/);
|
||||
try {
|
||||
expect(
|
||||
resolveMediaTreatmentSource(
|
||||
project,
|
||||
"compositions/scene.html",
|
||||
"../capture/talking-head.mp4?v=1#frame",
|
||||
),
|
||||
).toBe(join(project, "capture/talking-head.mp4"));
|
||||
expect(
|
||||
resolveMediaTreatmentSource(project, "compositions/scene.html", "assets/photo.webp"),
|
||||
).toBe(join(project, "assets/photo.webp"));
|
||||
expect(
|
||||
resolveMediaTreatmentSource(
|
||||
project,
|
||||
"compositions/scene.html",
|
||||
"/assets/My%20Clip.mp4?v=1",
|
||||
),
|
||||
).toBe(join(project, "assets/My Clip.mp4"));
|
||||
expect(() =>
|
||||
resolveMediaTreatmentSource(
|
||||
project,
|
||||
"compositions/scene.html",
|
||||
"https://example.com/a.mp4",
|
||||
),
|
||||
).toThrow(/local project asset/);
|
||||
expect(() => resolveMediaTreatmentSource(project, "compositions/scene.html", "#")).toThrow(
|
||||
/local project asset/,
|
||||
);
|
||||
expect(() =>
|
||||
resolveMediaTreatmentSource(project, "compositions/scene.html", "missing.mp4"),
|
||||
).toThrow(/Media file not found/);
|
||||
} finally {
|
||||
rmSync(project, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("merges a validated patch and reports the stored before and after payloads", () => {
|
||||
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
isColorGradingVariableRef,
|
||||
validateColorGradingContract,
|
||||
} from "@hyperframes/parsers/color-grading-contract";
|
||||
import {
|
||||
cleanAssetUrl,
|
||||
isRemoteOrInlineUrl,
|
||||
resolveExistingLocalAsset,
|
||||
} from "@hyperframes/parsers/asset-resolution";
|
||||
import { rewriteAssetPath } from "@hyperframes/parsers/asset-paths";
|
||||
import { patchElementInHtml } from "@hyperframes/studio-server/source-mutation";
|
||||
import { defineCommand } from "citty";
|
||||
@@ -247,7 +252,7 @@ export function getMediaTreatmentCapabilityDetail(id: string): unknown {
|
||||
description:
|
||||
"Deterministic source measurements for agent decisions; visual scopes remain a Studio display.",
|
||||
command:
|
||||
"hyperframes media-treatment --file <composition.html> --selector <media> --analyze --json",
|
||||
"hyperframes media-treatment --file compositions/scene.html --selector '#hero' --analyze --json",
|
||||
output: [
|
||||
"source color metadata and HDR/LOG warnings",
|
||||
"luma percentile and clipping evidence",
|
||||
@@ -481,34 +486,26 @@ function mediaSourceForElement(element: Element): string {
|
||||
return src;
|
||||
}
|
||||
|
||||
function stripUrlSuffix(value: string): string {
|
||||
const queryIndex = value.indexOf("?");
|
||||
const hashIndex = value.indexOf("#");
|
||||
if (queryIndex < 0 && hashIndex < 0) return value;
|
||||
const end =
|
||||
queryIndex < 0 ? hashIndex : hashIndex < 0 ? queryIndex : Math.min(queryIndex, hashIndex);
|
||||
return value.slice(0, end);
|
||||
}
|
||||
|
||||
export function resolveMediaTreatmentSource(
|
||||
projectDir: string,
|
||||
compositionFile: string,
|
||||
source: string,
|
||||
): string {
|
||||
const cleanSource = stripUrlSuffix(source.trim());
|
||||
if (/^[a-z][a-z\d+.-]*:/i.test(cleanSource) || cleanSource.startsWith("//")) {
|
||||
const sourceUrl = source.trim();
|
||||
if (!sourceUrl) throw new Error("Selected media has no analyzable local src");
|
||||
if (isRemoteOrInlineUrl(sourceUrl)) {
|
||||
throw new Error(
|
||||
"Media analysis requires a local project asset; freeze remote media with media-use first",
|
||||
);
|
||||
}
|
||||
const cleanSource = cleanAssetUrl(sourceUrl);
|
||||
if (!cleanSource) throw new Error("Selected media has no analyzable local src");
|
||||
const projectRelative = cleanSource.startsWith("/")
|
||||
? cleanSource.slice(1)
|
||||
? cleanSource
|
||||
: rewriteAssetPath(compositionFile, cleanSource);
|
||||
const mediaPath = resolve(projectDir, projectRelative);
|
||||
if (!isPathInside(mediaPath, projectDir)) {
|
||||
throw new Error("Selected media resolves outside the project");
|
||||
}
|
||||
return mediaPath;
|
||||
const asset = resolveExistingLocalAsset(projectDir, projectRelative);
|
||||
if (!asset) throw new Error(`Media file not found: ${source}`);
|
||||
return asset.resolved;
|
||||
}
|
||||
|
||||
function parseGrading(raw: string | undefined, apply: boolean, clear: boolean): unknown {
|
||||
@@ -604,7 +601,6 @@ function analyzeTarget(args: MediaTreatmentCommandArgs) {
|
||||
const source = mediaSourceForElement(element);
|
||||
const compositionFile = relative(project.dir, filePath).split("\\").join("/");
|
||||
const mediaPath = resolveMediaTreatmentSource(project.dir, compositionFile, source);
|
||||
if (!existsSync(mediaPath)) throw new Error(`Media file not found: ${source}`);
|
||||
return {
|
||||
ok: true,
|
||||
action: "analyze",
|
||||
|
||||
Reference in New Issue
Block a user