diff --git a/packages/core/src/lint/hyperframeLinter.ts b/packages/core/src/lint/hyperframeLinter.ts index 26cc01edb..bc3700ff8 100644 --- a/packages/core/src/lint/hyperframeLinter.ts +++ b/packages/core/src/lint/hyperframeLinter.ts @@ -391,23 +391,27 @@ export function lintHyperframeHtml( } } - // #3.7: Inline base64 audio/video — PROHIBITED - // Base64 audio/video bloats file size and breaks rendering. Use URLs or relative paths. + // #3.7: Fabricated inline base64 media — CRITICAL + // Inline base64 audio/video data is almost always fabricated garbage that + // won't play. Real audio files are 100KB+ when base64-encoded. { const base64MediaRe = - /src\s*=\s*["'](data:(?:audio|video)\/[^;]+;base64,([A-Za-z0-9+/=]{20,}))["']/gi; + /src\s*=\s*["'](data:(?:audio|video)\/[^;]+;base64,([A-Za-z0-9+/=]{100,}))["']/gi; let b64Match: RegExpExecArray | null; while ((b64Match = base64MediaRe.exec(source)) !== null) { + // Check if it's suspiciously repetitive (fake data has long runs of repeated chars) const sample = (b64Match[2] || "").slice(0, 200); const uniqueChars = new Set(sample.replace(/[A-Za-z0-9+/=]/g, (c) => c)).size; const dataSize = Math.round(((b64Match[2] || "").length * 3) / 4); const isSuspicious = uniqueChars < 15 || (dataSize > 1000 && dataSize < 50000); + // Any embedded base64 audio is suspicious — real audio should be a file pushFinding({ - code: "base64_media_prohibited", - severity: "error", - message: `Inline base64 audio/video detected (${(dataSize / 1024).toFixed(0)} KB)${isSuspicious ? " — likely fabricated data" : ""}. Base64 media is prohibited — it bloats file size and breaks rendering.`, - fixHint: - "Use a relative path (assets/music.mp3) or HTTPS URL for the audio/video src. Never embed media as base64.", + code: "fabricated_inline_media", + severity: isSuspicious ? "error" : "warning", + message: isSuspicious + ? `Fabricated base64 media detected (${(dataSize / 1024).toFixed(0)} KB). This is almost certainly fake data that won't play.` + : `Embedded base64 audio/video detected (${(dataSize / 1024).toFixed(0)} KB). Consider using a file URL instead.`, + fixHint: "Remove the data: URI and use a real media file URL instead.", snippet: truncateSnippet((b64Match[1] ?? "").slice(0, 80) + "..."), }); } diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts index 0feb38b95..0f3c8efb4 100644 --- a/packages/producer/src/services/htmlCompiler.ts +++ b/packages/producer/src/services/htmlCompiler.ts @@ -362,16 +362,24 @@ function promoteCssImportsToLinkTags(html: string): string { */ function scopeCssToComposition(css: string, compositionId: string): string { const scope = `[data-composition-id="${compositionId}"]`; + // Extract @import rules first — they have no {} block and the selector + // regex corrupts them by treating the text after @ as a selector. + const importRe = /@import\s+url\([^)]*\)\s*;|@import\s+["'][^"']+["']\s*;/gi; + const imports: string[] = []; + const cssWithoutImports = css.replace(importRe, (match) => { + imports.push(match.trim()); + return ""; + }); // Split on top-level rule boundaries. Simple regex approach: // scope each selector in rule blocks while preserving at-rules. - return css.replace(/([^{}@]+)\{/g, (match, selectors: string) => { + const scoped = cssWithoutImports.replace(/([^{}@]+)\{/g, (match, selectors: string) => { const trimmed = selectors.trim(); // Skip @-rule headers (they don't have selectors to scope) if (trimmed.startsWith("@")) return match; // Skip if already scoped to this composition if (trimmed.includes(`data-composition-id="${compositionId}"`)) return match; // Scope each comma-separated selector - const scoped = trimmed + const scopedSelectors = trimmed .split(",") .map((s: string) => { const sel = s.trim(); @@ -381,8 +389,9 @@ function scopeCssToComposition(css: string, compositionId: string): string { return `${scope} ${sel}`; }) .join(", "); - return `${scoped} {`; + return `${scopedSelectors} {`; }); + return imports.length > 0 ? imports.join("\n") + "\n\n" + scoped : scoped; } function coalesceHeadStylesAndBodyScripts(html: string): string { diff --git a/packages/producer/tests/css-import-scoping/meta.json b/packages/producer/tests/css-import-scoping/meta.json new file mode 100644 index 000000000..2db144af4 --- /dev/null +++ b/packages/producer/tests/css-import-scoping/meta.json @@ -0,0 +1,12 @@ +{ + "name": "css-import-scoping", + "description": "Verifies @import url() rules survive CSS composition scoping. Regression test for scopeCssToComposition corrupting @import into invalid @[data-composition-id] import url().", + "tags": ["regression", "css-scoping"], + "minPsnr": 30, + "maxFrameFailures": 0, + "minAudioCorrelation": 0, + "maxAudioLagWindows": 0, + "renderConfig": { + "fps": 30 + } +} diff --git a/packages/producer/tests/css-import-scoping/output/compiled.html b/packages/producer/tests/css-import-scoping/output/compiled.html new file mode 100644 index 000000000..f53b8a29a --- /dev/null +++ b/packages/producer/tests/css-import-scoping/output/compiled.html @@ -0,0 +1,82 @@ + + + + + + + + + +
+
+
+
CSS IMPORT TEST
+ + + + +
+
+ + + diff --git a/packages/producer/tests/css-import-scoping/output/output.mp4 b/packages/producer/tests/css-import-scoping/output/output.mp4 new file mode 100644 index 000000000..6ff38ac18 --- /dev/null +++ b/packages/producer/tests/css-import-scoping/output/output.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4234211da34fdc26ccefc72ea2dd859b24b776fd980ae51a109783397dbc2097 +size 38945 diff --git a/packages/producer/tests/css-import-scoping/src/compositions/overlay.html b/packages/producer/tests/css-import-scoping/src/compositions/overlay.html new file mode 100644 index 000000000..1ad009a87 --- /dev/null +++ b/packages/producer/tests/css-import-scoping/src/compositions/overlay.html @@ -0,0 +1,52 @@ + diff --git a/packages/producer/tests/css-import-scoping/src/index.html b/packages/producer/tests/css-import-scoping/src/index.html new file mode 100644 index 000000000..e2e2712c8 --- /dev/null +++ b/packages/producer/tests/css-import-scoping/src/index.html @@ -0,0 +1,25 @@ + + + + + + + + + +
+
+
+ + +