Files
2026-05-24 21:03:49 -04:00

202 lines
11 KiB
HTML
Raw Permalink 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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>什么是 token · narration demo</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
body { margin: 0; background: #0a0a0a; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; min-height: 100vh; display: flex; align-items: center; justify-content: center; flex-direction: column; }
#root { box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
.scene-padding { padding: 120px; height: 100%; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
// ── timeline.json (inline) ─────────────────────────────────
const TIMELINE = {
"title": "什么是 token",
"voice": null,
"speed": 1,
"gap": 0.4,
"totalDuration": 23.808,
"scenes": [
{"id":"intro","start":0,"end":4.368,"duration":4.368,"audio":"audio/intro.mp3","text":"你有没有想过,当我们和 AI 对话的时候AI 到底是怎么理解我们的话的呢。","cues":[{"id":"question","offset":1.08,"absoluteTime":1.08}]},
{"id":"token-1","start":4.768,"end":7.576,"duration":2.808,"audio":"audio/token-1.mp3","text":"答案是它根本不理解汉字,它只认识 token。","cues":[{"id":"reveal","offset":1.632,"absoluteTime":6.4}]},
{"id":"token-2","start":7.976,"end":16.808,"duration":8.832,"audio":"audio/token-2.mp3","text":"你可以把 token 理解成 AI 的最小信息单位。\n比如「人工智能」这四个字在 AI 眼里可能是两个 token人工智能。","cues":[{"id":"split","offset":5.4,"absoluteTime":13.376}]},
{"id":"ending","start":17.208,"end":23.664,"duration":6.456,"audio":"audio/ending.mp3","text":"所以下次看到「百万 token 上下文」这种说法,你就知道,它说的是 AI 一次能记住多少个这样的小块。","cues":[{"id":"context","offset":2.376,"absoluteTime":19.584}]}
],
"voiceover": "voiceover.mp3"
};
// ── narration_stage.jsx (inline) ───────────────────────────
const NarrationStageLib = (() => {
const NarrationContext = React.createContext({ time: 0, scene: null, sceneTime: 0, isCueTriggered: () => false, cueProgress: () => 0 });
function NarrationStage({ timeline, audioSrc, width = 1920, height = 1080, background = '#0e0e0e', controls = true, children }) {
const audioRef = React.useRef(null);
const [time, setTime] = React.useState(0);
const [playing, setPlaying] = React.useState(false);
const recording = typeof window !== 'undefined' && window.__recording === true;
React.useEffect(() => {
if (typeof window === 'undefined') return;
window.__totalDuration = timeline.totalDuration;
window.__ready = true;
}, [timeline.totalDuration]);
React.useEffect(() => {
let raf;
const tick = () => {
if (recording) {
if (typeof window.__time === 'number') setTime(window.__time);
} else if (audioRef.current && !audioRef.current.paused) {
setTime(audioRef.current.currentTime);
}
raf = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(raf);
}, [recording]);
const currentScene = React.useMemo(() => {
if (!timeline.scenes) return null;
for (let i = 0; i < timeline.scenes.length; i++) {
const s = timeline.scenes[i];
const next = timeline.scenes[i + 1];
if (time >= s.start && (!next || time < next.start)) return s;
}
return timeline.scenes[0];
}, [time, timeline.scenes]);
const sceneTime = currentScene ? Math.max(0, time - currentScene.start) : 0;
const allCues = React.useMemo(() => {
const map = {};
for (const s of timeline.scenes || []) for (const c of s.cues || []) map[c.id] = c;
return map;
}, [timeline.scenes]);
const isCueTriggered = React.useCallback((cueId) => { const c = allCues[cueId]; return c ? time >= c.absoluteTime : false; }, [allCues, time]);
const cueProgress = React.useCallback((cueId, ramp = 0.5) => { const c = allCues[cueId]; if (!c) return 0; const dt = time - c.absoluteTime; if (dt <= 0) return 0; if (dt >= ramp) return 1; return dt / ramp; }, [allCues, time]);
const ctx = { time, scene: currentScene, sceneTime, isCueTriggered, cueProgress };
const handlePlayPause = () => { if (!audioRef.current) return; if (audioRef.current.paused) { audioRef.current.play(); setPlaying(true); } else { audioRef.current.pause(); setPlaying(false); } };
const handleSeek = (e) => { if (!audioRef.current) return; const t = parseFloat(e.target.value); audioRef.current.currentTime = t; setTime(t); };
return (
<NarrationContext.Provider value={ctx}>
<div style={{ position: 'relative', width, height, background, overflow: 'hidden', color: '#fff', fontFamily: '-apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif' }}>
{children}
</div>
{!recording && <audio ref={audioRef} src={audioSrc} preload="auto" onEnded={() => setPlaying(false)} />}
{!recording && controls && (
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px', background: '#1a1a1a', color: '#ddd', fontFamily: 'monospace', fontSize: 13, width, boxSizing: 'border-box' }}>
<button onClick={handlePlayPause} style={{ padding: '6px 14px', background: '#fff', color: '#000', border: 0, borderRadius: 4, cursor: 'pointer', fontWeight: 600 }}>
{playing ? '❚❚ Pause' : '▶ Play'}
</button>
<input type="range" min={0} max={timeline.totalDuration} step={0.01} value={time} onChange={handleSeek} style={{ flex: 1 }} />
<span style={{ minWidth: 110, textAlign: 'right' }}>{time.toFixed(2)} / {timeline.totalDuration.toFixed(2)}s</span>
<span style={{ padding: '4px 10px', background: '#2a2a2a', borderRadius: 4, minWidth: 100, textAlign: 'center' }}>{currentScene ? currentScene.id : '—'}</span>
</div>
)}
</NarrationContext.Provider>
);
}
function Scene({ id, children, keepMounted = false }) {
const { scene, sceneTime } = React.useContext(NarrationContext);
const isActive = scene && scene.id === id;
if (!isActive && !keepMounted) return null;
const content = typeof children === 'function' ? children(sceneTime, scene) : children;
return <div style={{ position: 'absolute', inset: 0, opacity: isActive ? 1 : 0, pointerEvents: isActive ? 'auto' : 'none', transition: keepMounted ? 'opacity 0.2s' : undefined }}>{content}</div>;
}
function Cue({ id, ramp = 0.5, children }) {
const { isCueTriggered, cueProgress } = React.useContext(NarrationContext);
return children(isCueTriggered(id), cueProgress(id, ramp));
}
return { NarrationStage, Scene, Cue };
})();
const { NarrationStage, Scene, Cue } = NarrationStageLib;
// ── 视觉内容 ─────────────────────────────────────────────
const App = () => (
<NarrationStage timeline={TIMELINE} audioSrc="_narration_token/voiceover.mp3" width={1920} height={1080} background="#0a0a0a">
{/* Scene 1: 大问号引入 */}
<Scene id="intro">
<div className="scene-padding" style={{ alignItems: 'center', justifyContent: 'center' }}>
<Cue id="question">{(triggered, p) => (
<div style={{ fontSize: 320, color: triggered ? '#ffd54a' : '#3a3a3a', fontWeight: 200, transition: 'color 0.4s', transform: `scale(${0.8 + p * 0.2})`, lineHeight: 1 }}>?</div>
)}</Cue>
<div style={{ fontSize: 56, color: '#aaa', marginTop: 60, letterSpacing: '0.05em', fontWeight: 300 }}>AI 是怎么理解我们的话的</div>
</div>
</Scene>
{/* Scene 2: reveal 关键词 */}
<Scene id="token-1">
<div className="scene-padding" style={{ alignItems: 'center', justifyContent: 'center' }}>
<div style={{ fontSize: 64, color: '#888', marginBottom: 80, fontWeight: 300 }}>它不认识汉字</div>
<Cue id="reveal">{(triggered, p) => (
<div style={{
fontSize: 280, fontWeight: 700, color: '#ffd54a', letterSpacing: '0.05em',
opacity: p, transform: `translateY(${(1 - p) * 40}px)`,
fontFamily: 'monospace', textShadow: triggered ? '0 0 40px rgba(255, 213, 74, 0.4)' : 'none'
}}>
token
</div>
)}</Cue>
</div>
</Scene>
{/* Scene 3: 拆字演示 */}
<Scene id="token-2">
<div className="scene-padding" style={{ alignItems: 'center', justifyContent: 'center' }}>
<div style={{ fontSize: 48, color: '#aaa', marginBottom: 100, fontWeight: 300 }}>token = AI 的最小信息单位</div>
<Cue id="split">{(triggered, p) => (
<div style={{ display: 'flex', gap: triggered ? 80 : 8, transition: 'gap 0.6s cubic-bezier(0.16, 1, 0.3, 1)' }}>
<div style={{ fontSize: 200, fontWeight: 600, color: triggered ? '#ffd54a' : '#fff', padding: triggered ? '40px 60px' : '40px 20px', border: triggered ? '4px solid #ffd54a' : '4px solid transparent', borderRadius: 24, transition: 'all 0.6s cubic-bezier(0.16, 1, 0.3, 1)', background: triggered ? 'rgba(255, 213, 74, 0.05)' : 'transparent' }}>
人工
</div>
<div style={{ fontSize: 200, fontWeight: 600, color: triggered ? '#ffd54a' : '#fff', padding: triggered ? '40px 60px' : '40px 20px', border: triggered ? '4px solid #ffd54a' : '4px solid transparent', borderRadius: 24, transition: 'all 0.6s cubic-bezier(0.16, 1, 0.3, 1)', background: triggered ? 'rgba(255, 213, 74, 0.05)' : 'transparent' }}>
智能
</div>
</div>
)}</Cue>
<div style={{ fontSize: 36, color: '#666', marginTop: 60, opacity: 0.6 }}>人工智能= 2 token</div>
</div>
</Scene>
{/* Scene 4: 总结 */}
<Scene id="ending">
<div className="scene-padding" style={{ alignItems: 'center', justifyContent: 'center' }}>
<Cue id="context">{(triggered, p) => (
<>
<div style={{ fontSize: 96, fontWeight: 700, letterSpacing: '0.02em', marginBottom: 40, color: '#fff', opacity: triggered ? 1 : 0.3, transition: 'opacity 0.5s' }}>
<span style={{ color: '#ffd54a' }}>1,000,000</span> token
</div>
<div style={{ fontSize: 48, color: '#888', fontWeight: 300, opacity: p }}>
AI 一次能记住的<span style={{ color: '#fff', fontWeight: 500 }}>小块数量</span>
</div>
</>
)}</Cue>
</div>
</Scene>
{/* 全局水印 */}
<div style={{ position: 'absolute', bottom: 24, right: 32, fontSize: 11, color: 'rgba(255,255,255,0.35)', letterSpacing: '0.15em', fontFamily: 'monospace', pointerEvents: 'none', zIndex: 100 }}>
Created by Huashu-Design
</div>
</NarrationStage>
);
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>