#!/usr/bin/env python3 """Repair and validate Vietnamese text layout in the book's SVG diagrams. The Vietnamese labels are usually wider than their Chinese counterparts. This tool restores the original type sizes and wraps labels with SVG ``tspan`` elements. It deliberately does not use ``textLength`` because librsvg squeezes the glyphs, making the PDF hard to read. Usage: python3 fix_svg_text_layout.py --write python3 fix_svg_text_layout.py --check """ from __future__ import annotations import argparse import math from pathlib import Path import re import subprocess import sys import tempfile import xml.etree.ElementTree as ET SVG_NS = "http://www.w3.org/2000/svg" ET.register_namespace("", SVG_NS) NUM = re.compile(r"-?\d+(?:\.\d+)?") FONT_SIZE_DECL = re.compile(r"font-size\s*:\s*(-?\d+(?:\.\d+)?)", re.IGNORECASE) MIN_FONT = 10.0 LINE_HEIGHT = 1.18 # Undo lossy substitutions made by the previous fitter. Keeping this table # also makes --write able to repair SVGs that were already processed by it. RESTORE_TEXT = { '"Kết quả chưa đủ; tìm thêm"': '"Đang phân tích kết quả tìm kiếm... Thông tin chưa đầy đủ, cần tìm kiếm thêm"', "Lặp tiếp": "Tiếp tục chu kỳ", "Tìm mã → định vị bug → sửa tệp → chạy test → sửa lại → test đạt → xong": "Tìm kiếm mã → Xác định vị trí bug → Chỉnh sửa tệp → Chạy kiểm tra → " "Sửa lỗi → Chỉnh sửa lại → Kiểm tra đạt → Hoàn thành", "(5 vòng, 12 tool calls)": "(Lặp lại 5 lần, gọi công cụ 12 lần)", "③ Không gọi tool": "③ Trở về mà không gọi công cụ", "⑤ Quá giới hạn lỗi": "⑤ Số lỗi vượt quá giới hạn", "① Attention weight của query “How”": '① Trọng số chú ý của “How” đối với từng từ trong văn bản trước', "Query–Key score → softmax weight → weighted Value (chủ yếu “weather”)": "Điểm Query–Key → chuẩn hóa trọng số → tổng có trọng số của Value " "(chủ yếu tham chiếu đến “Thời tiết”)", "② Causal attention heatmap: chỉ nhìn token hiện tại và quá khứ": "② Bản đồ nhiệt chú ý nhân quả: mỗi từ chỉ nhìn thấy chính nó và văn bản trước đó", "Đậm hơn = chú ý cao; tam giác trên = token tương lai bị ẩn": "Ô càng đậm = chú ý càng cao; tam giác phía trên trống = không thấy từ chưa được tạo", "No status bar": "Không có thanh trạng thái", "With status bar": "Có thanh trạng thái", '"Negotiate Xfinity"': '"Liên hệ Xfinity để thương lượng giá"', "Result: 45m wait, no answer": "Kết quả: Đợi 45 phút, không kết nối", "Result: lots of search text...": "Kết quả: [Rất nhiều nội dung tìm kiếm...]", "Result: connected, $65/mo": "Kết quả: Đã kết nối, báo giá $65/tháng", "Result: confirmed $59/mo": "Kết quả: Xác nhận giảm giá xuống $59/tháng", '"Call again to follow up?"': '"Có thể gọi lại để nhắc họ không?"', "→ Model must scan context to count calls": "→ Mô hình phải quét toàn bộ ngữ cảnh để đếm số cuộc gọi", "Easy to miscount calls.": "Việc đếm số cuộc gọi rất dễ sai.", "[same trajectory]": "[Cùng nội dung trajectory]", "phone_call called 3 times (Xfinity: 3)": "phone_call đã được gọi 3 lần (Xfinity: 3)", "Constraint: limit reached (3/3) ✗": "Kiểm tra ràng buộc: Đã đạt giới hạn (3/3) ✗", "TODO: [✓] contact Xfinity [✓] confirm discount": "TODO: [✓] Liên hệ Xfinity [✓] Xác nhận giảm giá", "Time: 2025-09-14 10:30": "Thời gian hiện tại: 2025-09-14 10:30", "State: waiting for user confirmation": "Trạng thái hiện tại: Đang chờ người dùng xác nhận", "→ Model reads distilled state directly": "→ Mô hình đọc trực tiếp trạng thái đã cô đọng", "Follows constraints; no extra call": "Tuân thủ chính xác ràng buộc và không gọi thêm", "Tokens": "Số Token", "Compress": "Tỷ lệ nén", "Iter.": "Số lần lặp", "Result": "Kết quả", "Token usage comparison": "Trực quan hóa so sánh mức sử dụng Token", "individual summary": "tóm tắt riêng lẻ", "portfolio summary": "tóm tắt tổng hợp", "context-aware": "nhận biết ngữ cảnh", "aware + refs": "nhận biết + tham chiếu", "adaptive window": "cửa sổ thích ứng", "Context-aware compression: 77% fewer tokens, highest success, fewest iterations": "Nén theo ngữ cảnh: giảm 77% token, tỷ lệ thành công cao nhất, số lần lặp thấp nhất", "Key: include query intent and existing info in compression decisions": "Điểm mấu chốt: kết hợp ý định truy vấn và thông tin hiện có khi quyết định nén", '"Who to invite?"': '"Mời ai?"', "09:00 opens": "09:00 mở cửa", } # fig2-9 had previously been replaced wholesale with English labels. FIG2_9_TEXTS = [ "Cấp API (góc nhìn nhà phát triển)", "{", '"role"', ":", '"system"', ",", '"content"', ":", '"Bạn là một trợ lý"', "}", "{", '"role"', ":", '"user"', ",", '"content"', ":", '"Xin chào"', "}", "Cấp mô hình (sau Chat Template)", "<|im_start|>", "system", "Bạn là một trợ lý", "<|im_end|>", "<|im_start|>", "user", "Xin chào", "<|im_end|>", "<|im_start|>", "assistant", "(mô hình tạo nội dung từ đây)", ] def local(tag: str) -> str: return tag.rsplit("}", 1)[-1] def number(value, default=None): if value is None: return default match = NUM.search(str(value)) return float(match.group()) if match else default def fmt(value: float) -> str: return f"{value:.2f}".rstrip("0").rstrip(".") def text_nodes(root): return [node for node in root.iter() if local(node.tag) == "text"] def css_font_sizes(root): """Return ``.class`` → font-size mappings from SVG ``