{"repo": "boangs/rmkit", "files": {"README.md": {"8a5c3038406ec2b7d235ce776414bb3a11eb9b72": {"t": 1785674233.955879, "n": 6306, "h": "5b644a06e1a2f20a", "c": "# rmkit-cn\n\nreMarkable 平板的中文化、AI、IME 与扩展工具集。\n\n通过 [xovi](https://github.com/asivery/xovi) 的 `LD_PRELOAD` + qmldiff 机制注入,\n不修改 ROM、不破坏 OTA、与系统 A/B 分区机制兼容。\n\n---\n\n## 功能\n\n- 🇨🇳 **系统中文化**:UI 文字翻译(设置 / 文件夹 / 工具栏)+ 中文键盘\n- ⌨️ **拼音输入法**:手写键盘 → 拼音浮动候选栏,FST 词库 (rime-frost)\n- 🤖 **文本 AI**:选中文字 → 润色 / 翻译 / 总结 / 问答(OpenAI 兼容)\n- ✍️ **手写 AI**:选中手写笔迹 → 视觉识别 → AI 回答 → 文字插入 OR 模拟笔迹回写\n- 📥 **手机扫码上传**:reMarkable 网页二维码 → 手机推送 PDF/字体/壁纸 → 设备自动应用\n- 🎨 **高级面板**:字体管理、壁纸切换、自定义启动器、AI 配置\n- 🎮 **小游戏**:象棋、五子棋、华容道、跳棋\n- 📚 **KOReader 启动器**:通过 [appload](https://github.com/asivery/rm-appload) 集成\n\n---\n\n## 设备支持\n\n| 代号 | 机型 | 架构 | 验证状态 |\n|---|---|---|---|\n| `rmpp-chiappa` | **reMarkable Paper Pro Move** | aarch64 | ✅ 主力测试 |\n| `rmpp-ferrari` | reMarkable Paper Pro | aarch64 | ⚠️ 同 aarch64 路径,未单独验证 |\n| `rm2` | reMarkable 2 | armv7l | ⚠️ 代码支持,未近期实测 |\n\n所有功能依赖 [xovi](https://github.com/asivery/xovi)(已自动检测安装)。\n\n---\n\n## 安装方式\n\n**前置条件**:\n1. reMarkable 设备已开启 Developer Mode(**注意:这会清空设备所有数据**)\n - Settings → General → About → Copyrights → 最底部记录 SSH 密码\n - Settings → General → Software → Enable Developer mode\n2. 设备已 USB-C 连接电脑,能 `ssh root@10.11.99.1`\n3. **建议先跑一次 `ssh-copy-id root@10.11.99.1`**(输入上一步看到的密码)。脚本内部会发起 10+ 次 ssh/scp 调用,没配公钥的话每次都要重新输密码,体验很糟\n\n### 推荐:下载 Release 完整包(无需 git / Go 工具链)\n\n```bash\ncurl -fLO https://github.com/boangs/rmkit/releases/latest/download/rmkit-cn-v1.1.1.tar.gz\ntar -xzf rmkit-cn-v1.1.1.tar.gz\ncd rmkit-cn-v1.1.1\nbash installer/install.sh\n```\n\n完整包 (~77MB) 包含 install.sh、所有架构预编译产物 (`dist/`)、xovi 依赖 (`vendor/`)。\n\n### 开发者:git clone\n\n```bash\ngit clone https://github.com/boangs/rmkit\ncd rmkit\nbash installer/install.sh\n```\n\n`dist/` 在 .gitignore 里 (80MB 二进制不入 git)。install.sh 检测到缺失时自动从最新 Release 下载 `dist.tar.gz` (~34MB) 补齐。\n\n`install.sh` 自动完成 7 步:\n1. 检测设备架构 + 固件版本\n2. 检查 xovi(缺失时自动解压 `vendor/xovi/xovi-{arch}.tar.gz` 部署)\n3. 编译 `qmd-src/*.qmd` 到对应固件 hashtab\n4. tar 流式推送 31MB payload(含 IME / AI / 字体 / 高级面板)\n5. 写入 systemd unit + xochitl drop-in(**双写 ext4 lower 持久化**,含 wants symlink)\n6. 第一次启动:临时 `LD_PRELOAD xochitl` 生成 hashtab → 设备端在线编译 .qmd\n7. `systemctl restart xochitl` 让 LD_PRELOAD 立即生效\n\n整个过程 **0 砖机**(详见下文砖机修复历史)。\n\n安装完成后几秒内可用:\n- 任意输入框点击 → 切换 \"中文\" 布局 → 拼音输入\n- 选中文字 → \"AI\" 按钮 → 选操作(润色/翻译/总结/问答)\n- 选中手写笔迹 → \"AI\" 按钮 → 同上\n- Settings 顶部多了 \"高级\" 入口(字体、壁纸、AI 配置)\n\n**OTA 升级后**:reMarkable 固件升级会重刷 rootfs,需要重跑一次 `bash installer/install.sh`。\n\n### 卸载\n\n```bash\nbash installer/install.sh --uninstall\n```\n\n---\n\n## AI 功能配置\n\n安装完成后默认 AI 配置为空。需要配置 OpenAI 兼容服务(dashscope / 千问 / OpenAI 等):\n\n**方法 1:扫码 Web UI(最简单)**\n\n设备 Settings → 高级 → 显示二维码 → 手机扫码 → 网页填入:\n- API URL:如 `https://dashscope.aliyuncs.com/compatible-mode/v1`\n- API Key:`sk-...`\n- Model:`qwen3.6-plus`(或你的 vision 兼容模型)\n\n**方法 2:SSH 直接改**\n\n```bash\nssh root@10.11.99.1 'cat > /home/root/rmkit-cn/upload-server/ai-config.json' < must be preserved verbatim.\n\"\"\"\n\nimport argparse\nimport concurrent.futures\nimport json\nimport os\nimport re\nimport sys\nimport time\nimport urllib.request\nimport urllib.error\nfrom xml.etree import ElementTree as ET\n\n\nDEFAULT_URL = \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\"\n\nSYSTEM_PROMPT = \"\"\"You are a professional zh-CN (Simplified Chinese) UI translator for a reMarkable e-ink tablet.\n\nRules:\n- Translate the \"source\" text to natural, concise Simplified Chinese suitable for a consumer UI.\n- Keep placeholders like %1 %2 %n %L1 EXACTLY as-is (position may be adjusted to match Chinese grammar).\n- Keep HTML tags like
verbatim, including attributes.\n- Keep URLs, email-like tokens, and code identifiers unchanged.\n- If source is only placeholders or punctuation (e.g. \"%1\", \"99+\"), return it unchanged.\n- Do NOT translate brand names: reMarkable, Google, Dropbox, OneDrive, Wi-Fi.\n- Match typical iOS/Android Chinese UI conventions (e.g. \"Back\" -> \"返回\", \"Settings\" -> \"设置\", \"Cancel\" -> \"取消\", \"Done\" -> \"完成\").\n- Use the \"context\" as a hint for disambiguation; do not include it in your output.\n- Return ONLY a single JSON object mapping each id to its Chinese translation — no prose, no code fences.\"\"\"\n\n\ndef load_ts(path):\n tree = ET.parse(path)\n root = tree.getroot()\n root.set(\"language\", \"zh_CN\")\n items = []\n for ctx in root.findall(\"context\"):\n ctx_name = ctx.findtext(\"name\") or \"\"\n for msg in ctx.findall(\"message\"):\n src_el = msg.find(\"source\")\n tr_el = msg.find(\"translation\")\n if src_el is None or tr_el is None:\n continue\n src = src_el.text or \"\"\n if tr_el.get(\"type\") == \"obsolete\" or tr_el.get(\"type\") == \"vanished\":\n continue\n items.append({\n \"ctx\": ctx_name,\n \"source\": src,\n \"tr_el\": tr_el,\n \"comment\": msg.findtext(\"comment\") or \"\",\n \"extracomment\": msg.findtext(\"extracomment\") or \"\",\n })\n return tree, root, items\n\n\nSKIP_RE = re.compile(r\"^[\\s%0-9+\\-:/.,()\\[\\]]*$\")\n\n\ndef should_skip(src):\n if not src.strip():\n return True\n if SKIP_RE.match(src):\n return True\n return False\n\n\ndef call_api(url, api_key, model, payload):\n body = json.dumps(payload).encode(\"utf-8\")\n req = urllib.request.Request(\n url,\n data=body,\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n },\n method=\"POST\",\n )\n with urllib.request.urlopen(req, timeout=120) as resp:\n return json.loads(resp.read())\n\n\ndef translate_batch(url, api_key, model, batch):\n payload_items = {}\n for i, it in enumerate(batch):\n entry = {\"source\": it[\"source\"], \"context\": it[\"ctx\"]}\n if it[\"comment\"]:\n entry[\"comment\"] = it[\"comment\"]\n if it[\"extracomment\"]:\n entry[\"extracomment\"] = it[\"extracomment\"]\n payload_items[str(i)] = entry\n user_prompt = (\n \"Translate these UI strings to Simplified Chinese. Return a single JSON object.\\n\\n\"\n + json.dumps(payload_items, ensure_ascii=False, indent=2)\n )\n body = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": user_prompt},\n ],\n \"response_format\": {\"type\": \"json_object\"},\n \"temperature\": 0.2,\n }\n\n last_err = None\n for attempt in range(4):\n try:\n result = call_api(url, api_key, model, body)\n content = result[\"choices\"][0][\"message\"][\"content\"]\n data = json.loads(content)\n return {int(k): v for k, v in data.items()}\n except (urllib.error.HTTPError, urllib.error.URLError, KeyError, json.JSONDecodeError) as e:\n last_err = e\n time.sleep(2 ** attempt)\n raise RuntimeError(f\"Batch failed after retries: {last_err}\")\n\n\ndef main():\n ap = argparse.ArgumentParser()\n ap.add_argument(\"input\")\n ap.add_argument(\"output\")\n ap.add_argument(\"--model\", default=\"qwen3.6-plus\")\n ap.add_argument(\"--url\", default=DEFAULT_URL)\n ap.add_argument(\"--batch\", type=int, default=30)\n ap.add_argument(\"--concurrency\", type=int, default=5)\n args = ap.parse_args()\n\n api_key = os.environ.get(\"DASHSCOPE_API_KEY\")\n if not api_key:\n print(\"DASHSCOPE_API_KEY env var required\", file=sys.stderr)\n sys.exit(1)\n\n tree, root, items = load_ts(args.input)\n total = len(items)\n to_translate = []\n already = 0\n for idx, it in enumerate(items):\n if it[\"tr_el\"].text: # already translated — leave it\n already += 1\n continue\n if should_skip(it[\"source\"]):\n it[\"tr_el\"].text = it[\"source\"]\n else:\n to_translate.append((idx, it))\n print(f\"already translated: {already}\", file=sys.stderr)\n print(f\"total messages: {total}, skipping trivial: {total - len(to_translate)}, \"\n f\"to translate: {len(to_translate)}\", file=sys.stderr)\n\n batches = []\n for i in range(0, len(to_translate), args.batch):\n batches.append(to_translate[i:i + args.batch])\n print(f\"batches: {len(batches)} (size {args.batch}, concurrency {args.concurrency})\",\n file=sys.stderr)\n\n done = 0\n with concurrent.futures.ThreadPoolExecutor(max_workers=args.concurrency) as ex:\n futures = {\n ex.submit(translate_batch, args.url, api_key, args.model,\n [b[1] for b in batch]): batch\n for batch in batches\n }\n for fut in concurrent.futures.as_completed(futures):\n batch = futures[fut]\n try:\n result = fut.result()\n except Exception as e:\n print(f\"batch error: {e}\", file=sys.stderr)\n continue\n for local_i, (orig_idx, it) in enumerate(batch):\n tr = result.get(local_i)\n if tr is None:\n print(f\"missing translation for idx {orig_idx}: {it['source'][:60]!r}\",\n file=sys.stderr)\n continue\n it[\"tr_el\"].text = tr\n done += 1\n print(f\" progress {done}/{len(batches)}\", file=sys.stderr)\n\n # Strip type=\"unfinished\" from filled translations\n for it in items:\n if it[\"tr_el\"].text:\n if it[\"tr_el\"].get(\"type\") == \"unfinished\":\n del it[\"tr_el\"].attrib[\"type\"]\n\n tree.write(args.output, encoding=\"utf-8\", xml_declaration=True)\n print(f\"wrote {args.output}\", file=sys.stderr)\n\n\nif __name__ == \"__main__\":\n main()\n"}}}}