什么是token.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>什么是 token · narration demo</title>
  6. <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  7. <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  8. <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  9. <style>
  10. 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; }
  11. #root { box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
  12. .scene-padding { padding: 120px; height: 100%; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; }
  13. </style>
  14. </head>
  15. <body>
  16. <div id="root"></div>
  17. <script type="text/babel">
  18. // ── timeline.json (inline) ─────────────────────────────────
  19. const TIMELINE = {
  20. "title": "什么是 token",
  21. "voice": null,
  22. "speed": 1,
  23. "gap": 0.4,
  24. "totalDuration": 23.808,
  25. "scenes": [
  26. {"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}]},
  27. {"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}]},
  28. {"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}]},
  29. {"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}]}
  30. ],
  31. "voiceover": "voiceover.mp3"
  32. };
  33. // ── narration_stage.jsx (inline) ───────────────────────────
  34. const NarrationStageLib = (() => {
  35. const NarrationContext = React.createContext({ time: 0, scene: null, sceneTime: 0, isCueTriggered: () => false, cueProgress: () => 0 });
  36. function NarrationStage({ timeline, audioSrc, width = 1920, height = 1080, background = '#0e0e0e', controls = true, children }) {
  37. const audioRef = React.useRef(null);
  38. const [time, setTime] = React.useState(0);
  39. const [playing, setPlaying] = React.useState(false);
  40. const recording = typeof window !== 'undefined' && window.__recording === true;
  41. React.useEffect(() => {
  42. if (typeof window === 'undefined') return;
  43. window.__totalDuration = timeline.totalDuration;
  44. window.__ready = true;
  45. }, [timeline.totalDuration]);
  46. React.useEffect(() => {
  47. let raf;
  48. const tick = () => {
  49. if (recording) {
  50. if (typeof window.__time === 'number') setTime(window.__time);
  51. } else if (audioRef.current && !audioRef.current.paused) {
  52. setTime(audioRef.current.currentTime);
  53. }
  54. raf = requestAnimationFrame(tick);
  55. };
  56. tick();
  57. return () => cancelAnimationFrame(raf);
  58. }, [recording]);
  59. const currentScene = React.useMemo(() => {
  60. if (!timeline.scenes) return null;
  61. for (let i = 0; i < timeline.scenes.length; i++) {
  62. const s = timeline.scenes[i];
  63. const next = timeline.scenes[i + 1];
  64. if (time >= s.start && (!next || time < next.start)) return s;
  65. }
  66. return timeline.scenes[0];
  67. }, [time, timeline.scenes]);
  68. const sceneTime = currentScene ? Math.max(0, time - currentScene.start) : 0;
  69. const allCues = React.useMemo(() => {
  70. const map = {};
  71. for (const s of timeline.scenes || []) for (const c of s.cues || []) map[c.id] = c;
  72. return map;
  73. }, [timeline.scenes]);
  74. const isCueTriggered = React.useCallback((cueId) => { const c = allCues[cueId]; return c ? time >= c.absoluteTime : false; }, [allCues, time]);
  75. 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]);
  76. const ctx = { time, scene: currentScene, sceneTime, isCueTriggered, cueProgress };
  77. const handlePlayPause = () => { if (!audioRef.current) return; if (audioRef.current.paused) { audioRef.current.play(); setPlaying(true); } else { audioRef.current.pause(); setPlaying(false); } };
  78. const handleSeek = (e) => { if (!audioRef.current) return; const t = parseFloat(e.target.value); audioRef.current.currentTime = t; setTime(t); };
  79. return (
  80. <NarrationContext.Provider value={ctx}>
  81. <div style={{ position: 'relative', width, height, background, overflow: 'hidden', color: '#fff', fontFamily: '-apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif' }}>
  82. {children}
  83. </div>
  84. {!recording && <audio ref={audioRef} src={audioSrc} preload="auto" onEnded={() => setPlaying(false)} />}
  85. {!recording && controls && (
  86. <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px', background: '#1a1a1a', color: '#ddd', fontFamily: 'monospace', fontSize: 13, width, boxSizing: 'border-box' }}>
  87. <button onClick={handlePlayPause} style={{ padding: '6px 14px', background: '#fff', color: '#000', border: 0, borderRadius: 4, cursor: 'pointer', fontWeight: 600 }}>
  88. {playing ? '❚❚ Pause' : '▶ Play'}
  89. </button>
  90. <input type="range" min={0} max={timeline.totalDuration} step={0.01} value={time} onChange={handleSeek} style={{ flex: 1 }} />
  91. <span style={{ minWidth: 110, textAlign: 'right' }}>{time.toFixed(2)} / {timeline.totalDuration.toFixed(2)}s</span>
  92. <span style={{ padding: '4px 10px', background: '#2a2a2a', borderRadius: 4, minWidth: 100, textAlign: 'center' }}>{currentScene ? currentScene.id : '—'}</span>
  93. </div>
  94. )}
  95. </NarrationContext.Provider>
  96. );
  97. }
  98. function Scene({ id, children, keepMounted = false }) {
  99. const { scene, sceneTime } = React.useContext(NarrationContext);
  100. const isActive = scene && scene.id === id;
  101. if (!isActive && !keepMounted) return null;
  102. const content = typeof children === 'function' ? children(sceneTime, scene) : children;
  103. return <div style={{ position: 'absolute', inset: 0, opacity: isActive ? 1 : 0, pointerEvents: isActive ? 'auto' : 'none', transition: keepMounted ? 'opacity 0.2s' : undefined }}>{content}</div>;
  104. }
  105. function Cue({ id, ramp = 0.5, children }) {
  106. const { isCueTriggered, cueProgress } = React.useContext(NarrationContext);
  107. return children(isCueTriggered(id), cueProgress(id, ramp));
  108. }
  109. return { NarrationStage, Scene, Cue };
  110. })();
  111. const { NarrationStage, Scene, Cue } = NarrationStageLib;
  112. // ── 视觉内容 ─────────────────────────────────────────────
  113. const App = () => (
  114. <NarrationStage timeline={TIMELINE} audioSrc="_narration_token/voiceover.mp3" width={1920} height={1080} background="#0a0a0a">
  115. {/* Scene 1: 大问号引入 */}
  116. <Scene id="intro">
  117. <div className="scene-padding" style={{ alignItems: 'center', justifyContent: 'center' }}>
  118. <Cue id="question">{(triggered, p) => (
  119. <div style={{ fontSize: 320, color: triggered ? '#ffd54a' : '#3a3a3a', fontWeight: 200, transition: 'color 0.4s', transform: `scale(${0.8 + p * 0.2})`, lineHeight: 1 }}>?</div>
  120. )}</Cue>
  121. <div style={{ fontSize: 56, color: '#aaa', marginTop: 60, letterSpacing: '0.05em', fontWeight: 300 }}>AI 是怎么理解我们的话的</div>
  122. </div>
  123. </Scene>
  124. {/* Scene 2: reveal 关键词 */}
  125. <Scene id="token-1">
  126. <div className="scene-padding" style={{ alignItems: 'center', justifyContent: 'center' }}>
  127. <div style={{ fontSize: 64, color: '#888', marginBottom: 80, fontWeight: 300 }}>它不认识汉字</div>
  128. <Cue id="reveal">{(triggered, p) => (
  129. <div style={{
  130. fontSize: 280, fontWeight: 700, color: '#ffd54a', letterSpacing: '0.05em',
  131. opacity: p, transform: `translateY(${(1 - p) * 40}px)`,
  132. fontFamily: 'monospace', textShadow: triggered ? '0 0 40px rgba(255, 213, 74, 0.4)' : 'none'
  133. }}>
  134. token
  135. </div>
  136. )}</Cue>
  137. </div>
  138. </Scene>
  139. {/* Scene 3: 拆字演示 */}
  140. <Scene id="token-2">
  141. <div className="scene-padding" style={{ alignItems: 'center', justifyContent: 'center' }}>
  142. <div style={{ fontSize: 48, color: '#aaa', marginBottom: 100, fontWeight: 300 }}>token = AI 的最小信息单位</div>
  143. <Cue id="split">{(triggered, p) => (
  144. <div style={{ display: 'flex', gap: triggered ? 80 : 8, transition: 'gap 0.6s cubic-bezier(0.16, 1, 0.3, 1)' }}>
  145. <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' }}>
  146. 人工
  147. </div>
  148. <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' }}>
  149. 智能
  150. </div>
  151. </div>
  152. )}</Cue>
  153. <div style={{ fontSize: 36, color: '#666', marginTop: 60, opacity: 0.6 }}>「人工智能」= 2 个 token</div>
  154. </div>
  155. </Scene>
  156. {/* Scene 4: 总结 */}
  157. <Scene id="ending">
  158. <div className="scene-padding" style={{ alignItems: 'center', justifyContent: 'center' }}>
  159. <Cue id="context">{(triggered, p) => (
  160. <>
  161. <div style={{ fontSize: 96, fontWeight: 700, letterSpacing: '0.02em', marginBottom: 40, color: '#fff', opacity: triggered ? 1 : 0.3, transition: 'opacity 0.5s' }}>
  162. <span style={{ color: '#ffd54a' }}>1,000,000</span> token
  163. </div>
  164. <div style={{ fontSize: 48, color: '#888', fontWeight: 300, opacity: p }}>
  165. ≈ AI 一次能记住的<span style={{ color: '#fff', fontWeight: 500 }}>「小块」数量</span>
  166. </div>
  167. </>
  168. )}</Cue>
  169. </div>
  170. </Scene>
  171. {/* 全局水印 */}
  172. <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 }}>
  173. Created by Huashu-Design
  174. </div>
  175. </NarrationStage>
  176. );
  177. ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  178. </script>
  179. </body>
  180. </html>