/* =========================================================
   figures.jsx — static lecture figures (v1–v10)
   ---------------------------------------------------------
   Chapter notes carry a line like `@fig v1-blocking`, which
   pages.jsx replaces with <Figure name="v1-blocking" idx={n} />.
   Each figure is a dependency-free, theme-aware SVG using the
   shared primitives below and the site language (useL()).
   Keep in-SVG text to SHORT labels; put full sentences in the
   `cap` (an HTML figcaption the browser wraps). figures3.jsx
   defines <Figure> + the window export; figures4.jsx adds the
   per-module architecture diagrams.
   ========================================================= */

const FIGN = {};
const FTONE = { p: "var(--primary)", a: "var(--accent)", m: "var(--muted)", bad: "#c0453f", ok: "#2e9e6b", warn: "#d98a1f", n: "var(--surface-2)" };

function FigFrame({ w = 680, h = 220, cap, idx, children }) {
  const L = useL();
  return (
    <figure className="vo-fig">
      <svg className="vo-fig-svg" viewBox={`0 0 ${w} ${h}`} width="100%" preserveAspectRatio="xMidYMid meet" role="img">
        {children}
      </svg>
      {cap ? <figcaption>{idx ? <span className="fno">{L(`图 ${idx}`, `Fig. ${idx}`)}</span> : null}{cap}</figcaption> : null}
    </figure>
  );
}
function FArrow({ x1, y1, x2, y2, dash, c = "var(--muted)", wdt = 1.3 }) {
  const ang = Math.atan2(y2 - y1, x2 - x1), s = 5.5;
  const tip = `${x2},${y2} ${(x2 - s * Math.cos(ang - 0.42)).toFixed(1)},${(y2 - s * Math.sin(ang - 0.42)).toFixed(1)} ${(x2 - s * Math.cos(ang + 0.42)).toFixed(1)},${(y2 - s * Math.sin(ang + 0.42)).toFixed(1)}`;
  return <g><line x1={x1} y1={y1} x2={x2} y2={y2} stroke={c} strokeWidth={wdt} strokeDasharray={dash ? "4 3" : ""} /><polygon points={tip} fill={c} /></g>;
}
function FBox({ x, y, w, h, label, sub, tone = "n" }) {
  const solid = tone !== "n";
  const c = FTONE[tone] || FTONE.n;
  return (
    <g>
      <rect x={x} y={y} width={w} height={h} rx="6" fill={solid ? `color-mix(in srgb, ${c} 84%, transparent)` : "var(--surface-2)"} stroke={c} strokeWidth="1.2" />
      <text x={x + w / 2} y={y + h / 2 + (sub ? -3 : 1)} textAnchor="middle" dominantBaseline="middle" style={{ font: "600 11.5px var(--f-mono)", fill: solid ? "#fff" : "var(--ink)" }}>{label}</text>
      {sub ? <text x={x + w / 2} y={y + h / 2 + 11} textAnchor="middle" style={{ font: "500 8.5px var(--f-mono)", fill: solid ? "rgba(255,255,255,.85)" : "var(--muted)" }}>{sub}</text> : null}
    </g>
  );
}
function FT({ x, y, children, anchor = "middle", cls = "tm" }) {
  return <text x={x} y={y} textAnchor={anchor} className={cls}>{children}</text>;
}

