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:
Miguel Ángel
2026-10-15 21:16:20 +00:00
committed by GitHub
parent cea8aa8a7d
commit 0d6bdd453b
6 changed files with 97 additions and 4 deletions
@@ -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", () => {
+14 -1
View File
@@ -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);
});
});
+1 -1
View File
@@ -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);
+15
View File
@@ -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();
},
);
});
+3 -2
View File
@@ -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 = [