Files
hyperframes/skills/hyperframes-animation/rules/asr-keyword-glow.md
T
WaterrrForever 853256403b feat(skills): c2v mining pass — 7 new blueprints, 10 new rules, compacted recipe corpus (#2680)
* feat(skills): c2v mining pass over animation blueprints and rules

Compacts ~45 existing animation rules/blueprints into tighter recipe form
(net -3.4k lines) and adds 17 mined from the c2v corpus:

- 7 blueprints: agent-progress-theater, camera-journey, fixed-anchor-cycle,
  panel-edit-live-sync, prompt-type-submit-generate,
  transcript-scroll-artifact-reveal, zoom-out-workspace-reveal
- 10 rules: 3d-camera-flight, anchored-layout-expand, chart-scrub-readout,
  chromatic-glitch, control-target-sync, cursor-drag, gradient-text-sweep,
  multi-cursor-choreography, particle-burst, theme-crossfade-morph






Both indexes updated.

* feat(skills): sync product-launch script bank with mined blueprint roles

The role->blueprint script bank in product-launch-video/story-design.md is
kept 1:1 with blueprints-index role declarations, which the c2v mining pass
expanded. Adds the 25 missing entries (script-shape descriptor + example
lines + pattern): 13 for the 7 new blueprints, 12 for role widenings on 7
existing ones (cursor-ui-demo, dataviz-countup, titlecard-reveal, et al.),
and states the 1:1 sync contract in the bank's intro.

* docs(skills): cover constellation-hub scatter-drift variant in the script bank

Review follow-up on #2680: the SOCIAL_PROOF constellation-hub entry
patterned only the orbit shape; the c2v pass added a scatter-drift
end-card variant with the opposite geometry (no hub, no ring). Adds an
example line and extends the pattern so a scatter-drift beat's VO isn't
steered toward the orbit shape.
2026-07-21 17:28:55 +08:00

129 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: asr-keyword-glow
description: Keywords glow + scale up when "spoken" — attack/sustain/release envelope synced to per-word timestamps. Even without real audio, hardcoded timings create a "narrator emphasis" effect.
metadata:
tags: asr, audio-sync, highlight, glow, keyword, text, speech, emphasis
---
# ASR Keyword Glow
Words in a phrase visually activate (glow blur + scale) when "spoken", following an attack-sustain-release envelope over per-word `{ start, end }` timestamps. In a real ASR pipeline the timings come from a word-level transcript (`hyperframes transcribe` — same shape); for promo video, hand-author them to control emphasis pacing. The envelope never falls to zero after a word — it decays to a rest level, leaving a breadcrumb of recent emphasis.
## How It Works
A single linear driver tween (`ease: "none"` — any other ease distorts the per-word envelope; do not change) sweeps scene time; its `onUpdate` loops over ALL words computing each one's envelope: 0 before `start`, linear attack to 1 over `ATTACK_DUR`, sustain at 1 until `end`, decay to `REST_LEVEL` over `RELEASE`, then hold at rest. The envelope drives `text-shadow` blur and `scale` — one driver for the whole phrase, never one tween per word (60+ words would bloat the timeline).
## Recipe
```html
<!-- inside a standard scene clip (hyperframes-core) -->
<div class="phrase">
<span class="word" data-word="{w1Key}">{w1}</span>
<span class="word" data-word="{w2Key}">{w2}</span>
<!-- … the final word may be the brand, with the .brand modifier -->
<span class="word brand" data-word="{brandKey}">{brandWord}</span>
</div>
```
```css
.phrase {
display: flex;
flex-wrap: wrap;
justify-content: center;
color: {restColor};
}
.word {
display: inline-block; /* required for transform on <span> */
transform-origin: 50% 50%;
text-shadow: 0 0 0 {glowColorTransparent};
}
.word.brand {
color: {brandAccentColor};
}
```
```js
// Per-word spoken windows — one entry per span; brand word 1.5-2× a normal word's window.
const TIMINGS = {
// {w1Key}: { start: …, end: … }, — seconds, local to the scene
};
function envelope(time, start, end) {
if (time < start) return 0;
if (time < end) return Math.min((time - start) / ATTACK_DUR, 1);
const releaseEnd = end + RELEASE;
if (time < releaseEnd) return 1 - ((time - end) / RELEASE) * (1 - REST_LEVEL);
return REST_LEVEL;
}
const words = document.querySelectorAll(".word");
const driver = { t: 0 };
tl.to(
driver,
{
t: SCENE_DURATION,
duration: SCENE_DURATION,
ease: "none", // linear — t maps 1:1 to scene time
onUpdate: () => {
words.forEach((el) => {
const timing = TIMINGS[el.dataset.word];
if (!timing) return;
const env = envelope(driver.t, timing.start, timing.end);
el.style.textShadow = `0 0 ${MAX_BLUR * env}px ${glowColorRgba(env)}`;
el.style.transform = `scale(${1 + MAX_SCALE_BOOST * env})`;
});
},
},
0,
);
```
`glowColorRgba(env)` returns the glow color with `env`-modulated alpha.
## Variations
- **Karaoke style (RECOMMENDED for video narration)** — the default amplitudes read too subtle in video: inactive words still dominate. Render inactive words DIM and lerp the active word toward bright + larger; at any moment 12 words are bright (spoken + lingering rest) and the rest is dim. Use for short phrases (510 words) where one word at a time should POP; keep the subtle default for long dense text. Pushes MAX_BLUR, MAX_SCALE_BOOST, and REST↔ACTIVE contrast; everything else identical:
```js
function lerpChannel(a, b, t) {
return Math.round(a + (b - a) * t);
}
function colorAt(env, isBrand) {
const target = isBrand ? BRAND_RGB : ACTIVE_RGB;
return `rgb(${lerpChannel(REST_RGB.r, target.r, env)}, ${lerpChannel(REST_RGB.g, target.g, env)}, ${lerpChannel(REST_RGB.b, target.b, env)})`;
}
// in onUpdate: el.style.color = colorAt(env, el.classList.contains("brand"));
```
- **Multi-octave glow** — multiply the sustain by `1 + sin(driver.t × PULSE_HZ) × PULSE_AMPLITUDE` so high-emphasis words breathe at peak.
- **Color shift on the peak** — same channel-lerp from `restColor``peakColor` as `env` rises (non-karaoke form).
- **3D pop-out** — add `translateZ(env × MAX_POP_Z)` so the spoken word leans toward camera; requires `perspective` on the parent.
- **From real ASR transcripts** — convert `{ word, start_ms, end_ms }` entries to seconds and feed in identically.
## Values
| token | default style | karaoke style | notes |
| --------------- | -------------------- | ------------- | ---------------------------------------------------------- |
| ATTACK_DUR | 0.10.25s | same | must be < the shortest word's window or it never reaches 1 |
| RELEASE | 0.20.5s | same | decay to rest |
| REST_LEVEL | 0.150.4 | 0.050.2 | > 0 (breadcrumb), < 1 |
| MAX_BLUR | 1525px | 3045px | bigger = "shouting" |
| MAX_SCALE_BOOST | 0.030.10 | 0.150.25 | additive at peak (0.08 ⇒ scale 1.08) |
| PULSE_HZ / AMP | 410 rad/s / 0.10.3 | — | multi-octave variation |
| MAX_POP_Z | 2060px | — | 3D variation |
| SCENE_DURATION | = `data-duration` | same | driver must end in sync with the scene's seek window |
## Critical Constraints
- **Timings monotonic, non-overlapping** — every entry's `end` < the next entry's `start`; overlapping windows make the envelope ambiguous.
- **Brand word window 1.52× a normal word** — the brand is the headline; let it sustain.
- **Driver ease stays `"none"`** — any other ease warps every word's envelope timing.
- **`text-shadow`, not `box-shadow`** — the glow must hug the GLYPH (speaking emphasis), not the inline-block rectangle.
- **One driver looping all words** — never one tween per word.
- **Commit to a style** — values between the default and karaoke columns yield awkward "half-loud" emphasis.
- **Climax dwell ≥1s** after the final word's emphasis — the last word IS the headline beat.
## See also
`3d-text-depth-layers` (depth on the active word at peak) · `sine-wave-loop` (idle breathe between emphasis moments) · `context-sensitive-cursor` (typewriter matching the ASR cadence) · `/media-use` for `hyperframes transcribe` and caption rendering.