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