/* ---------------- v1 · blocking, not queueing ---------------- */
FIGN["v1-blocking"] = function ({ idx }) {
  const L = useL();
  return (
    <FigFrame idx={idx} h={212} cap={L("网页的请求会排队,电话不会。前台正在忙的时候打进来的顾客,听到的是忙音或六声无人接,然后他挂掉打给隔壁——你甚至不知道这通电话存在过。这就是为什么门店客服的账要用 Erlang-B 呼损模型算,而不是用等待时间算。", "A web request queues; a phone call does not. A customer calling while the desk is busy hears a busy tone or six unanswered rings, hangs up and calls the shop next door — and you never learn the call existed. That is why a shop's service bill is computed with Erlang-B blocking rather than with waiting time.")}>
      <FT x={120} y={20} cls="tk">{L("网页 / IM:排队", "Web / IM: queued")}</FT>
      {[0, 1, 2, 3].map((i) => <FBox key={i} x={20 + i * 52} w={44} y={34} h={26} label={`#${i + 1}`} tone="p" />)}
      <FArrow x1={232} y1={47} x2={268} y2={47} />
      <FBox x={270} y={30} w={66} h={34} label={L("处理", "serve")} tone="ok" />
      <FT x={178} y={78} cls="tn">{L("等一会儿,但都会被处理", "they wait, and all get served")}</FT>

      <line x1={352} y1={14} x2={352} y2={196} stroke="var(--hairline-strong)" strokeDasharray="4 3" />

      <FT x={520} y={20} cls="tk">{L("电话:不排队", "Phone: no queue")}</FT>
      <FBox x={376} y={34} w={44} h={26} label="#1" tone="ok" />
      <FBox x={428} y={34} w={44} h={26} label="#2" tone="bad" />
      <FBox x={480} y={34} w={44} h={26} label="#3" tone="bad" />
      <FBox x={532} y={34} w={44} h={26} label="#4" tone="bad" />
      <FArrow x1={420} y1={47} x2={596} y2={47} c="#2e9e6b" />
      <FT x={614} y={50} anchor="end" cls="tp">✓</FT>
      <FArrow x1={450} y1={64} x2={450} y2={104} c="#c0453f" />
      <FArrow x1={502} y1={64} x2={502} y2={104} c="#c0453f" />
      <FArrow x1={554} y1={64} x2={554} y2={104} c="#c0453f" />
      <FBox x={392} y={108} w={224} h={30} label={L("忙音 → 打给隔壁", "busy tone → the shop next door")} tone="bad" />
      <FT x={504} y={158} cls="tk">{L("你看不到这些电话,它们不进任何报表", "you never see these calls; no report contains them")}</FT>
      <FT x={504} y={178} cls="tn">{L("呼损率 = Erlang-B(线路数, 话务强度)", "blocking = Erlang-B(channels, offered traffic)")}</FT>
    </FigFrame>
  );
};

/* ---------------- v2 · the triage ---------------- */
FIGN["v2-triage"] = function ({ idx }) {
  const L = useL();
  const rows = [
    { l: L("问价格 · 问时间地址 · 问空位", "price · hours · availability"), share: 51, tone: "ok", note: L("答案确定,交给 AI", "definite → AI") },
    { l: L("预约 · 改约 · 指定技师", "book · reschedule · therapist"), share: 30, tone: "p", note: L("写操作,AI + 幂等", "write → AI + key") },
    { l: L("会员卡 · 团购券 · 发票", "membership · vouchers · invoices"), share: 13, tone: "warn", note: L("规则复杂,先人工", "complex → human") },
    { l: L("投诉 · 身体状况 · 越界试探", "complaints · health · out-of-bounds"), share: 6, tone: "bad", note: L("需要共情或授权,必须人工", "human, always") },
  ];
  let y = 34;
  return (
    <FigFrame idx={idx} h={214} cap={L("把一个月的通话记录按占比和「需要什么能力」排开,结论非常稳定:前两类占了八成话量、几乎不需要判断力,正是 AI 的区间;最后一类只占百分之几,却要么需要共情、要么需要授权、要么需要一个人来承担责任——把它们硬塞给 AI,自助率的数字会上升,顾客却是被劝退而不是被解决。", "Sort a month of calls by share and by what capability they demand, and the picture is remarkably stable: the top two groups are eighty percent of volume and need almost no judgement — the AI's zone. The last group is a few percent and needs empathy, authority, or someone to own the outcome. Force it onto the machine and the self-service number rises while the customer is deflected rather than served.")}>
      {rows.map((r, i) => {
        const w = (r.share / 51) * 330;
        const el = (
          <g key={i}>
            <rect x={200} y={y} width={Math.max(6, w)} height={26} rx="4" fill={`color-mix(in srgb, ${FTONE[r.tone]} 80%, transparent)`} stroke={FTONE[r.tone]} />
            <text x={194} y={y + 17} textAnchor="end" style={{ font: "500 10.5px var(--f-body)", fill: "var(--ink)" }}>{r.l}</text>
            <text x={206 + Math.max(6, w)} y={y + 17} style={{ font: "600 10.5px var(--f-mono)", fill: "var(--ink)" }}>{`${r.share}%`}</text>
            <text x={556} y={y + 17} style={{ font: "500 10px var(--f-mono)", fill: "var(--muted)" }}>{r.note}</text>
          </g>
        );
        y += 40;
        return el;
      })}
      <FT x={340} y={20} cls="tk">{L("一个月通话记录的真实分布", "the real distribution in a month of calls")}</FT>
      <line x1={548} y1={26} x2={548} y2={186} stroke="var(--hairline-strong)" strokeDasharray="3 3" />
      <FT x={340} y={204} cls="tn">{L("占比越大 ≠ 越难;最难的恰恰在最下面那一条", "bigger share ≠ harder; the hardest work is the bottom row")}</FT>
    </FigFrame>
  );
};

