feat(producer): support entryFile for rendering individual compositions (#42)

## Changes

- Add optional `entryFile` parameter to render API endpoints (`/v1/render` and `/v1/render-stream`)
- Enable rendering individual sub-compositions by extracting them from index.html context when the entry file is a `<template>` wrapper
- Change base64 audio/video linting from detecting "fabricated" media to prohibiting all inline base64 media
- Add manifest path resolution for bundled producer deployments

## API Changes

- `RenderConfig` — new optional `entryFile` field for specifying HTML file to render
- `server.ts` — parses `entryFile` from request body, validates file exists in project directory
- `executeRenderJob` — uses `entryFile` instead of hardcoded `"index.html"`

## Template Extraction

- `extractStandaloneEntryFromIndex` — extracts sub-composition hosts from index.html and creates standalone render context
- Handles `<template>` entry files by finding matching `data-composition-src` in index.html and isolating that host
- Resets `data-start` to 0 for standalone rendering

## Linting Updates

- Change rule #3.7 from detecting "fabricated" base64 media to prohibiting all inline base64 audio/video
- Lower detection threshold from 100+ to 20+ base64 characters
- All base64 media now triggers error severity with clearer messaging about file size bloat

## Usage

```json
POST /v1/render-stream
{ "projectDir": "/path/to/project", "entryFile": "compositions/intro.html" }
```

Omit `entryFile` for default behavior (renders `index.html`).
This commit is contained in:
Miguel Ángel
2026-03-27 04:28:51 +01:00
committed by GitHub
parent 244a9cbe60
commit dc0d69a21b
5 changed files with 192 additions and 17 deletions
+8 -12
View File
@@ -391,27 +391,23 @@ export function lintHyperframeHtml(
}
}
// #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.
// #3.7: Inline base64 audio/video — PROHIBITED
// Base64 audio/video bloats file size and breaks rendering. Use URLs or relative paths.
{
const base64MediaRe =
/src\s*=\s*["'](data:(?:audio|video)\/[^;]+;base64,([A-Za-z0-9+/=]{100,}))["']/gi;
/src\s*=\s*["'](data:(?:audio|video)\/[^;]+;base64,([A-Za-z0-9+/=]{20,}))["']/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: "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.",
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.",
snippet: truncateSnippet((b64Match[1] ?? "").slice(0, 80) + "..."),
});
}