[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
---
|
||||
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 1–2 words are bright (spoken + lingering rest) and the rest is dim. Use for short phrases (5–10 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.1–0.25s | same | must be < the shortest word's window or it never reaches 1 |
|
||||
| RELEASE | 0.2–0.5s | same | decay to rest |
|
||||
| REST_LEVEL | 0.15–0.4 | 0.05–0.2 | > 0 (breadcrumb), < 1 |
|
||||
| MAX_BLUR | 15–25px | 30–45px | bigger = "shouting" |
|
||||
| MAX_SCALE_BOOST | 0.03–0.10 | 0.15–0.25 | additive at peak (0.08 ⇒ scale 1.08) |
|
||||
| PULSE_HZ / AMP | 4–10 rad/s / 0.1–0.3 | — | multi-octave variation |
|
||||
| MAX_POP_Z | 20–60px | — | 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.5–2× 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.
|
||||
Reference in New Issue
Block a user