/* ---------------- v3 · turns multiply ---------------- */
FIGN["v3-turns"] = function ({ idx }) {
  const L = useL();
  const steps = [L("问候", "greet"), L("意图", "intent"), L("日期", "date"), L("时间", "time"), L("项目", "service"), L("技师", "staff"), L("查空档", "look up"), L("确认", "confirm")];
  return (
    <FigFrame idx={idx} h={218} cap={L("每一轮 95% 的成功率听起来很高,但轮次是连乘的:八轮之后只剩 66%。这解释了为什么「少问一轮」比「模型再准两个点」值钱得多——前者只要改提示词和交互设计,后者要换更贵的模型。一句话多槽抽取(顾客说「明天下午三点两个人做肩颈」直接填满四个槽)和默认值兜底(不指定技师就派单)是最有效的两个手段。", "95% per turn sounds excellent, but turns multiply: eight of them leave 66%. This is why removing one question is worth far more than two points of model accuracy — the first is a prompt rewrite, the second a more expensive model. Multi-slot extraction (one sentence filling four slots) and sensible defaults (assign any available therapist) are the two most effective moves.")}>
      {steps.map((s, i) => {
        const x = 16 + i * 82;
        const p = Math.pow(0.95, i + 1);
        return (
          <g key={i}>
            <FBox x={x} y={40} w={70} h={30} label={s} tone={i < 2 ? "p" : i < 6 ? "n" : "a"} />
            {i < steps.length - 1 ? <FArrow x1={x + 70} y1={55} x2={x + 82} y2={55} /> : null}
            <text x={x + 35} y={88} textAnchor="middle" style={{ font: "600 9.5px var(--f-mono)", fill: p < 0.75 ? "#c0453f" : "var(--muted)" }}>{`${Math.round(p * 100)}%`}</text>
            <rect x={x + 8} y={96} width={54} height={Math.max(2, p * 56)} rx="2"
              fill={`color-mix(in srgb, ${p < 0.75 ? FTONE.bad : FTONE.p} 70%, transparent)`} stroke={p < 0.75 ? FTONE.bad : FTONE.p}
              transform={`translate(0, ${56 - Math.max(2, p * 56)})`} />
          </g>
        );
      })}
      <FT x={340} y={24} cls="tk">{L("每轮 95%,累计成功率往下掉", "95% per turn, and the running product falls")}</FT>
      <line x1={12} y1={152} x2={668} y2={152} stroke="var(--hairline-strong)" />
      <FT x={340} y={176} cls="tm">{L("0.95⁸ = 66% — 顾客不是被一个错误劝退的,是被八次机会里的任意一次", "0.95⁸ = 66% — not one mistake loses them, but any one of eight chances")}</FT>
      <FT x={340} y={198} cls="tn">{L("多槽抽取 + 默认值 → 8 轮压到 5 轮 → 77%", "multi-slot extraction + defaults → 8 turns to 5 → 77%")}</FT>
    </FigFrame>
  );
};

