mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 16:42:27 +00:00
fix(skills): preserve caption skin contrast states (#2486)
* fix(skills): preserve caption skin contrast states * chore(skills): refresh caption contrast manifest * fix(skills): keep caption skin ownership aligned * style(skills): format shared caption builders
This commit is contained in:
@@ -34,9 +34,10 @@
|
||||
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseStoryboard } from "./lib/storyboard.mjs";
|
||||
import { captionBand, parseFormat } from "./lib/dimensions.mjs";
|
||||
import { parseColors, parseFonts, semanticColors, lum } from "./lib/tokens.mjs";
|
||||
import { parseColors, parseFonts, semanticColors } from "./lib/tokens.mjs";
|
||||
|
||||
const flag = (argv, name, def) => {
|
||||
const i = argv.indexOf(`--${name}`);
|
||||
@@ -276,23 +277,11 @@ function buildFromSkin(skin, groups, total, W, H, tokens, die, faces = "", fonts
|
||||
// overflow:hidden — nothing is clipped); zeroing it would need an airy line-height that
|
||||
// balloons the pill, which is worse. Override only if a brand font genuinely clips.
|
||||
out += "\n<style>\n .caption-line { line-height: 1.1 !important; }\n</style>";
|
||||
// dark-ground caption contrast (auto). The preset caption skins are tuned for a LIGHT
|
||||
// pill (cream); on a dark brand ground the pill goes near-black, so the skin's faint
|
||||
// upcoming-word mix and light highlight block turn unreadable. When the resolved caption
|
||||
// canvas is dark, override the three word states: a brighter muted upcoming color + an
|
||||
// on-brand ACCENT highlight block with light text. Light grounds are left untouched.
|
||||
const capCanvas = (tokens.match(/--cap-canvas:\s*(#[0-9a-fA-F]{6})/) || [])[1];
|
||||
if (capCanvas && (lum(capCanvas) ?? 255) < 90) {
|
||||
out +=
|
||||
"\n<style>\n" +
|
||||
" .caption-word { color: color-mix(in srgb, var(--cap-ink) 64%, var(--cap-canvas)); }\n" +
|
||||
" .caption-word.is-active { color: var(--cap-ink); background: var(--cap-accent); box-shadow: 0 0 0 0.06em var(--cap-accent); }\n" +
|
||||
" .caption-word.is-spoken { color: var(--cap-ink); background: transparent; box-shadow: none; }\n" +
|
||||
"</style>";
|
||||
}
|
||||
return `<template id="captions-template" data-composition-id="captions" data-width="${W}" data-height="${H}">\n${out.trim()}\n</template>\n`;
|
||||
}
|
||||
|
||||
export { buildFromSkin };
|
||||
|
||||
// @font-face for the brand display/body fonts, matched from the project's font dirs
|
||||
// (staged assets/fonts first, else capture/assets/fonts) by family-name prefix, with
|
||||
// weight parsed from the filename. Paths are relative to compositions/captions.html.
|
||||
@@ -498,11 +487,13 @@ function buildCaptionsHtml(groups, total, W, H) {
|
||||
`;
|
||||
}
|
||||
|
||||
const sub = process.argv[2];
|
||||
if (sub === "build" || sub === undefined) runBuild(process.argv.slice(sub === "build" ? 3 : 2));
|
||||
else {
|
||||
console.error(
|
||||
"usage: node captions.mjs build [--storyboard …] [--audio-meta …] [--hyperframes .]",
|
||||
);
|
||||
process.exit(2);
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const sub = process.argv[2];
|
||||
if (sub === "build" || sub === undefined) runBuild(process.argv.slice(sub === "build" ? 3 : 2));
|
||||
else {
|
||||
console.error(
|
||||
"usage: node captions.mjs build [--storyboard …] [--audio-meta …] [--hyperframes .]",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { buildFromSkin } from "./captions.mjs";
|
||||
|
||||
const presetsDir = fileURLToPath(
|
||||
new URL("../../hyperframes-creative/frame-presets/", import.meta.url),
|
||||
);
|
||||
const skins = readdirSync(presetsDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => ({
|
||||
name: entry.name,
|
||||
source: readFileSync(
|
||||
new URL(
|
||||
`../../hyperframes-creative/frame-presets/${entry.name}/caption-skin.html`,
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
),
|
||||
}))
|
||||
.filter(({ source }) => source.includes(".caption-word.is-active"));
|
||||
|
||||
for (const canvas of ["#f7f3e8", "#111827"]) {
|
||||
for (const skin of skins) {
|
||||
test(`${skin.name} preserves word-state rules on ${canvas}`, () => {
|
||||
const active = skin.source.match(/\.caption-word\.is-active\s*\{[^}]*\}/s)?.[0];
|
||||
const spoken = skin.source.match(/\.caption-word\.is-spoken\s*\{[^}]*\}/s)?.[0];
|
||||
assert.ok(active, "skin must define an active-word rule");
|
||||
assert.ok(spoken, "skin must define a spoken-word rule");
|
||||
|
||||
const output = buildFromSkin(
|
||||
skin.source,
|
||||
[],
|
||||
1,
|
||||
1920,
|
||||
1080,
|
||||
`:root { --cap-canvas: ${canvas}; --cap-ink: #111111; --cap-accent: #ffcc00; }`,
|
||||
(message) => {
|
||||
throw new Error(message);
|
||||
},
|
||||
);
|
||||
|
||||
assert.ok(output.includes(active));
|
||||
assert.ok(output.includes(spoken));
|
||||
assert.equal(output.match(/\.caption-word\.is-active\s*\{/g)?.length, 1);
|
||||
assert.equal(output.match(/\.caption-word\.is-spoken\s*\{/g)?.length, 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user