arxiv-paper-zh 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +33 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/bin/arxiv-paper-zh.mjs +210 -0
- package/install.sh +105 -0
- package/package.json +46 -0
- package/skills/arxiv-paper-zh/SKILL.md +97 -0
- package/skills/arxiv-paper-zh/agents/openai.yaml +4 -0
- package/skills/arxiv-paper-zh/references/paper-translation-packages.txt +53 -0
- package/skills/arxiv-paper-zh/scripts/audit_tex_translation.py +63 -0
- package/skills/arxiv-paper-zh/scripts/build_and_check.py +24 -0
- package/skills/arxiv-paper-zh/scripts/inventory_and_shard.py +36 -0
- package/skills/arxiv-paper-zh/scripts/prepare_output_layout.py +45 -0
- package/skills/arxiv-paper-zh/scripts/prepare_tex_runtime.py +55 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "arxiv-paper-zh",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Verify and translate arXiv papers into compilable Chinese editions with paired English and Chinese PDFs.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "AndrewYq",
|
|
7
|
+
"url": "https://github.com/yqstar"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/yqstar/arxiv-paper-zh#readme",
|
|
10
|
+
"repository": "https://github.com/yqstar/arxiv-paper-zh",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"arxiv",
|
|
14
|
+
"translation",
|
|
15
|
+
"latex",
|
|
16
|
+
"chinese",
|
|
17
|
+
"agent-skill"
|
|
18
|
+
],
|
|
19
|
+
"skills": "./skills/",
|
|
20
|
+
"interface": {
|
|
21
|
+
"displayName": "arXiv 论文中文化",
|
|
22
|
+
"shortDescription": "核验并翻译 arXiv 论文,交付中英文源码与 PDF。",
|
|
23
|
+
"longDescription": "核验论文身份,下载并翻译 TeX 源码,检查依赖和漏译,最后编译并逐页核验中英文 PDF。",
|
|
24
|
+
"developerName": "AndrewYq",
|
|
25
|
+
"category": "Productivity",
|
|
26
|
+
"capabilities": [
|
|
27
|
+
"Read",
|
|
28
|
+
"Write"
|
|
29
|
+
],
|
|
30
|
+
"websiteURL": "https://github.com/yqstar/arxiv-paper-zh",
|
|
31
|
+
"defaultPrompt": "使用 arxiv-paper-zh 核验并翻译我指定的论文,交付中英文源码与 PDF。"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AndrewYq
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# arxiv-paper-zh
|
|
2
|
+
|
|
3
|
+
将 arXiv/LaTeX 论文快速转换为可编译的中文版本。该 Agent Skill 会先核验论文身份,再下载 TeX 源码,按内容量并行翻译,自动检查漏译与 TeX 依赖,最后使用 XeLaTeX 编译并逐页检查 PDF。
|
|
4
|
+
|
|
5
|
+
方案参考了科学空间的[《让 AI 翻译一篇完整的论文》](https://spaces.ac.cn/archives/11578),并针对 Codex、Claude Code 等 Agent Skills 兼容环境补充了并行分片、依赖缓存、自动审计和可重复构建流程。
|
|
6
|
+
|
|
7
|
+
## 功能
|
|
8
|
+
|
|
9
|
+
- 核对论文简称、标题、作者、摘要、URL 与 arXiv ID,避免翻错同名论文。
|
|
10
|
+
- 按 `arxiv-paper/<paper-name>/` 统一保存原始源码、中文源码以及中英文 PDF。
|
|
11
|
+
- 按可见英文词量均衡分片,支持多个 subagent 并行处理长论文。
|
|
12
|
+
- 保留公式、数值、引用键、标签、人名、模型名、数据集名和常用缩写。
|
|
13
|
+
- 翻译正文、章节标题、脚注、表头、表注和 caption。
|
|
14
|
+
- 批量检查并安装缺失的 TeX 宏包,复用共享 TinyTeX/TeX Live 缓存。
|
|
15
|
+
- 自动执行漏译审计、BibTeX/Biber 构建和引用收敛检查。
|
|
16
|
+
- 使用 XeLaTeX 生成中文 PDF,并要求逐页视觉核验。
|
|
17
|
+
|
|
18
|
+
## 项目结构
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
arxiv-paper-zh/
|
|
22
|
+
├── .codex-plugin/
|
|
23
|
+
│ └── plugin.json
|
|
24
|
+
├── .agents/
|
|
25
|
+
│ └── plugins/
|
|
26
|
+
│ └── marketplace.json
|
|
27
|
+
├── bin/
|
|
28
|
+
│ └── arxiv-paper-zh.mjs
|
|
29
|
+
├── skills/
|
|
30
|
+
│ └── arxiv-paper-zh/
|
|
31
|
+
│ ├── SKILL.md
|
|
32
|
+
│ ├── agents/
|
|
33
|
+
│ │ └── openai.yaml
|
|
34
|
+
│ ├── scripts/
|
|
35
|
+
│ │ ├── audit_tex_translation.py
|
|
36
|
+
│ │ ├── build_and_check.py
|
|
37
|
+
│ │ ├── inventory_and_shard.py
|
|
38
|
+
│ │ ├── prepare_output_layout.py
|
|
39
|
+
│ │ └── prepare_tex_runtime.py
|
|
40
|
+
│ └── references/
|
|
41
|
+
│ └── paper-translation-packages.txt
|
|
42
|
+
├── install.sh
|
|
43
|
+
├── package.json
|
|
44
|
+
├── .gitignore
|
|
45
|
+
└── README.md
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`skills/arxiv-paper-zh/` 是可以被 Agent 独立加载的完整 Skill;根目录同时提供 Codex Plugin manifest、npm/npx 安装器和原有 Shell 安装器。
|
|
49
|
+
|
|
50
|
+
## 环境要求
|
|
51
|
+
|
|
52
|
+
- macOS 或 Linux。
|
|
53
|
+
- Python 3.9 或更高版本。
|
|
54
|
+
- 可执行 `curl`、`tar` 等常用命令。
|
|
55
|
+
- Codex、Claude Code,或其他支持 [Agent Skills 开放格式](https://agentskills.io/) 的客户端。
|
|
56
|
+
- 编译时需要 XeLaTeX。可以使用系统 TeX Live,也可以让 Skill 准备并复用 TinyTeX。
|
|
57
|
+
- 第一次下载论文源码或安装缺失宏包时需要网络连接。
|
|
58
|
+
|
|
59
|
+
TeX 运行时不包含在本仓库中,避免让仓库体积增加数百 MB。不同论文缺少的宏包会被批量安装到共享运行时。
|
|
60
|
+
|
|
61
|
+
## 安装
|
|
62
|
+
|
|
63
|
+
### 使用 npm / npx
|
|
64
|
+
|
|
65
|
+
无需先克隆仓库,可直接安装到 Codex 和 Claude Code:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx arxiv-paper-zh install
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
安装到 Codex、Claude Code 和通用 Agent Skills 目录:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx arxiv-paper-zh install --all
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
也可以选择单个客户端或安装到指定项目:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx arxiv-paper-zh install --codex
|
|
81
|
+
npx arxiv-paper-zh install --claude
|
|
82
|
+
npx arxiv-paper-zh install --agents
|
|
83
|
+
npx arxiv-paper-zh install --project /path/to/project --all
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
npm 安装器会复制完整 Skill 目录。目标已存在时默认停止;明确需要升级或替换时增加 `--force`。
|
|
87
|
+
|
|
88
|
+
### 作为 Codex Plugin 安装
|
|
89
|
+
|
|
90
|
+
本仓库同时是一个 Codex Plugin,并通过仓库内的 marketplace 从 npm registry 获取版本化包:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
codex plugin marketplace add yqstar/arxiv-paper-zh --ref main
|
|
94
|
+
codex plugin add arxiv-paper-zh@arxiv-paper-zh
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 从源码一键安装到 Codex 和 Claude Code
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
git clone https://github.com/yqstar/arxiv-paper-zh.git
|
|
101
|
+
cd arxiv-paper-zh
|
|
102
|
+
./install.sh --all
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
安装脚本默认创建符号链接,因此更新仓库后各客户端会立即使用新版本。它不会覆盖已经存在的同名 Skill。
|
|
106
|
+
|
|
107
|
+
### 仅安装到指定环境
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
./install.sh --codex
|
|
111
|
+
./install.sh --claude
|
|
112
|
+
./install.sh --agents
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
对应的用户级目录为:
|
|
116
|
+
|
|
117
|
+
| 环境 | 安装位置 |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| Codex | `~/.codex/skills/arxiv-paper-zh` |
|
|
120
|
+
| Claude Code | `~/.claude/skills/arxiv-paper-zh` |
|
|
121
|
+
| 通用 Agent Skills | `~/.agents/skills/arxiv-paper-zh` |
|
|
122
|
+
|
|
123
|
+
如果客户端已经启动,请重新开启会话;Claude Code 通常支持技能目录的热更新。
|
|
124
|
+
|
|
125
|
+
### 项目级安装
|
|
126
|
+
|
|
127
|
+
把 Skill 安装到某个仓库,仅供该项目使用:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
./install.sh --project /path/to/project --agents
|
|
131
|
+
./install.sh --project /path/to/project --claude
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
这会分别创建:
|
|
135
|
+
|
|
136
|
+
- `/path/to/project/.agents/skills/arxiv-paper-zh`
|
|
137
|
+
- `/path/to/project/.claude/skills/arxiv-paper-zh`
|
|
138
|
+
|
|
139
|
+
### 手动安装
|
|
140
|
+
|
|
141
|
+
也可以直接复制或链接 Skill 子目录。例如:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
ln -s "$(pwd)/skills/arxiv-paper-zh" ~/.codex/skills/arxiv-paper-zh
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
不要只复制 `SKILL.md`,因为工作流还依赖 `scripts/` 和 `references/`。
|
|
148
|
+
|
|
149
|
+
## 使用
|
|
150
|
+
|
|
151
|
+
安装后直接向 Agent 描述论文即可:
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
使用 arxiv-paper-zh 翻译论文 RankMixer。
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
也可以提供更精确的信息:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
使用 arxiv-paper-zh 翻译 arXiv:2507.15551。保留公式、引用和人名,
|
|
161
|
+
将产物放到 arxiv-paper/RankMixer,分别编译并返回中英文 PDF。
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Codex 中可显式调用:
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
$arxiv-paper-zh 翻译 arXiv:2507.15551
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Claude Code 中可直接输入自然语言,或在发现该 Skill 后使用:
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
/arxiv-paper-zh 翻译 arXiv:2507.15551
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
如果简称可能对应多篇论文,Skill 会先返回候选论文的完整标题、作者与 arXiv ID,请用户确认后再下载。
|
|
177
|
+
|
|
178
|
+
## 交付内容
|
|
179
|
+
|
|
180
|
+
每次任务均按论文简称建立固定目录。例如 `paper-name=EST`:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
arxiv-paper/EST/
|
|
184
|
+
├── latex/
|
|
185
|
+
│ ├── source.tar
|
|
186
|
+
│ ├── paper-en/
|
|
187
|
+
│ └── paper-zh/
|
|
188
|
+
├── paper-en/
|
|
189
|
+
│ └── EST-en.pdf
|
|
190
|
+
└── paper-zh/
|
|
191
|
+
└── EST-zh.pdf
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
- `latex/source.tar`:从 arXiv 下载的原始压缩包。
|
|
195
|
+
- `latex/paper-en/`:未经翻译的英文源码。
|
|
196
|
+
- `latex/paper-zh/`:保持可编译性的中文源码副本。
|
|
197
|
+
- `paper-en/<paper-name>-en.pdf`:英文原版编译结果。
|
|
198
|
+
- `paper-zh/<paper-name>-zh.pdf`:中文译版编译结果。
|
|
199
|
+
|
|
200
|
+
`paper-name` 使用用户熟悉的简短名称并保留大小写,例如 `EST`、`Onetrans`,且只能包含英文字母、数字、点、下划线和连字符。
|
|
201
|
+
|
|
202
|
+
参考文献元数据默认不翻译;原图内部文字不修改,只翻译必要 caption。公式内的英文说明按“公式不变”原则保留。
|
|
203
|
+
|
|
204
|
+
## 内置工具
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# 创建并输出固定的论文产物路径
|
|
208
|
+
python3 skills/arxiv-paper-zh/scripts/prepare_output_layout.py EST --root arxiv-paper
|
|
209
|
+
|
|
210
|
+
# 统计文件并生成均衡翻译分片
|
|
211
|
+
python3 skills/arxiv-paper-zh/scripts/inventory_and_shard.py arxiv-paper/EST/latex/paper-zh --entry main.tex --workers 3 --json
|
|
212
|
+
|
|
213
|
+
# 检查预装包和论文专用宏包
|
|
214
|
+
python3 skills/arxiv-paper-zh/scripts/prepare_tex_runtime.py arxiv-paper/EST/latex/paper-zh --preset --kpsewhich /path/to/kpsewhich
|
|
215
|
+
|
|
216
|
+
# 扫描可能漏译的英文自然语言
|
|
217
|
+
python3 skills/arxiv-paper-zh/scripts/audit_tex_translation.py arxiv-paper/EST/latex/paper-zh
|
|
218
|
+
|
|
219
|
+
# 自动运行 XeLaTeX 与 BibTeX/Biber,检查构建日志
|
|
220
|
+
python3 skills/arxiv-paper-zh/scripts/build_and_check.py arxiv-paper/EST/latex/paper-zh/main.tex --tex-bin /path/to/tex/bin
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## 兼容性说明
|
|
224
|
+
|
|
225
|
+
本项目遵循以 `SKILL.md` 为入口的 Agent Skills 目录格式。核心翻译与构建步骤可以跨客户端复用,但以下能力取决于具体客户端:
|
|
226
|
+
|
|
227
|
+
- 是否支持自动触发 Skill。
|
|
228
|
+
- 是否支持并行 subagent。
|
|
229
|
+
- 是否允许网络下载与安装宏包。
|
|
230
|
+
- 是否提供可写文件系统和本地 XeLaTeX。
|
|
231
|
+
|
|
232
|
+
客户端不支持 subagent 时,Agent 应顺序处理分片;不影响翻译规则与构建脚本的使用。
|
|
233
|
+
|
|
234
|
+
## 发布前检查
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
python3 -m py_compile skills/arxiv-paper-zh/scripts/*.py
|
|
238
|
+
python3 /path/to/skill-creator/scripts/quick_validate.py skills/arxiv-paper-zh
|
|
239
|
+
shellcheck install.sh # 可选
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
发布前还应选择并添加合适的开源许可证,例如 MIT、Apache-2.0 或 GPL-3.0。
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const SKILL_NAME = "arxiv-paper-zh";
|
|
17
|
+
const PACKAGE_ROOT = fileURLToPath(new URL("../", import.meta.url));
|
|
18
|
+
const SOURCE_DIR = join(PACKAGE_ROOT, "skills", SKILL_NAME);
|
|
19
|
+
const PACKAGE_JSON = JSON.parse(
|
|
20
|
+
readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
function usage() {
|
|
24
|
+
process.stdout.write(`Usage: arxiv-paper-zh [install] [options]
|
|
25
|
+
|
|
26
|
+
Install the ${SKILL_NAME} Agent Skill.
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--all Install for Codex, Claude Code, and generic Agent Skills
|
|
30
|
+
--codex Install for Codex
|
|
31
|
+
--claude Install for Claude Code
|
|
32
|
+
--agents Install in the generic .agents/skills directory
|
|
33
|
+
--project PATH Install at project scope instead of user scope
|
|
34
|
+
--force Replace an existing installation
|
|
35
|
+
-h, --help Show this help
|
|
36
|
+
-v, --version Show the package version
|
|
37
|
+
|
|
38
|
+
With no target option, Codex and Claude Code are selected.
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fail(message) {
|
|
43
|
+
process.stderr.write(`error: ${message}\n`);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function pathExists(path) {
|
|
48
|
+
try {
|
|
49
|
+
lstatSync(path);
|
|
50
|
+
return true;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error?.code === "ENOENT") {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseArgs(argv) {
|
|
60
|
+
const args = [...argv];
|
|
61
|
+
if (args[0] === "install") {
|
|
62
|
+
args.shift();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const options = {
|
|
66
|
+
agents: false,
|
|
67
|
+
claude: false,
|
|
68
|
+
codex: false,
|
|
69
|
+
force: false,
|
|
70
|
+
project: null,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
74
|
+
const arg = args[index];
|
|
75
|
+
switch (arg) {
|
|
76
|
+
case "--all":
|
|
77
|
+
options.agents = true;
|
|
78
|
+
options.claude = true;
|
|
79
|
+
options.codex = true;
|
|
80
|
+
break;
|
|
81
|
+
case "--agents":
|
|
82
|
+
options.agents = true;
|
|
83
|
+
break;
|
|
84
|
+
case "--claude":
|
|
85
|
+
options.claude = true;
|
|
86
|
+
break;
|
|
87
|
+
case "--codex":
|
|
88
|
+
options.codex = true;
|
|
89
|
+
break;
|
|
90
|
+
case "--force":
|
|
91
|
+
options.force = true;
|
|
92
|
+
break;
|
|
93
|
+
case "--project":
|
|
94
|
+
index += 1;
|
|
95
|
+
if (index >= args.length) {
|
|
96
|
+
throw new Error("--project requires a path");
|
|
97
|
+
}
|
|
98
|
+
options.project = resolve(args[index]);
|
|
99
|
+
break;
|
|
100
|
+
case "-h":
|
|
101
|
+
case "--help":
|
|
102
|
+
usage();
|
|
103
|
+
return null;
|
|
104
|
+
case "-v":
|
|
105
|
+
case "--version":
|
|
106
|
+
process.stdout.write(`${PACKAGE_JSON.version}\n`);
|
|
107
|
+
return null;
|
|
108
|
+
default:
|
|
109
|
+
throw new Error(`unknown option: ${arg}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!options.agents && !options.claude && !options.codex) {
|
|
114
|
+
options.claude = true;
|
|
115
|
+
options.codex = true;
|
|
116
|
+
}
|
|
117
|
+
return options;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function destinations(options) {
|
|
121
|
+
const paths = [];
|
|
122
|
+
if (options.project) {
|
|
123
|
+
if (!existsSync(options.project)) {
|
|
124
|
+
throw new Error(`project path does not exist: ${options.project}`);
|
|
125
|
+
}
|
|
126
|
+
if (options.codex || options.agents) {
|
|
127
|
+
paths.push(join(options.project, ".agents", "skills", SKILL_NAME));
|
|
128
|
+
}
|
|
129
|
+
if (options.claude) {
|
|
130
|
+
paths.push(join(options.project, ".claude", "skills", SKILL_NAME));
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
const userHome = homedir();
|
|
134
|
+
if (options.codex) {
|
|
135
|
+
const codexRoot = process.env.CODEX_HOME
|
|
136
|
+
? resolve(process.env.CODEX_HOME)
|
|
137
|
+
: join(userHome, ".codex");
|
|
138
|
+
paths.push(join(codexRoot, "skills", SKILL_NAME));
|
|
139
|
+
}
|
|
140
|
+
if (options.claude) {
|
|
141
|
+
paths.push(join(userHome, ".claude", "skills", SKILL_NAME));
|
|
142
|
+
}
|
|
143
|
+
if (options.agents) {
|
|
144
|
+
paths.push(join(userHome, ".agents", "skills", SKILL_NAME));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return [...new Set(paths)];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function install(destination, force) {
|
|
151
|
+
const parent = dirname(destination);
|
|
152
|
+
mkdirSync(parent, { recursive: true });
|
|
153
|
+
|
|
154
|
+
if (pathExists(destination) && !force) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`destination already exists: ${destination} (use --force to replace it)`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const suffix = `${process.pid}-${Date.now()}`;
|
|
161
|
+
const temporary = join(parent, `.${SKILL_NAME}.tmp-${suffix}`);
|
|
162
|
+
const backup = join(parent, `.${SKILL_NAME}.backup-${suffix}`);
|
|
163
|
+
let backedUp = false;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
cpSync(SOURCE_DIR, temporary, { recursive: true, errorOnExist: true });
|
|
167
|
+
if (pathExists(destination)) {
|
|
168
|
+
renameSync(destination, backup);
|
|
169
|
+
backedUp = true;
|
|
170
|
+
}
|
|
171
|
+
renameSync(temporary, destination);
|
|
172
|
+
if (backedUp) {
|
|
173
|
+
rmSync(backup, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
177
|
+
if (backedUp && !pathExists(destination)) {
|
|
178
|
+
renameSync(backup, destination);
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
process.stdout.write(`installed: ${destination}\n`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
if (!existsSync(join(SOURCE_DIR, "SKILL.md"))) {
|
|
188
|
+
throw new Error(`packaged skill is missing: ${SOURCE_DIR}`);
|
|
189
|
+
}
|
|
190
|
+
const options = parseArgs(process.argv.slice(2));
|
|
191
|
+
if (options) {
|
|
192
|
+
const targets = destinations(options);
|
|
193
|
+
if (!options.force) {
|
|
194
|
+
const existing = targets.find(pathExists);
|
|
195
|
+
if (existing) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`destination already exists: ${existing} (use --force to replace it)`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const destination of targets) {
|
|
202
|
+
install(destination, options.force);
|
|
203
|
+
}
|
|
204
|
+
process.stdout.write(
|
|
205
|
+
"Installation complete. Start a new agent session if the skill is not detected immediately.\n",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
fail(error.message);
|
|
210
|
+
}
|
package/install.sh
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SKILL_NAME="arxiv-paper-zh"
|
|
5
|
+
REPO_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
6
|
+
SKILL_DIR="$REPO_DIR/skills/$SKILL_NAME"
|
|
7
|
+
PROJECT_DIR=""
|
|
8
|
+
INSTALL_CODEX=0
|
|
9
|
+
INSTALL_CLAUDE=0
|
|
10
|
+
INSTALL_AGENTS=0
|
|
11
|
+
|
|
12
|
+
usage() {
|
|
13
|
+
printf '%s\n' \
|
|
14
|
+
"Usage: ./install.sh [--all] [--codex] [--claude] [--agents] [--project PATH]" \
|
|
15
|
+
"" \
|
|
16
|
+
" --all Install for Codex, Claude Code, and generic Agent Skills" \
|
|
17
|
+
" --codex Install for Codex" \
|
|
18
|
+
" --claude Install for Claude Code" \
|
|
19
|
+
" --agents Install in the generic .agents/skills directory" \
|
|
20
|
+
" --project PATH Install at project scope instead of user scope" \
|
|
21
|
+
" -h, --help Show this help"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
while [ "$#" -gt 0 ]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--all)
|
|
27
|
+
INSTALL_CODEX=1
|
|
28
|
+
INSTALL_CLAUDE=1
|
|
29
|
+
INSTALL_AGENTS=1
|
|
30
|
+
;;
|
|
31
|
+
--codex) INSTALL_CODEX=1 ;;
|
|
32
|
+
--claude) INSTALL_CLAUDE=1 ;;
|
|
33
|
+
--agents) INSTALL_AGENTS=1 ;;
|
|
34
|
+
--project)
|
|
35
|
+
shift
|
|
36
|
+
if [ "$#" -eq 0 ]; then
|
|
37
|
+
printf 'error: --project requires a path\n' >&2
|
|
38
|
+
exit 2
|
|
39
|
+
fi
|
|
40
|
+
PROJECT_DIR="$(cd -- "$1" && pwd)"
|
|
41
|
+
;;
|
|
42
|
+
-h|--help)
|
|
43
|
+
usage
|
|
44
|
+
exit 0
|
|
45
|
+
;;
|
|
46
|
+
*)
|
|
47
|
+
printf 'error: unknown option: %s\n' "$1" >&2
|
|
48
|
+
usage >&2
|
|
49
|
+
exit 2
|
|
50
|
+
;;
|
|
51
|
+
esac
|
|
52
|
+
shift
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
if [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_AGENTS" -eq 0 ]; then
|
|
56
|
+
INSTALL_CODEX=1
|
|
57
|
+
INSTALL_CLAUDE=1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
install_link() {
|
|
61
|
+
destination="$1"
|
|
62
|
+
parent="$(dirname -- "$destination")"
|
|
63
|
+
mkdir -p -- "$parent"
|
|
64
|
+
|
|
65
|
+
if [ -L "$destination" ]; then
|
|
66
|
+
current="$(readlink "$destination")"
|
|
67
|
+
if [ "$current" = "$SKILL_DIR" ]; then
|
|
68
|
+
printf 'already installed: %s\n' "$destination"
|
|
69
|
+
return
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
if [ -e "$destination" ] || [ -L "$destination" ]; then
|
|
74
|
+
printf 'error: destination already exists: %s\n' "$destination" >&2
|
|
75
|
+
printf 'remove or rename it manually, then run this installer again.\n' >&2
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
ln -s -- "$SKILL_DIR" "$destination"
|
|
80
|
+
printf 'installed: %s -> %s\n' "$destination" "$SKILL_DIR"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if [ -n "$PROJECT_DIR" ]; then
|
|
84
|
+
if [ "$INSTALL_CODEX" -eq 1 ]; then
|
|
85
|
+
install_link "$PROJECT_DIR/.agents/skills/$SKILL_NAME"
|
|
86
|
+
fi
|
|
87
|
+
if [ "$INSTALL_CLAUDE" -eq 1 ]; then
|
|
88
|
+
install_link "$PROJECT_DIR/.claude/skills/$SKILL_NAME"
|
|
89
|
+
fi
|
|
90
|
+
if [ "$INSTALL_AGENTS" -eq 1 ]; then
|
|
91
|
+
install_link "$PROJECT_DIR/.agents/skills/$SKILL_NAME"
|
|
92
|
+
fi
|
|
93
|
+
else
|
|
94
|
+
if [ "$INSTALL_CODEX" -eq 1 ]; then
|
|
95
|
+
install_link "${CODEX_HOME:-$HOME/.codex}/skills/$SKILL_NAME"
|
|
96
|
+
fi
|
|
97
|
+
if [ "$INSTALL_CLAUDE" -eq 1 ]; then
|
|
98
|
+
install_link "$HOME/.claude/skills/$SKILL_NAME"
|
|
99
|
+
fi
|
|
100
|
+
if [ "$INSTALL_AGENTS" -eq 1 ]; then
|
|
101
|
+
install_link "$HOME/.agents/skills/$SKILL_NAME"
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
printf '%s\n' "Installation complete. Start a new agent session if the skill is not detected immediately."
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "arxiv-paper-zh",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install the arxiv-paper-zh Agent Skill for Codex, Claude Code, and compatible agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"arxiv-paper-zh": "./bin/arxiv-paper-zh.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
".codex-plugin/",
|
|
11
|
+
"bin/",
|
|
12
|
+
"skills/",
|
|
13
|
+
"install.sh",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test tests/*.test.mjs",
|
|
19
|
+
"check": "node --check bin/arxiv-paper-zh.mjs && python3 -c \"from pathlib import Path; [compile(path.read_bytes(), str(path), 'exec') for path in Path('skills/arxiv-paper-zh/scripts').glob('*.py')]\" && npm test"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/yqstar/arxiv-paper-zh.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/yqstar/arxiv-paper-zh#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/yqstar/arxiv-paper-zh/issues"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"agent-skills",
|
|
34
|
+
"arxiv",
|
|
35
|
+
"codex",
|
|
36
|
+
"claude-code",
|
|
37
|
+
"latex",
|
|
38
|
+
"translation",
|
|
39
|
+
"chinese"
|
|
40
|
+
],
|
|
41
|
+
"author": "AndrewYq",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: arxiv-paper-zh
|
|
3
|
+
description: Verify that a requested paper name, acronym, title, authors, abstract, URL, and arXiv ID identify the intended work; then rapidly download and translate its TeX source into Chinese with balanced subagent sharding, deterministic artifact paths under arxiv-paper, paired English/Chinese PDFs, cached dependencies, audits, XeLaTeX compilation, and full-page visual verification. Use when a user asks to locate and translate an arXiv or LaTeX research paper into a compilable Chinese edition, especially for ambiguous acronyms or long papers requiring parallel translation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# arXiv 论文快速中文化
|
|
7
|
+
|
|
8
|
+
交付保留原始数学、数据、引用和作者信息的中英文可编译源码及经过页面核验的中英文 PDF。身份正确性与译文完整性优先,但要并行执行互不依赖的阶段。
|
|
9
|
+
|
|
10
|
+
## 输出目录契约
|
|
11
|
+
|
|
12
|
+
将全部用户产物固定放到 `arxiv-paper/<paper-name>/`。`paper-name` 优先使用用户给出的论文简称并保留大小写,例如 `EST`、`Onetrans`;若用户只给出标题,则核验论文身份后生成简短名称。名称必须匹配 `[A-Za-z0-9][A-Za-z0-9._-]*`,不得包含空格、斜杠或 arXiv ID,除非 ID 本身就是用户指定名称。
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
arxiv-paper/<paper-name>/
|
|
16
|
+
├── latex/
|
|
17
|
+
│ ├── source.tar
|
|
18
|
+
│ ├── paper-en/
|
|
19
|
+
│ └── paper-zh/
|
|
20
|
+
├── paper-en/
|
|
21
|
+
│ └── <paper-name>-en.pdf
|
|
22
|
+
└── paper-zh/
|
|
23
|
+
└── <paper-name>-zh.pdf
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`latex/paper-en/` 保存未经翻译的解压源码,`latex/paper-zh/` 保存其完整中文副本;两个 PDF 目录只保存最终交付 PDF。开始下载前先运行:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python3 scripts/prepare_output_layout.py <paper-name> --root arxiv-paper
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
以脚本输出的绝对路径为准,后续不得另建 `work/<arxiv-id>`、顶层 `paper_cn/` 或其他平行交付目录。临时渲染图、构建日志和联系表可放在论文目录下的隐藏临时目录,交付前不得混入 `paper-en/` 或 `paper-zh/`。
|
|
33
|
+
|
|
34
|
+
## 共享论文翻译运行时
|
|
35
|
+
|
|
36
|
+
优先复用固定位置的 TinyTeX/TeX Live,不要将运行时复制进每个论文目录。首次建立或主动刷新共享运行时时,使用内置预装清单一次检查并一次批量安装:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python3 scripts/prepare_tex_runtime.py --preset \
|
|
40
|
+
--kpsewhich <shared-tex-root>/bin/<platform>/kpsewhich \
|
|
41
|
+
--tlmgr <shared-tex-root>/bin/<platform>/tlmgr --install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
预装清单位于 `references/paper-translation-packages.txt`,覆盖 XeLaTeX 中文排版、BibTeX/Biber、常见数学与字体、表格、算法、代码、绘图、caption,以及 ACM、IEEE、Elsevier、Springer 模板。首次安装需要联网;后续任务先离线检查,只有论文特有依赖缺失时才联网批量补装。不要安装 `scheme-full`。
|
|
45
|
+
|
|
46
|
+
## 快速流水线
|
|
47
|
+
|
|
48
|
+
1. 从 arXiv 摘要页或 API 核验规范化 ID、完整标题、作者、摘要、版本和日期。简称不是唯一标识。只有一个候选与用户主题高度一致时才继续,否则先让用户确认。明确告知用户“标题 + 作者 + arXiv ID”。
|
|
49
|
+
2. 确定并校验 `paper-name`,运行 `scripts/prepare_output_layout.py`。从 `https://export.arxiv.org/e-print/<ID>` 下载原始压缩包到 `latex/source.tar`,解压到 `latex/paper-en/`。用主 TeX 的 `\title{}`、作者或 README 做第二次身份核验;不一致时停止。
|
|
50
|
+
3. 将 `latex/paper-en/` 的全部内容完整复制到 `latex/paper-zh/`,不得产生 `latex/paper-zh/paper-en/` 额外嵌套;此后只修改中文副本。分别定位英文和中文入口文件。
|
|
51
|
+
4. 立即并行启动两条路径:
|
|
52
|
+
|
|
53
|
+
- 翻译路径:按可见英文词量生成均衡文件分片;正文较多时占满可用 subagent 槽位。每个代理只能编辑明确分配的文件。
|
|
54
|
+
- 构建路径:主代理同时本地化入口/样式文件、准备中文字体并预检 TeX 依赖。不要等待翻译结束后才开始配置编译。
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python3 scripts/inventory_and_shard.py arxiv-paper/<paper-name>/latex/paper-zh --entry main.tex --workers 3 --json
|
|
58
|
+
python3 scripts/prepare_tex_runtime.py arxiv-paper/<paper-name>/latex/paper-zh --preset --kpsewhich /path/to/kpsewhich
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
5. 若依赖缺失,使用同一脚本增加 `--tlmgr /path/to/tlmgr --install`,在一次 `tlmgr` 调用中补齐预装包和论文特有包;不得逐个安装。优先复用共享运行时。只有不存在可用运行时时才在可写缓存目录安装便携 TinyTeX;不得使用 `sudo`、`scheme-full` 或完整 TeX Live collection。
|
|
62
|
+
6. 主代理修改入口文件以支持 XeLaTeX 中文排版,通常使用 `\usepackage[UTF8,fontset=fandol]{ctex}`。删除仅适用于 pdfLaTeX 的 `inputenc` 和 T1 `fontenc`。本地化模板硬编码的 `Abstract`、`References`、`Keywords` 等可见字符串。
|
|
63
|
+
7. 翻译所有渲染的英文自然语言:标题、摘要、正文、章节标题、列表、脚注、表头、表注和 caption。保留公式、数学符号、数值、引用键、label/ref、URL、LaTeX 结构、人名、模型名、数据集名、缩写与通行技术标识。图片只翻译必要 caption;算法/代码只翻译自然语言注释、docstring、caption 和说明。参考文献元数据默认不翻译。
|
|
64
|
+
8. 子代理完成后,主代理只运行一次全局审计并人工复核全部命中;不得仅扫描顶层章节,也不得把子代理的自检当作最终证明:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python3 scripts/audit_tex_translation.py arxiv-paper/<paper-name>/latex/paper-zh
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
9. 先使用原论文声明或兼容的构建引擎编译 `latex/paper-en/`,确保英文源码、参考文献和交叉引用收敛。再使用自动构建脚本识别 BibTeX/Biber,并完成中文 XeLaTeX 收敛:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python3 scripts/build_and_check.py arxiv-paper/<paper-name>/latex/paper-zh/main.tex --tex-bin /path/to/tex/bin
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
若仍报告缺包,一次批量安装全部新缺失项后重试。不得接受 LaTeX 错误、未定义引用/citation 或缺失字符。
|
|
77
|
+
10. 分别低分辨率渲染中英文 PDF 的全部页面生成联系表,检查裁切、重叠、表格溢出、图片和页数;只对可疑页面高分辨率渲染。另用能正确显示 CJK 的系统 PDF 引擎抽查中文字体。Poppler 缺少 CMap 时不得把空白中文误判为正常。
|
|
78
|
+
11. 将英文成品复制为 `paper-en/<paper-name>-en.pdf`,将中文成品复制为 `paper-zh/<paper-name>-zh.pdf`。回复列出完整标题、作者、arXiv ID、`latex/paper-en/`、`latex/paper-zh/` 以及两个 PDF 的绝对路径。
|
|
79
|
+
|
|
80
|
+
## 并行规则
|
|
81
|
+
|
|
82
|
+
- 分片前先创建 `latex/paper-zh/`,并按脚本的权重而非文件数分配。
|
|
83
|
+
- 入口文件、共享宏和样式只由主代理修改;任何文件不得由两个代理同时编辑。
|
|
84
|
+
- 主代理在翻译进行时完成依赖预检、字体方案和编译入口改造。
|
|
85
|
+
- 共享运行时只建立一次;论文任务不得重复下载或复制相同运行时。
|
|
86
|
+
- 对小论文不强制派发代理;代理启动与合并开销高于预计翻译时间时直接处理。
|
|
87
|
+
|
|
88
|
+
## 完成标准
|
|
89
|
+
|
|
90
|
+
- arXiv 元数据与源码身份两次核验通过,原始源码未修改。
|
|
91
|
+
- 英文与中文源码分别完整位于 `latex/paper-en/` 和 `latex/paper-zh/`,原始压缩包位于 `latex/source.tar`;渲染的英文自然语言均已翻译或被人工判定为允许保留项。
|
|
92
|
+
- `paper-en/<paper-name>-en.pdf` 与 `paper-zh/<paper-name>-zh.pdf` 均构建成功,参考文献和交叉引用收敛;中文版无缺字。
|
|
93
|
+
- 中英文 PDF 的全部页面均已检查,中文字体由 CJK 能力正常的渲染器确认。
|
|
94
|
+
|
|
95
|
+
## 方案参考
|
|
96
|
+
|
|
97
|
+
本 Skill 的论文中文化方案参考了科学空间文章:[《让 AI 翻译一篇完整的论文》](https://spaces.ac.cn/archives/11578)。具体实现结合 Codex 的并行代理、依赖缓存、自动审计和 XeLaTeX 构建流程进行了调整。
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# TeX Live package | representative file. Keep one package per line.
|
|
2
|
+
latexmk|bin:latexmk
|
|
3
|
+
biber|bin:biber
|
|
4
|
+
ctex|ctex.sty
|
|
5
|
+
xecjk|xeCJK.sty
|
|
6
|
+
fandol|FandolSong-Regular.otf
|
|
7
|
+
unicode-math|unicode-math.sty
|
|
8
|
+
libertinus-fonts|LibertinusMath-Regular.otf
|
|
9
|
+
amsmath|amsmath.sty
|
|
10
|
+
amsfonts|amssymb.sty
|
|
11
|
+
mathtools|mathtools.sty
|
|
12
|
+
biblatex|biblatex.sty
|
|
13
|
+
natbib|natbib.sty
|
|
14
|
+
booktabs|booktabs.sty
|
|
15
|
+
multirow|multirow.sty
|
|
16
|
+
tools|tabularx.sty
|
|
17
|
+
makecell|makecell.sty
|
|
18
|
+
colortbl|colortbl.sty
|
|
19
|
+
graphics|graphicx.sty
|
|
20
|
+
xcolor|xcolor.sty
|
|
21
|
+
caption|caption.sty
|
|
22
|
+
subcaption|subcaption.sty
|
|
23
|
+
float|float.sty
|
|
24
|
+
wrapfig|wrapfig.sty
|
|
25
|
+
algorithm2e|algorithm2e.sty
|
|
26
|
+
algorithms|algorithm.sty
|
|
27
|
+
algorithmicx|algorithmicx.sty
|
|
28
|
+
listings|listings.sty
|
|
29
|
+
tcolorbox|tcolorbox.sty
|
|
30
|
+
pgf|tikz.sty
|
|
31
|
+
tikzfill|tikzfill.image.sty
|
|
32
|
+
cleveref|cleveref.sty
|
|
33
|
+
hyperref|hyperref.sty
|
|
34
|
+
url|url.sty
|
|
35
|
+
microtype|microtype.sty
|
|
36
|
+
geometry|geometry.sty
|
|
37
|
+
enumitem|enumitem.sty
|
|
38
|
+
setspace|setspace.sty
|
|
39
|
+
l3packages|xfrac.sty
|
|
40
|
+
preprint|balance.sty
|
|
41
|
+
comment|comment.sty
|
|
42
|
+
acmart|acmart.cls
|
|
43
|
+
ieeetran|IEEEtran.cls
|
|
44
|
+
elsarticle|elsarticle.cls
|
|
45
|
+
llncs|llncs.cls
|
|
46
|
+
newtx|newtxtext.sty
|
|
47
|
+
libertine|libertine.sty
|
|
48
|
+
inconsolata|inconsolata.sty
|
|
49
|
+
psnfss|pifont.sty
|
|
50
|
+
ifoddpage|ifoddpage.sty
|
|
51
|
+
relsize|relsize.sty
|
|
52
|
+
pdfcol|pdfcol.sty
|
|
53
|
+
ncctools|manyfoot.sty
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Inventory TeX files and flag likely untranslated English prose."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
PROSE = re.compile(r"[A-Za-z]{4,}(?:[ \t]+[A-Za-z][A-Za-z'’-]{2,}){2,}")
|
|
12
|
+
COMMAND_ONLY = re.compile(r"^[ \t]*\\(?:usepackage|documentclass|input|include|addbibresource)\b")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def tex_files(root: Path) -> list[Path]:
|
|
16
|
+
return sorted(path for path in root.rglob("*.tex") if path.is_file())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def visible_part(line: str) -> str:
|
|
20
|
+
escaped = False
|
|
21
|
+
for index, char in enumerate(line):
|
|
22
|
+
if char == "%" and not escaped:
|
|
23
|
+
return line[:index]
|
|
24
|
+
escaped = char == "\\" and not escaped
|
|
25
|
+
if char != "\\":
|
|
26
|
+
escaped = False
|
|
27
|
+
return line
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> int:
|
|
31
|
+
parser = argparse.ArgumentParser()
|
|
32
|
+
parser.add_argument("root", type=Path)
|
|
33
|
+
parser.add_argument("--inventory", action="store_true")
|
|
34
|
+
args = parser.parse_args()
|
|
35
|
+
root = args.root.resolve()
|
|
36
|
+
files = tex_files(root)
|
|
37
|
+
if not files:
|
|
38
|
+
parser.error(f"no .tex files below {root}")
|
|
39
|
+
|
|
40
|
+
if args.inventory:
|
|
41
|
+
for path in files:
|
|
42
|
+
count = sum(1 for _ in path.open(encoding="utf-8", errors="replace"))
|
|
43
|
+
print(f"{count:5d} {path.relative_to(root)}")
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
hits = 0
|
|
47
|
+
for path in files:
|
|
48
|
+
for number, raw in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
|
|
49
|
+
line = visible_part(raw)
|
|
50
|
+
if not line.strip() or COMMAND_ONLY.match(line):
|
|
51
|
+
continue
|
|
52
|
+
if PROSE.search(line):
|
|
53
|
+
hits += 1
|
|
54
|
+
excerpt = line.strip()
|
|
55
|
+
if len(excerpt) > 220:
|
|
56
|
+
excerpt = excerpt[:217] + "..."
|
|
57
|
+
print(f"{path.relative_to(root)}:{number}: {excerpt}")
|
|
58
|
+
print(f"suspect_lines={hits}")
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build with XeLaTeX, select BibTeX/Biber, and audit the final log."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import argparse, re, shutil, subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
ERROR = re.compile(r"LaTeX Error|Undefined control sequence|Emergency stop|Fatal error|undefined references|Citation .* undefined|Reference .* undefined|Missing character")
|
|
7
|
+
def tool(name: str, bindir: Path | None) -> str:
|
|
8
|
+
candidate = bindir / name if bindir else None; result = str(candidate) if candidate and candidate.exists() else shutil.which(name)
|
|
9
|
+
if not result: raise SystemExit(f"missing executable: {name}")
|
|
10
|
+
return result
|
|
11
|
+
def run(command: list[str], cwd: Path) -> None: print("+ " + " ".join(command)); subprocess.run(command, cwd=cwd, check=True)
|
|
12
|
+
def main() -> int:
|
|
13
|
+
parser = argparse.ArgumentParser(); parser.add_argument("entry", type=Path); parser.add_argument("--tex-bin", type=Path); args = parser.parse_args()
|
|
14
|
+
entry = args.entry.resolve(); args.tex_bin = args.tex_bin.resolve() if args.tex_bin else None; cwd, stem = entry.parent, entry.stem; source = entry.read_text(encoding="utf-8", errors="replace")
|
|
15
|
+
command = [tool("xelatex", args.tex_bin), "-interaction=nonstopmode", "-halt-on-error", entry.name]; run(command, cwd)
|
|
16
|
+
if re.search(r"\\(?:usepackage(?:\[[^]]*\])?\{biblatex\}|addbibresource)", source): run([tool("biber", args.tex_bin), stem], cwd)
|
|
17
|
+
elif re.search(r"\\bibliography\s*\{", source): run([tool("bibtex", args.tex_bin), stem], cwd)
|
|
18
|
+
run(command, cwd); run(command, cwd)
|
|
19
|
+
hits = [line for line in (cwd / f"{stem}.log").read_text(encoding="utf-8", errors="replace").splitlines() if ERROR.search(line)]
|
|
20
|
+
if hits: print("\n".join(hits)); return 2
|
|
21
|
+
pdf = cwd / f"{stem}.pdf"
|
|
22
|
+
if not pdf.is_file() or not pdf.stat().st_size: print(f"missing output: {pdf}"); return 3
|
|
23
|
+
print(f"build_ok={pdf}"); return 0
|
|
24
|
+
if __name__ == "__main__": raise SystemExit(main())
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Inventory reachable TeX sources and greedily balance translation shards."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import argparse, json, re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
INCLUDE = re.compile(r"\\(?:input|include)\s*\{([^}]+)\}")
|
|
7
|
+
COMMENT = re.compile(r"(?<!\\)%.*$")
|
|
8
|
+
COMMAND = re.compile(r"\\[A-Za-z@]+\*?(?:\[[^]]*\])?")
|
|
9
|
+
MATH = re.compile(r"\$[^$]*\$|\\\([^)]*\\\)|\\\[[^]]*\\\]", re.S)
|
|
10
|
+
def reachable(entry: Path, root: Path) -> list[Path]:
|
|
11
|
+
pending, seen = [entry.resolve()], set()
|
|
12
|
+
while pending:
|
|
13
|
+
path = pending.pop()
|
|
14
|
+
if path in seen or not path.is_file() or (root not in path.parents and path != root): continue
|
|
15
|
+
seen.add(path)
|
|
16
|
+
for value in INCLUDE.findall(path.read_text(encoding="utf-8", errors="replace")):
|
|
17
|
+
child = (path.parent / value.strip()).resolve()
|
|
18
|
+
pending.append(child if child.suffix else child.with_suffix(".tex"))
|
|
19
|
+
return sorted(seen)
|
|
20
|
+
def weight(path: Path) -> int:
|
|
21
|
+
text = "\n".join(COMMENT.sub("", line) for line in path.read_text(encoding="utf-8", errors="replace").splitlines())
|
|
22
|
+
return len(re.findall(r"[A-Za-z]{2,}", COMMAND.sub("", MATH.sub("", text))))
|
|
23
|
+
def main() -> int:
|
|
24
|
+
parser = argparse.ArgumentParser(); parser.add_argument("root", type=Path); parser.add_argument("--entry", type=Path); parser.add_argument("--workers", type=int, default=3); parser.add_argument("--min-weight", type=int, default=1); parser.add_argument("--json", action="store_true"); args = parser.parse_args()
|
|
25
|
+
root = args.root.resolve(); entry = (root / args.entry).resolve() if args.entry else None
|
|
26
|
+
files = reachable(entry, root) if entry else sorted(root.rglob("*.tex")); shared = {"macro.tex", "macros.tex", "commands.tex"}; records = [(p, weight(p)) for p in files if p.is_file() and p != entry and p.name.lower() not in shared]; records = [(p, score) for p, score in records if score >= args.min_weight]
|
|
27
|
+
groups = [[] for _ in range(max(1, args.workers))]; totals = [0] * len(groups)
|
|
28
|
+
for path, score in sorted(records, key=lambda item: item[1], reverse=True):
|
|
29
|
+
index = min(range(len(groups)), key=totals.__getitem__); groups[index].append((path, score)); totals[index] += score
|
|
30
|
+
payload = [{"worker": i + 1, "weight": totals[i], "files": [str(p.relative_to(root)) for p, _ in group]} for i, group in enumerate(groups)]
|
|
31
|
+
if args.json: print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
32
|
+
else:
|
|
33
|
+
for group in payload:
|
|
34
|
+
print(f"worker={group['worker']} weight={group['weight']}"); [print(f" {p}") for p in group["files"]]
|
|
35
|
+
return 0
|
|
36
|
+
if __name__ == "__main__": raise SystemExit(main())
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Create the deterministic artifact layout for one translated paper."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
SAFE_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> int:
|
|
16
|
+
parser = argparse.ArgumentParser()
|
|
17
|
+
parser.add_argument("paper_name", help="short, filesystem-safe paper name, e.g. EST or Onetrans")
|
|
18
|
+
parser.add_argument("--root", type=Path, default=Path("arxiv-paper"))
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
name = args.paper_name.strip()
|
|
22
|
+
if not SAFE_NAME.fullmatch(name):
|
|
23
|
+
parser.error("paper_name must match [A-Za-z0-9][A-Za-z0-9._-]*")
|
|
24
|
+
|
|
25
|
+
paper_root = (args.root / name).resolve()
|
|
26
|
+
paths = {
|
|
27
|
+
"paper_root": paper_root,
|
|
28
|
+
"latex_root": paper_root / "latex",
|
|
29
|
+
"source_archive": paper_root / "latex" / "source.tar",
|
|
30
|
+
"latex_en": paper_root / "latex" / "paper-en",
|
|
31
|
+
"latex_zh": paper_root / "latex" / "paper-zh",
|
|
32
|
+
"pdf_en": paper_root / "paper-en" / f"{name}-en.pdf",
|
|
33
|
+
"pdf_zh": paper_root / "paper-zh" / f"{name}-zh.pdf",
|
|
34
|
+
}
|
|
35
|
+
for key in ("latex_en", "latex_zh"):
|
|
36
|
+
paths[key].mkdir(parents=True, exist_ok=True)
|
|
37
|
+
paths["pdf_en"].parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
paths["pdf_zh"].parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
print(json.dumps({key: str(value) for key, value in paths.items()}, ensure_ascii=False, indent=2))
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check or batch-install paper-translation and document-specific TeX packages."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import argparse, os, re, shutil, subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
USEPACKAGE = re.compile(r"\\usepackage(?:\[[^]]*\])?\{([^}]+)\}")
|
|
8
|
+
ALIASES = {"balance": "preprint", "manyfoot": "ncctools", "tikzfill.image": "tikzfill", "xspace": "tools", "graphicx": "graphics", "pifont": "psnfss", "xfrac": "l3packages"}
|
|
9
|
+
TRANSITIVE = {"algorithm2e": ["ifoddpage", "relsize"], "tcolorbox": ["tikzfill", "pdfcol"]}
|
|
10
|
+
PRESET = Path(__file__).resolve().parent.parent / "references" / "paper-translation-packages.txt"
|
|
11
|
+
|
|
12
|
+
def executable(value: str | None, fallback: str) -> str:
|
|
13
|
+
result = value or shutil.which(fallback)
|
|
14
|
+
if not result: raise SystemExit(f"missing executable: {fallback}")
|
|
15
|
+
path = Path(result).expanduser()
|
|
16
|
+
return os.path.abspath(path) if path.exists() else result
|
|
17
|
+
|
|
18
|
+
def read_preset() -> dict[str, str]:
|
|
19
|
+
result = {}
|
|
20
|
+
for raw in PRESET.read_text(encoding="utf-8").splitlines():
|
|
21
|
+
line = raw.strip()
|
|
22
|
+
if line and not line.startswith("#"):
|
|
23
|
+
package, probe = line.split("|", 1); result[package.strip()] = probe.strip()
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
def main() -> int:
|
|
27
|
+
parser = argparse.ArgumentParser()
|
|
28
|
+
parser.add_argument("root", type=Path, nargs="?")
|
|
29
|
+
parser.add_argument("--kpsewhich")
|
|
30
|
+
parser.add_argument("--tlmgr")
|
|
31
|
+
parser.add_argument("--preset", action="store_true", help="include the reusable paper-translation preset")
|
|
32
|
+
parser.add_argument("--install", action="store_true", help="install all missing packages in one tlmgr call")
|
|
33
|
+
args = parser.parse_args()
|
|
34
|
+
if not args.root and not args.preset: parser.error("provide root and/or --preset")
|
|
35
|
+
kpsewhich = executable(args.kpsewhich, "kpsewhich")
|
|
36
|
+
candidates: dict[str, str] = read_preset() if args.preset else {}
|
|
37
|
+
if args.root:
|
|
38
|
+
for path in args.root.resolve().rglob("*.tex"):
|
|
39
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
40
|
+
for group in USEPACKAGE.findall(text):
|
|
41
|
+
for name in (x.strip() for x in group.split(",") if x.strip()):
|
|
42
|
+
package = ALIASES.get(name, name); candidates.setdefault(package, f"{name}.sty")
|
|
43
|
+
for dependency in TRANSITIVE.get(name, []): candidates.setdefault(dependency, f"{dependency}.sty")
|
|
44
|
+
missing = []; tex_bin = Path(kpsewhich).resolve().parent
|
|
45
|
+
for package, probe in sorted(candidates.items()):
|
|
46
|
+
found = (tex_bin / probe.removeprefix("bin:")).exists() if probe.startswith("bin:") else bool(subprocess.run([kpsewhich, probe], capture_output=True, text=True).stdout.strip())
|
|
47
|
+
if not found: missing.append(package)
|
|
48
|
+
print(f"preset_file={PRESET}" if args.preset else "preset_file=")
|
|
49
|
+
print("checked_packages=" + " ".join(sorted(candidates)))
|
|
50
|
+
print("missing_packages=" + " ".join(missing))
|
|
51
|
+
if args.install and missing:
|
|
52
|
+
return subprocess.run([executable(args.tlmgr, "tlmgr"), "install", *missing]).returncode
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__": raise SystemExit(main())
|