/* ---------------- v4 · Nyquist ---------------- */
FIGN["v4-nyquist"] = function ({ idx }) {
  const L = useL();
  const bands = Array.from({ length: 16 }, (_, i) => i * 500);
  return (
    <FigFrame idx={idx} h={218} cap={L("奈奎斯特定理:采样率的一半就是能记录的最高频率。8 kHz 的电话链路只能记到 4 kHz,而汉语里区分 s / sh / f / x 这些擦音的辨别能量大量分布在 4–8 kHz——所以「四」和「十」、「十四」和「四十」在电话里特别容易混。这不是模型不够好,是物理上限;对策是复述确认、数字热词,以及在能走宽带的渠道上别降到 8 kHz。", "Nyquist: half the sample rate is the highest frequency you can record. An 8 kHz phone link reaches only 4 kHz, while much of the energy that separates Mandarin fricatives s, sh, f and x sits between 4 and 8 kHz — which is why four and ten are so easily confused on a call. This is a physical ceiling, not a weak model. The remedies are read-back confirmation, digit biasing, and not dropping to 8 kHz on channels that can carry more.")}>
      {bands.map((f, i) => {
        const kept = f < 4000;
        const h = 14 + 58 * Math.exp(-Math.pow((f - 900) / 1700, 2)) + 22 * Math.exp(-Math.pow((f - 5200) / 1900, 2));
        return <rect key={i} x={40 + i * 38} y={122 - h} width={32} height={h} rx="2"
          fill={kept ? "color-mix(in srgb, var(--primary) 78%, transparent)" : "var(--surface-2)"}
          stroke={kept ? "var(--primary)" : "var(--hairline-strong)"} />;
      })}
      <line x1={36} y1={122} x2={652} y2={122} stroke="var(--hairline-strong)" />
      <line x1={40 + 8 * 38 - 4} y1={20} x2={40 + 8 * 38 - 4} y2={130} stroke="#c0453f" strokeDasharray="4 3" strokeWidth="1.6" />
      <FT x={40 + 8 * 38 + 2} y={32} anchor="start" cls="ta">{L("4 kHz:8 kHz 电话的天花板", "4 kHz: the 8 kHz phone ceiling")}</FT>
      <FT x={160} y={142} cls="tk">{L("元音与共振峰(保留)", "vowels and formants (kept)")}</FT>
      <FT x={500} y={142} cls="tn">{L("擦音辨别能量(丢失)", "fricative cues (lost)")}</FT>
      <FBox x={80} y={158} w={220} h={30} label={L("16 kHz · CER ≈ 3%", "16 kHz · CER ≈ 3%")} tone="ok" />
      <FBox x={380} y={158} w={220} h={30} label={L("8 kHz 电话 · CER ≈ 9%", "8 kHz phone · CER ≈ 9%")} tone="warn" />
      <FT x={340} y={206} cls="tn">{L("同一个模型,换一条链路就差三倍", "the same model, a factor of three apart on two links")}</FT>
    </FigFrame>
  );
};

/* ---------------- v5 · four ASR architectures ---------------- */
FIGN["v5-arch"] = function ({ idx }) {
  const L = useL();
  const cols = [
    { t: "CTC", tone: "warn", note: L("帧独立 · 可流式", "frame-independent · streams"), rows: [L("对齐自学", "learns alignment"), L("不建模输出依赖", "no output dependency"), L("读起来不通顺", "reads awkwardly")] },
    { t: "RNN-T", tone: "p", note: L("流式主力", "the streaming workhorse"), rows: [L("+ 预测网络", "+ prediction net"), L("条件于已输出文字", "conditions on emitted text"), L("延迟与准确兼顾", "balances both")] },
    { t: "Paraformer", tone: "a", note: L("非自回归 · 极快", "non-autoregressive · fast"), rows: [L("并行出整句", "whole utterance in parallel"), L("批量转写首选", "best for bulk transcription"), L("流式需改造", "streaming needs work")] },
    { t: "Whisper / AED", tone: "ok", note: L("全局注意力 · 最准", "global attention · most accurate"), rows: [L("看完整段再解码", "decodes after the segment"), L("鲁棒、多语种", "robust, multilingual"), L("天生不适合实时", "structurally not realtime")] },
  ];
  return (
    <FigFrame idx={idx} h={222} cap={L("四种架构其实在回答同一个问题:怎么把一串声学帧对齐到一串文字。CTC 用空白符号让模型自学对齐,简单但每帧独立解码;RNN-T 加了一个预测网络把已输出的文字也纳入条件,于是既能流式又有语言建模能力,成为今天实时语音的主力;Paraformer 一次并行吐出整句,快得多,适合批量转写;Whisper 这类注意力编解码器依赖整段音频的全局注意力,最准也最鲁棒,但结构上就不适合低延迟流式。", "The four architectures answer one question: how to align a stream of acoustic frames to a stream of characters. CTC introduces a blank symbol and lets the model learn alignment itself — simple, but each frame decodes independently. RNN-T adds a prediction network conditioning on what has been emitted, giving both streaming and language modelling, which is why it carries most realtime speech today. Paraformer emits the whole utterance in parallel, far faster, ideal for bulk transcription. Attention encoder-decoders like Whisper rely on global attention over a full segment: the most accurate and robust, and structurally unsuited to low latency.")}>
      {cols.map((c, i) => {
        const x = 16 + i * 168;
        return (
          <g key={i}>
            <FBox x={x} y={20} w={152} h={34} label={c.t} sub={c.note} tone={c.tone} />
            {c.rows.map((r, j) => (
              <g key={j}>
                <rect x={x} y={62 + j * 34} width={152} height={28} rx="5" fill="var(--surface-2)" stroke="var(--hairline-strong)" />
                <text x={x + 76} y={80 + j * 34} textAnchor="middle" style={{ font: "500 9.5px var(--f-body)", fill: "var(--ink)" }}>{r}</text>
              </g>
            ))}
          </g>
        );
      })}
      <line x1={16} y1={178} x2={664} y2={178} stroke="var(--hairline-strong)" />
      <FT x={92} y={196} cls="tn">{L("延迟低 ←", "lower latency ←")}</FT>
      <FT x={588} y={196} cls="tn">{L("→ 准确率高", "→ higher accuracy")}</FT>
      <FT x={340} y={212} cls="tm">{L("实时电话选 RNN-T;事后质检与批量转写选 Paraformer / Whisper", "RNN-T for live calls; Paraformer or Whisper for QA and bulk transcription")}</FT>
    </FigFrame>
  );
};

