| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- <!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>
|