mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(cli): capture writes inline SVG logos whole and parakeet takes long audio (#5662)
* fix(cli): capture writes inline SVG logos whole and parakeet takes long audio * fix(cli): keep the decode timeout comment under the line limit
This commit is contained in:
@@ -351,6 +351,7 @@ describe("drop counts — why a referenced asset is not in the capture", () => {
|
||||
expect(css).toContain("assets/fonts/font-0.woff2");
|
||||
expect(drops).toEqual({
|
||||
"size-floor": 0,
|
||||
"size-ceiling": 0,
|
||||
"budget-exhausted": 0,
|
||||
"cap-reached": 0,
|
||||
unavailable: 0,
|
||||
@@ -421,6 +422,32 @@ describe("drop counts — why a referenced asset is not in the capture", () => {
|
||||
expect(drops["cap-reached"]).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
it("writes an inline SVG logo over 10 KB whole, and skips one over the size cap uncut", async () => {
|
||||
await withTempDir(async (dir) => {
|
||||
const shapes = (n: number) =>
|
||||
Array.from({ length: n }, (_, i) => `<path d="M${i} 0h1v10h-1z"/>`).join("");
|
||||
const logo = `<svg viewBox="0 0 3000 10">${shapes(3000)}</svg>`;
|
||||
const huge = `<svg viewBox="0 0 1 10">${shapes(1)}<desc>${"x".repeat(3 * 1024 * 1024)}</desc></svg>`;
|
||||
expect(logo.length).toBeGreaterThan(10_000);
|
||||
const { assets, drops } = await downloadAssets(
|
||||
{
|
||||
svgs: [
|
||||
{ outerHTML: logo, isLogo: true },
|
||||
{ outerHTML: huge, isLogo: false },
|
||||
],
|
||||
sections: [],
|
||||
ogImage: "",
|
||||
} as unknown as DesignTokens,
|
||||
dir,
|
||||
);
|
||||
expect(assets).toHaveLength(1);
|
||||
const written = readFileSync(join(dir, assets[0]!.localPath), "utf-8");
|
||||
expect(written).toBe(toStandaloneSvg(logo));
|
||||
expect(written.endsWith("</svg>")).toBe(true);
|
||||
expect(drops["size-ceiling"]).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("asset fetches present the same identity as the page navigation", () => {
|
||||
|
||||
@@ -40,6 +40,8 @@ interface DownloadBudgetOptions {
|
||||
export type AssetDropReason =
|
||||
/** Fetched, then judged too small to be a real asset rather than a spacer or tracking pixel. */
|
||||
| "size-floor"
|
||||
/** Larger than any one asset may be; skipped whole, never cut. */
|
||||
| "size-ceiling"
|
||||
/** The post-navigation clock ran out before this one was reached. */
|
||||
| "budget-exhausted"
|
||||
/** A per-run or per-family limit was already met. */
|
||||
@@ -51,7 +53,13 @@ export type AssetDropCounts = Record<AssetDropReason, number>;
|
||||
|
||||
/** A tally with every reason at zero — the shape a caller merges into. */
|
||||
export function noDrops(): AssetDropCounts {
|
||||
return { "size-floor": 0, "budget-exhausted": 0, "cap-reached": 0, unavailable: 0 };
|
||||
return {
|
||||
"size-floor": 0,
|
||||
"size-ceiling": 0,
|
||||
"budget-exhausted": 0,
|
||||
"cap-reached": 0,
|
||||
unavailable: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** Sum two tallies. Used to fold the font pass and the asset pass into one capture-wide count. */
|
||||
@@ -326,6 +334,7 @@ export async function downloadAssets(
|
||||
ensureCaptureDirSync(outputDir, join(outputDir, "assets", "svgs"));
|
||||
const usedSvgNames = new Set<string>();
|
||||
const MAX_INLINE_SVGS = 30;
|
||||
const MAX_INLINE_SVG_BYTES = 2 * 1024 * 1024;
|
||||
drops["cap-reached"] += Math.max(0, tokens.svgs.length - MAX_INLINE_SVGS);
|
||||
for (let i = 0; i < tokens.svgs.length && i < MAX_INLINE_SVGS; i++) {
|
||||
const svg = tokens.svgs[i]!;
|
||||
@@ -335,6 +344,10 @@ export async function downloadAssets(
|
||||
}
|
||||
// Hash the bytes that actually land on disk, so the filename still can't drift from content.
|
||||
const svgFile = toStandaloneSvg(svg.outerHTML);
|
||||
if (Buffer.byteLength(svgFile, "utf-8") > MAX_INLINE_SVG_BYTES) {
|
||||
drops["size-ceiling"]++;
|
||||
continue;
|
||||
}
|
||||
const slug = svgContentHashSlug(svgFile, !!svg.isLogo);
|
||||
let finalSlug = slug;
|
||||
let suffix = 2;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runInNewContext } from "node:vm";
|
||||
import { Window } from "happy-dom";
|
||||
import type { Page } from "puppeteer-core";
|
||||
import { extractTokens } from "./tokenExtractor.js";
|
||||
|
||||
async function tokensOf(body: string) {
|
||||
const window = new Window();
|
||||
window.document.body.innerHTML = body;
|
||||
const evaluate: Page["evaluate"] = async (script, ..._args) => {
|
||||
if (typeof script !== "string") throw new Error("Expected a page expression");
|
||||
return runInNewContext(script, {
|
||||
window,
|
||||
document: window.document,
|
||||
location: window.location,
|
||||
getComputedStyle: window.getComputedStyle.bind(window),
|
||||
});
|
||||
};
|
||||
try {
|
||||
return await extractTokens({ evaluate } as Page);
|
||||
} finally {
|
||||
await window.happyDOM.close();
|
||||
}
|
||||
}
|
||||
|
||||
describe("capture tokens", () => {
|
||||
it("keeps an inline SVG logo over 10 KB whole", async () => {
|
||||
const shapes = Array.from({ length: 3000 }, (_, i) => `<path d="M${i} 0h1v10h-1z"></path>`);
|
||||
const logo = `<svg viewBox="0 0 3000 10" width="300">${shapes.join("")}</svg>`;
|
||||
expect(logo.length).toBeGreaterThan(10_000);
|
||||
const tokens = await tokensOf(`<header class="logo"><a href="/">${logo}</a></header>`);
|
||||
expect(tokens.svgs).toHaveLength(1);
|
||||
expect(tokens.svgs[0]!.outerHTML.length).toBe(logo.length);
|
||||
expect(tokens.svgs[0]!.outerHTML.endsWith("</svg>")).toBe(true);
|
||||
expect(tokens.svgs[0]!.isLogo).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -386,7 +386,7 @@ const EXTRACT_SCRIPT = `(() => {
|
||||
viewBox: svg.getAttribute("viewBox") || undefined,
|
||||
width: Math.round(rect.width),
|
||||
height: Math.round(rect.height),
|
||||
outerHTML: svg.outerHTML.slice(0, 10000),
|
||||
outerHTML: svg.outerHTML,
|
||||
isLogo: isLogo
|
||||
};
|
||||
}).filter(Boolean).slice(0, 50);
|
||||
|
||||
@@ -14,7 +14,9 @@ import { basename, dirname, join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { DownloadOptions } from "../cloud/download.js";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
decodeTimeoutMs,
|
||||
ensureParakeetModel,
|
||||
installSherpaRuntime,
|
||||
SHERPA_RUNTIME_DIR,
|
||||
@@ -470,3 +472,16 @@ describe("sherpaUnsupportedReason", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decodeTimeoutMs", () => {
|
||||
// 1851.245125 s of audio once gave 3702490.25, which execFile refused as out of range.
|
||||
it.each([1851.245125, 10_000_000])(
|
||||
"gives execFile a timeout it accepts for %s s of audio",
|
||||
(seconds) => {
|
||||
const timeout = decodeTimeoutMs(seconds);
|
||||
expect(Number.isInteger(timeout)).toBe(true);
|
||||
expect(timeout).toBeGreaterThanOrEqual(Math.min(seconds * 2000, 2 ** 31 - 1));
|
||||
expect(() => execFileSync(process.execPath, ["-e", ""], { timeout })).not.toThrow();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -82,8 +82,9 @@ const PARAKEET_MODEL_FILES: readonly ModelFile[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/** At least 30 minutes, and twice the audio length for slow CPUs. */
|
||||
const decodeTimeoutMs = (audioSeconds: number) => Math.max(1_800_000, audioSeconds * 2000);
|
||||
/** At least 30 minutes, twice the audio length for slow CPUs; whole ms, within Node's timer limit. */
|
||||
export const decodeTimeoutMs = (audioSeconds: number) =>
|
||||
Math.min(2 ** 31 - 1, Math.max(1_800_000, Math.ceil(audioSeconds * 2000)));
|
||||
|
||||
/** The platforms sherpa-onnx-node publishes a native package for. */
|
||||
const SUPPORTED_TARGETS = [
|
||||
|
||||
Reference in New Issue
Block a user