/* ---------------- v6 · endpointing ---------------- */
FIGN["v6-endpoint"] = function ({ idx }) {
  const L = useL();
  const speech = [[40, 70], [120, 52], [196, 44], [266, 86]];
  return (
    <FigFrame idx={idx} h={216} cap={L("机器判断「你说完了没有」,唯一可靠的信号是停顿。阈值定短(300 ms),顾客说到「明天下午……嗯……三点」的自然停顿就被切断,机器抢话;定长(1200 ms),每一轮都白等近一秒,十轮就是十秒沉默。真实的人类停顿分布有长尾,所以这是概率问题而不是常数,最优区间通常落在 600–800 ms。语义端点检测——让模型判断这句话在意图上完整没有——能让两条曲线同时往下走。", "The only reliable signal that you have finished is silence. Set the threshold short (300 ms) and the natural hesitation in tomorrow afternoon… um… three o'clock is cut off and the machine talks over the caller. Set it long (1200 ms) and every turn wastes nearly a second, ten seconds across ten turns. Real pause distributions have a long tail, so this is a probability problem rather than a constant, with the usable region typically 600–800 ms. Semantic endpointing — letting a model judge whether the utterance is intentionally complete — pulls both curves down at once.")}>
      {speech.map(([x, w], i) => <rect key={i} x={x} y={40} width={w} height={26} rx="3" fill="color-mix(in srgb, var(--primary) 78%, transparent)" stroke="var(--primary)" />)}
      <FT x={100} y={30} cls="tn">{L("「明天下午」", "tomorrow afternoon")}</FT>
      <FT x={222} y={30} cls="tn">{L("「嗯……」", "um…")}</FT>
      <FT x={310} y={30} cls="tn">{L("「三点」", "three o'clock")}</FT>
      <FT x={186} y={84} cls="tn">{L("420 ms 思考停顿", "420 ms thinking pause")}</FT>
      <line x1={172} y1={38} x2={172} y2={76} stroke="#c0453f" strokeDasharray="3 2" />
      <line x1={196} y1={38} x2={196} y2={76} stroke="#c0453f" strokeDasharray="3 2" />

      <FBox x={392} y={34} w={126} h={38} label={L("阈值 300 ms", "threshold 300 ms")} sub={L("在停顿处被切断", "cut at the pause")} tone="bad" />
      <FT x={455} y={88} cls="tn">{L("机器抢话 → 顾客挂断", "talks over → they hang up")}</FT>
      <FBox x={530} y={34} w={126} h={38} label={L("阈值 1200 ms", "threshold 1200 ms")} sub={L("每轮白等一秒", "a wasted second per turn")} tone="warn" />
      <FT x={593} y={88} cls="tn">{L("呆滞 → 顾客失去耐心", "sluggish → they lose patience")}</FT>

      <rect x={392} y={110} width={264} height={40} rx="6" fill="color-mix(in srgb, #2e9e6b 20%, transparent)" stroke="#2e9e6b" />
      <FT x={524} y={128} cls="tk">{L("600–800 ms:可用区间", "600–800 ms: the usable region")}</FT>
      <FT x={524} y={143} cls="tn">{L("+ 语义端点检测 → 两端一起改善", "+ semantic endpointing improves both ends")}</FT>
      <FT x={340} y={186} cls="tm">{L("这一个参数对体验的影响,超过模型准确率", "this one parameter moves perceived quality more than model accuracy does")}</FT>
      <FT x={340} y={206} cls="tn">{L("能量 VAD → WebRTC VAD → 神经 VAD(Silero)→ 语义端点", "energy VAD → WebRTC VAD → neural VAD (Silero) → semantic endpointing")}</FT>
    </FigFrame>
  );
};

