fix: address QA report P0-P2 issues for 10/10 agent experience (#208)

## Summary

Addresses all 8 issues from the QA report to improve agent and user experience.

### P0 — Must Fix

- **Blank template broken captions**: Removed `compositions/captions.html` and its reference from the blank template. Every agent (10/10) hit 404 errors during render.
- **Inner-wrapper example**: Added a clear structural comment in the blank template showing the correct `class="clip"` + inner wrapper pattern.
- **Sub-composition introspection**: `hyperframes compositions` now reads external HTML files referenced via `data-composition-src` and shows their real duration/element count instead of `0.0s / 0 elements`.

### P1 — Fix Soon

- **Font mapping warnings**: Now lists all mapped fonts, suggests alternatives (use a mapped font, add @font-face, install locally), and links to docs.
- **Browser 404s**: Non-font "Failed to load resource" 404s now prefixed with `[non-blocking]` instead of `[Browser:ERROR]`.
- **Render concurrency**: Default workers increased from `cores/2` (max 4) to `cores*3/4` (max 6). Added `--concurrency` alias.

### P2 — Nice to Have

- **Transform conflict fix suggestion**: `gsap_css_transform_conflict` now suggests exact GSAP property replacements (e.g., `xPercent: -50, yPercent: -50`).
- **Upgrade --yes**: Now actually runs the install instead of just printing the command.

## Before / After

### Font mapping warning

**Before:**

```
[Compiler] No deterministic font mapping for: DM Sans
```

**After:**

```
[Compiler] No deterministic font mapping for: DM Sans
  Mapped fonts: arial → inter, courier → jetbrains-mono, ...
  To fix, pick one:
    1. Use a mapped font name instead (see list above)
    2. Add a @font-face block in your HTML with a local or hosted font file
    3. Install the font locally on the render machine (Docker: add to Dockerfile)
    4. Add an alias to FONT_ALIASES in deterministicFonts.ts (for contributors)
```

### Browser 404s during render

**Before:** `[Browser:ERROR] Failed to load resource: the server responded with a status of 404`
**After:** `[non-blocking] Failed to load resource: the server responded with a status of 404`

### Transform conflict lint

**Before:** `Fix: Remove the transform from CSS and use tl.fromTo...`
**After:** `Fix: Remove transform: translate(-50%, -50%) from CSS and replace with GSAP properties: xPercent: -50, yPercent: -50`

### Compositions command

**Before:**

```
overlay   0.0s   1920×1080   0 elements
```

**After:**

```
overlay   8.0s   1920×1080   2 elements ← compositions/overlay.html
```

## Test plan

- [x] All 422 core tests pass
- [x] TypeScript compiles cleanly (all 4 packages)
- [x] Full monorepo build succeeds
- [x] `hyperframes init --template blank` ships without broken captions reference
This commit is contained in:
Miguel Ángel
2026-04-06 18:56:39 +02:00
committed by GitHub
parent c47e710ffc
commit baa3d813be
8 changed files with 198 additions and 136 deletions
@@ -1,95 +0,0 @@
<template id="captions-template">
<div
data-composition-id="captions"
data-width="1920"
data-height="1080"
data-duration="__VIDEO_DURATION__"
>
<div id="captions-container"></div>
<style>
[data-composition-id="captions"] {
width: 1920px;
height: 1080px;
pointer-events: none;
}
[data-composition-id="captions"] #captions-container {
position: absolute;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 150px;
}
.caption-group {
position: absolute;
opacity: 0;
font-family: "Inter", sans-serif;
font-size: 48px;
font-weight: 700;
color: #ffffff;
text-shadow:
0 2px 8px rgba(0, 0, 0, 0.8),
0 0 2px rgba(0, 0, 0, 0.9);
white-space: nowrap;
max-width: 1600px;
overflow: hidden;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
(function () {
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
const script = [];
if (script.length === 0) {
window.__timelines["captions"] = tl;
return;
}
const container = document.getElementById("captions-container");
// Group words into lines (max 5 words per line)
const lines = [];
for (let i = 0; i < script.length; i += 5) {
const lineWords = script.slice(i, i + 5);
lines.push({
text: lineWords.map((w) => w.text).join(" "),
start: lineWords[0].start,
end: lineWords[lineWords.length - 1].end,
});
}
lines.forEach((line, index) => {
const el = document.createElement("div");
el.className = "caption-group";
el.textContent = line.text;
container.appendChild(el);
tl.fromTo(
el,
{ opacity: 0, y: 20 },
{ opacity: 1, y: 0, duration: 0.3, ease: "power3.out" },
line.start,
);
const hideTime =
index < lines.length - 1 ? Math.min(line.end, lines[index + 1].start) : line.end;
tl.to(el, { opacity: 0, y: -10, duration: 0.25, ease: "power2.in" }, hideTime - 0.25);
tl.set(el, { opacity: 0, visibility: "hidden" }, hideTime);
});
window.__timelines["captions"] = tl;
})();
</script>
</div>
</template>
+9 -10
View File
@@ -48,21 +48,20 @@
data-volume="1"
></audio>
<div
id="captions-comp"
data-composition-id="captions"
data-composition-src="compositions/captions.html"
data-start="0"
data-duration="__VIDEO_DURATION__"
data-track-index="3"
data-width="1920"
data-height="1080"
></div>
<!--
ANIMATION PATTERN: The clip div controls timing/visibility.
Always put your content in a CHILD element and animate THAT.
<div class="clip" ...> ← timing only, don't animate this
<div id="my-title">...</div> ← animate this with GSAP
</div>
-->
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// tl.from("#my-title", { opacity: 0, y: -50, duration: 1 }, 0);
window.__timelines["main"] = tl;
</script>
</body>