/* ---------------- v7 · slot accuracy ---------------- */
FIGN["v7-slot"] = function ({ idx }) {
  const L = useL();
  const rows = [
    { l: L("整段转写 CER", "transcript CER"), v: 0.95, txt: "95%", tone: "ok" },
    { l: L("技师姓名(3 字)", "therapist name (3 chars)"), v: Math.pow(0.95, 3), txt: "86%", tone: "p" },
    { l: L("日期 + 时间(6 字)", "date + time (6 chars)"), v: Math.pow(0.95, 6), txt: "74%", tone: "warn" },
    { l: L("11 位手机号", "11-digit phone number"), v: Math.pow(0.95, 11), txt: "57%", tone: "bad" },
  ];
  return (
    <FigFrame idx={idx} h={212} cap={L("字错率 5% 听起来很不错,但业务要的不是「平均对多少」,而是「关键槽位有没有全对」。一个 11 位手机号每一位都要对,全对的概率是 0.95 的 11 次方,大约 57%——近一半顾客要把号码再念一遍。整段 CER 是给模型看的,槽位准确率才是给生意看的。补救有三层:热词与语言模型偏置抬高专有名词的先验、逆文本规范化把「幺三八」还原成数字串、以及在关键槽位上复述确认给你第二次机会。", "A 5% character error rate sounds fine, but the business does not care about the average — it cares whether the critical slot is entirely correct. Every one of eleven digits must land, so a phone number is fully right with probability 0.95 to the eleventh, about 57%: nearly half your customers repeat it. Transcript CER is a number for the model; slot accuracy is the number for the business. The remedies come in three layers: hotword and language-model biasing to raise the prior on proper nouns, inverse text normalisation to recover a digit string, and read-back confirmation to buy a second attempt.")}>
      {rows.map((r, i) => {
        const y = 34 + i * 36;
        return (
          <g key={i}>
            <text x={214} y={y + 17} textAnchor="end" style={{ font: "500 10.5px var(--f-body)", fill: "var(--ink)" }}>{r.l}</text>
            <rect x={222} y={y} width={360} height={24} rx="4" fill="var(--surface-2)" stroke="var(--hairline-strong)" />
            <rect x={222} y={y} width={Math.max(4, r.v * 360)} height={24} rx="4" fill={`color-mix(in srgb, ${FTONE[r.tone]} 80%, transparent)`} stroke={FTONE[r.tone]} />
            <text x={592} y={y + 17} style={{ font: "600 11px var(--f-mono)", fill: "var(--ink)" }}>{r.txt}</text>
          </g>
        );
      })}
      <FT x={340} y={22} cls="tk">{L("同一个 5% 字错率下,各类槽位一次全对的概率", "at one and the same 5% character error, the chance each slot is wholly right")}</FT>
      <FT x={340} y={190} cls="tm">{L("0.95¹¹ = 57%:模型没变,难度却翻了倍", "0.95¹¹ = 57%: the model did not change, the difficulty did")}</FT>
      <FT x={340} y={206} cls="tn">{L("热词偏置 + 逆文本规范化 + 关键槽位复述确认", "hotword biasing + inverse text normalisation + read-back confirmation")}</FT>
    </FigFrame>
  );
};

/* ---------------- v8 · the TTS pipeline ---------------- */
FIGN["v8-pipeline"] = function ({ idx }) {
  const L = useL();
  return (
    <FigFrame idx={idx} h={214} cap={L("神经语音合成的两段式:声学模型把规范化后的文本变成梅尔频谱(Tacotron 2 自回归带注意力,FastSpeech 2 非自回归并行且更稳),声码器再把频谱还原成波形——这一步最吃算力,WaveNet 太慢,HiFi-GAN 是今天的实用解。端到端模型(VITS、CosyVoice 系列)把两段合一,还顺带解决了零样本音色复刻。实时率 RTF 是工程上最重要的数字:GPU 上好的模型能到 0.05,也就是二十倍实时。", "The neural two-stage pipeline: an acoustic model turns normalised text into a mel spectrogram (Tacotron 2 autoregressive with attention; FastSpeech 2 non-autoregressive, parallel and more stable), then a vocoder turns the spectrogram back into a waveform — the most compute-hungry step, where WaveNet was too slow and HiFi-GAN is today's practical answer. End-to-end models (VITS, the CosyVoice family) fuse both stages and add zero-shot voice cloning. Real-time factor is the number that matters in engineering: a good model on a GPU reaches 0.05, twenty times realtime.")}>
      <FBox x={18} y={52} w={104} h={42} label={L("文本", "text")} sub={L("TN + SSML", "TN + SSML")} tone="n" />
      <FArrow x1={122} y1={73} x2={150} y2={73} />
      <FBox x={152} y={52} w={130} h={42} label={L("声学模型", "acoustic model")} sub="Tacotron2 / FastSpeech2" tone="p" />
      <FArrow x1={282} y1={73} x2={310} y2={73} />
      <FBox x={312} y={52} w={120} h={42} label={L("梅尔频谱", "mel spectrogram")} tone="n" />
      <FArrow x1={432} y1={73} x2={460} y2={73} />
      <FBox x={462} y={52} w={116} h={42} label={L("声码器", "vocoder")} sub="HiFi-GAN" tone="a" />
      <FArrow x1={578} y1={73} x2={606} y2={73} />
      <FBox x={608} y={52} w={54} h={42} label={L("波形", "wave")} tone="ok" />
      <FT x={520} y={110} cls="tn">{L("算力大头在这一步", "most of the compute lives here")}</FT>

      <rect x={152} y={128} width={426} height={38} rx="7" fill="none" stroke="var(--accent)" strokeDasharray="5 3" />
      <FT x={365} y={152} cls="ta">{L("端到端:VITS / CosyVoice —— 一段式,支持零样本音色复刻", "end-to-end: VITS / CosyVoice — one stage, zero-shot voice cloning")}</FT>
      <FT x={340} y={190} cls="tm">{L("MOS 3.4(参数)→ 4.4(神经)→ 4.7(真人)", "MOS 3.4 (parametric) → 4.4 (neural) → 4.7 (human)")}</FT>
      <FT x={340} y={206} cls="tn">{L("RTF 0.05 = 20 倍实时;RTF 0.5 = 两路并发就开始排队", "RTF 0.05 = 20× realtime; RTF 0.5 = two concurrent sessions already queue")}</FT>
    </FigFrame>
  );
};

/* ---------------- v9 · text normalisation ---------------- */
FIGN["v9-tn"] = function ({ idx }) {
  const L = useL();
  const rows = [
    { raw: "¥138", bad: L("人民币符号一百三十八", "currency-symbol one-three-eight"), good: L("一百三十八元", "one hundred and thirty-eight yuan") },
    { raw: "13:30", bad: L("十三比三十", "thirteen colon thirty"), good: L("下午一点半", "half past one in the afternoon") },
    { raw: "13800135768", bad: L("一百三十八亿……", "thirteen billion eight hundred…"), good: L("幺三八 零零幺 三五七六八", "one-three-eight · pause · digits") },
    { raw: "B座2F", bad: L("B 座二F", "B block two F"), good: L("B 座二楼", "Block B, second floor") },
  ];
  return (
    <FigFrame idx={idx} h={220} cap={L("顾客说「一听就是机器人」,八成不是音质问题,是读法问题。业务系统吐出来的文本里全是雷区:金额、时间、电话号码、楼层、多音字。解决分两层——文本规范化(TN)在进入合成器之前把它们按口语读法展开,这是规则和词典的活儿,不该指望模型自己猜;SSML 再补上停顿、语速与「按电话号码读」这类朗读方式。这一整层优化没有任何推理成本。", "When a customer says it sounds like a robot, four times out of five the problem is reading rather than audio. What the business system emits is full of hazards: amounts, times, phone numbers, floors and heteronyms. The fix has two layers — text normalisation expands them into how they are actually said before synthesis, a job for rules and dictionaries rather than guesswork; then SSML adds pauses, rate and reading modes such as speak-as-telephone. None of this layer costs any inference.")}>
      <FT x={92} y={24} cls="tk">{L("原始文本", "source text")}</FT>
      <FT x={290} y={24} cls="tn">{L("裸文本直接合成", "raw into the synthesiser")}</FT>
      <FT x={530} y={24} cls="tp">{L("规范化之后", "after normalisation")}</FT>
      {rows.map((r, i) => {
        const y = 38 + i * 38;
        return (
          <g key={i}>
            <FBox x={24} y={y} w={136} h={28} label={r.raw} tone="n" />
            <FArrow x1={160} y1={y + 14} x2={186} y2={y + 14} c="#c0453f" />
            <rect x={188} y={y} width={204} height={28} rx="5" fill="color-mix(in srgb, #c0453f 14%, transparent)" stroke="#c0453f" />
            <text x={290} y={y + 18} textAnchor="middle" style={{ font: "500 9.5px var(--f-body)", fill: "var(--ink)" }}>{r.bad}</text>
            <FArrow x1={396} y1={y + 14} x2={422} y2={y + 14} c="#2e9e6b" />
            <rect x={424} y={y} width={232} height={28} rx="5" fill="color-mix(in srgb, #2e9e6b 16%, transparent)" stroke="#2e9e6b" />
            <text x={540} y={y + 18} textAnchor="middle" style={{ font: "500 9.5px var(--f-body)", fill: "var(--ink)" }}>{r.good}</text>
          </g>
        );
      })}
      <FT x={340} y={208} cls="tm">{L("TN 是规则与词典,SSML 是标记语言 —— 两者都不花推理成本", "TN is rules and dictionaries, SSML is markup — neither costs inference")}</FT>
    </FigFrame>
  );
};

/* ---------------- v10 · cloning and the gate ---------------- */
FIGN["v10-clone"] = function ({ idx }) {
  const L = useL();
  const gates = [L("本人书面授权", "written authorisation"), L("用途与期限限定", "scope and term"), L("可撤回", "revocable"), L("合成内容标识", "labelled output"), L("样本销毁约定", "sample destruction"), L("离职后下线", "retire on departure")];
  return (
    <FigFrame idx={idx} h={218} cap={L("技术上,三到十秒参考音频就能零样本复刻一个人的声音,相似度大约七成;半小时干净录音微调可以到九成以上,连口头禅和停顿习惯都学得像。正因为太容易,门槛被放在了法律一侧:《民法典》第 1023 条把自然人的声音参照肖像权保护,未经本人同意不得使用,合成内容还要按规定标识。实践中最容易出事的是离职——一位技师走了,他的声音还在给你的门店接电话。", "Technically, three to ten seconds of reference audio clones a voice zero-shot at roughly seventy percent similarity; half an hour of clean recording fine-tuned exceeds ninety, catching verbal tics and pause habits. Because it is that easy, the barrier sits on the legal side: China's Civil Code article 1023 protects a natural person's voice by reference to portrait rights, so it may not be used without consent, and synthetic output must be labelled. The commonest accident in practice is departure — a therapist leaves and their voice still answers your phone.")}>
      <FT x={168} y={22} cls="tk">{L("技术曲线:越容易,风险越大", "technical curve: easier every year")}</FT>
      <line x1={40} y1={124} x2={310} y2={124} stroke="var(--hairline-strong)" />
      <line x1={40} y1={34} x2={40} y2={124} stroke="var(--hairline-strong)" />
      <path d="M40,110 C90,84 140,66 190,58 C240,52 280,48 306,46" fill="none" stroke="var(--primary)" strokeWidth="2" />
      <circle cx={72} cy={98} r="3.5" fill="var(--accent)" />
      <FT x={92} y={92} anchor="start" cls="tn">{L("3 秒 · 约 70%", "3 s · ~70%")}</FT>
      <circle cx={244} cy={50} r="3.5" fill="var(--accent)" />
      <FT x={240} y={42} anchor="end" cls="tn">{L("30 分钟微调 · >90%", "30 min fine-tune · >90%")}</FT>
      <FT x={175} y={140} cls="tn">{L("参考音频时长(对数)", "reference audio (log scale)")}</FT>

      <line x1={336} y1={20} x2={336} y2={196} stroke="var(--hairline-strong)" strokeDasharray="4 3" />
      <FT x={500} y={22} cls="ta">{L("合规闸门:六项全过才可上线", "the gate: all six or it does not ship")}</FT>
      {gates.map((g, i) => (
        <g key={i}>
          <rect x={362} y={34 + i * 25} width={286} height={21} rx="4" fill="var(--surface-2)" stroke="var(--hairline-strong)" />
          <text x={372} y={49 + i * 25} style={{ font: "500 10px var(--f-body)", fill: "var(--ink)" }}>{`${i + 1}. ${g}`}</text>
        </g>
      ))}
      <FT x={340} y={210} cls="tm">{L("相似度再高,这张表没勾满就不能上线", "however high the similarity, an incomplete table does not ship")}</FT>
    </FigFrame>
  );
};
