le-wechat-article v1.3:融合 human-writing 活人感写法(材料关 / 说话位置 / 活人感审读 + check_prose.py 双自检);附 check_article front matter 剥离修复

This commit is contained in:
leroy
2026-10-06 21:30:36 +08:00
parent 500d8224db
commit 3af717a76d
10 changed files with 788 additions and 19 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ skills/
触发方式:「写篇公众号文章」「把这份素材整理成推文」「同步到公众号草稿箱」。
方法论来源(只借机制,不复制表达):[mp-article-writor](https://github.com/balabalabalading/huuuuuuho-skills/tree/main/skills/mp-article-writor)(MIT,类型路由 / 去模板化 / 语义保真)· [wechat-article-skills](https://github.com/aiworkskills/wechat-article-skills)(Apache-2.0,产出配额 / AI 味指纹 / 机器自检)。
方法论来源(只借机制,不复制表达):[mp-article-writor](https://github.com/balabalabalading/huuuuuuho-skills/tree/main/skills/mp-article-writor)(MIT,类型路由 / 去模板化 / 语义保真)· [wechat-article-skills](https://github.com/aiworkskills/wechat-article-skills)(Apache-2.0,产出配额 / AI 味指纹 / 机器自检)· [human-writing](https://github.com/KKKKhazix/human-writing)(MIT,活人感写法 / 说话位置 / 模型腔硬禁与 check_prose 检查器)。
排版主题库直接引入:[gzh-design-skill](https://github.com/isjiamu/gzh-design-skill)(AGPL-3.0)的 6 套主题组件库、平台合规校验器与预览页脚本,存放于 `skills/le-wechat-article/references/themes/` 并适用其原协议(边界与修改声明见该目录 README)。
+2
View File
@@ -30,4 +30,6 @@
方法融合自两个开源项目(仅借机制与方法,未复制表达):[mp-article-writor](https://github.com/balabalabalading/huuuuuuho-skills/tree/main/skills/mp-article-writor)(MIT)的类型路由、去模板化审读与语义保真;[wechat-article-skills](https://github.com/aiworkskills/wechat-article-skills)(Apache-2.0)的产出配额、AI 味指纹阈值与机器自检思想。
活人感写作法(材料关 / 说话位置 / 行文规则 / 模型腔硬禁清单)与 `check_prose.py` 检查器提炼自 [KKKKhazix/human-writing](https://github.com/KKKKhazix/human-writing)(MIT),按本工坊的文章类型体系适配,出处标注见 [references/human-voice.md](references/human-voice.md) 与审读手册致谢。
排版主题库(`references/themes/` 与配套校验/预览脚本)**直接引入**自 [gzh-design-skill](https://github.com/isjiamu/gzh-design-skill)(AGPL-3.0,甲木 Jiamu × 摸鱼小李)——该部分适用其原协议,本 skill 其余部分为 MIT,边界与修改声明见 [themes/README.md](references/themes/README.md)。
+20 -10
View File
@@ -20,7 +20,7 @@ description: >
- **真实性是底线。** 每个事实性陈述都要能追溯到用户素材、公开可验证信息或用户声明的个人经历。素材不足就明说缺什么,不为凑篇幅编造细节、数据、经历或情绪。
- **类型决定写法。** 不同文章类型有不同的开头、节奏和审读标准(见 [references/article-types.md](references/article-types.md)),不把所有文章写成一种腔调。
- **修改不得走样。** 审读修改时保护事实、否定、范围、时间、归因和确定程度——不得把相关写成因果、可能写成确定、计划写成已完成。
- **判断放参考、数数放脚本。** 结构和审读的判断读 references/;能机器量的(标题层级、加粗密度、金句数、表格、中英文间距)交给 `check_article.py`,不靠眼睛。
- **判断放参考、数数放脚本。** 结构和审读的判断读 references/;能机器量的(标题层级、加粗密度、金句数、表格、中英文间距)交给 `check_article.py`,模型腔硬禁(翻案腔、破折号、冒号、黑话、路标)交给 `check_prose.py`,不靠眼睛。
## 工作流
@@ -49,10 +49,12 @@ description: >
画像还是空模板、或所选账号没有分节时,用其中的「首访问题」问用户一次,把答复写回该文件(此文件随本 skill 存放,属于个人资产);每个账号首次使用各问一次,之后不再重复问。事实卡留空则跳过,用户随时可补。画像与事实卡都不能覆盖类型路由、事实核查和真实性底线。
**校准结果是固定停点(轻量)**:展示本次将采用的约束——该账号画像要点(定位/声音/文末块/排版主题)、将取用的事实卡条目、敏感词要求——用户确认或调整后进第 3 步。画像早已填好、无新信息时也要展示:给用户当场临时调整的机会(如本文换排版主题、跳过文末块),展示可精简为几行。
**校准结果是固定停点(轻量)**:展示本次将采用的约束——该账号画像要点(定位/声音/文末块)、**排版主题选择**、将取用的事实卡条目、敏感词要求——用户确认或调整后进第 3 步。**排版主题以选择题呈现**:按文章类型从 [themes/theme-index.md](references/themes/theme-index.md) 给出 1 个推荐主题(附一句理由)+ 2–3 个备选(各附一句适用理由),该账号画像的默认主题标注出来;用户回复主题名或序号选定,说「按推荐」即用推荐项(可用 AskUserQuestion 出选项)。画像早已填好、无新信息时也要展示:给用户当场临时调整的机会(如换排版主题、跳过文末块),展示可精简为几行,但主题选择题不省。
### 第 3 步:大纲与图位
起草大纲前先过**材料关**([human-voice.md](references/human-voice.md) 第一节):非虚构正文计划 ≥1200 字的,先列**至少 5 件可追溯的具体材料**(动作、数字、时间、原话、失败、代价、后续,注明来自用户哪句话或哪份可靠来源),并确认能组成一条实际过程;大纲每个主要小节标注依托的材料。列不出 5 件——能检索的先研究再重新计数;依赖用户经历的回第 1 步用「三问」一次问全;仍不足就**缩题或压短篇幅**,不靠推演和重复解释灌字数。
按所选类型的结构骨架起草大纲,同时规划图位(按 style-rules 的密度档位表选一档)。每个图位标注「信息位 / 节奏位」:
- **信息位**:删掉这张图读者会**看不懂**(流程、结构、数据、对比)→ 图上文字逐字取自文章
@@ -64,11 +66,14 @@ description: >
### 第 4 步:写初稿
遵守 style-rules 的全部硬规则(标题层级、表格限制、中英文间距、直角引号),并满足产出配额——这些规则不靠自觉,第 6 步有脚本核验:
动笔前读 [human-voice.md](references/human-voice.md)(活人感写法),内部回答「说话位置五问」——不把答案原样交给用户。活人感按类型分浓度(叙事/复盘高、技术/教程克制、简报不加),它来自材料与信息来路,不是口头禅与假细节。
遵守 style-rules 的全部硬规则(标题层级、表格、中英文间距、直角引号),并满足产出配额——这些规则不靠自觉,第 6 步有脚本核验:
- **加粗是扫读落点**:每 100 字左右一处,连续 200 字不得没有;每个小节至少一处。验收标准:把全文加粗按顺序连起来读,应该是一篇能独立看懂的缩写版
- **摘要 ≠ 导语**:front matter 的 digest 与正文开头导语不得是同一句话
- **金句卡至多一处**:`> 金句。 —— 出处`,多了就退化成装饰
- **金句卡至多一处**:`> 金句。(出处)`,多了就退化成装饰
- **模型腔硬禁**(human-voice.md 第七节,`check_prose.py` 核验):翻案腔零容忍、正文无破折号、冒号只引人物直接原话、黑话与洞察路标清零
初稿写到 `article.md`,同时复制一份完整副本到同目录 `review-baseline.md`。基线在终稿交付前不得修改——第 5 步审读要对照它检测语义漂移。
@@ -76,7 +81,7 @@ description: >
两项审读,各起独立 subagent(关注点不同,混在一次会顾此失彼),都给它:作者素材、`review-baseline.md`、当前 `article.md`、文章类型、[references/review-playbook.md](references/review-playbook.md)。
**5a 内容审读**——按手册输出意见清单(每条含位置、可观察的问题、对阅读的影响、必须保留的信息、建议方向):去模板化、AI 味指纹(按防误判阈值,命中本身不是修改理由)、语义保真风险(对照基线标出可能改变事实/否定/范围/条件/时间/归因/确定程度/立场的位置)。
**5a 内容审读**——按手册输出意见清单(每条含位置、可观察的问题、对阅读的影响、必须保留的信息、建议方向):去模板化、AI 味指纹(按防误判阈值,命中本身不是修改理由)、**活人感审读**(谁在说 / 推进 / 表演性中文 / 结尾 / 冷读)、**模型腔硬禁清单**(命中即必改,与合规同级)、语义保真风险(对照基线标出可能改变事实/否定/范围/条件/时间/归因/确定程度/立场的位置)。
**5b 事实核查 + 合规扫描**——数字、事件、产品名、引用是否可追溯到素材;平台合规逐类扫描(违禁内容、广告法绝对化用语、医疗金融承诺、未授权素材、画像自定义敏感词清单)——**合规命中一律必改,与 AI 味的建议级不同**。
@@ -90,10 +95,12 @@ subagent 可以给出「没有需要修改的问题」;不要为填满清单
**长文终审**(正文 ≥2500 字或类型为测评/复盘/叙事/技术):修改完成后、交付前,再起一个 subagent 对照「作者素材 + 基线 + 当前稿」做终审——只查三件事:新增/遗漏/强化了主张没有、合规清零没有、HKR 一句话总评。轻稿(简报 <2500 字)跳过终审,主 agent 自查即可。
然后跑自检直到退出码为 0:
然后跑双脚本自检,直到两个退出码都为 0:
```bash
python3 scripts/check_article.py <文章目录>/article.md --type <主类型>
# 活人感硬禁:fail 项清零;「需人工判断」项按语境裁决,不改也说明理由
python3 scripts/check_prose.py <文章目录>/article.md
```
脚本输出加粗串读提要——自己读一遍那一串,像词云或像把段落重念一遍都是挑错了落点,这要人来判断,脚本量不了。
@@ -104,7 +111,7 @@ python3 scripts/check_article.py <文章目录>/article.md --type <主类型>
0. **图片生产(默认执行,用户明确不要图时才跳过)**:正文图位与封面按 [visual-production.md](references/visual-production.md) 生产——交给 ckm-banner-design,或 SVG 自绘 + 浏览器截图均可,满足 brief 验收点即可。信息位图上的文字**逐字取自文章**,配色与所选排版主题一致;可编辑源文件(SVG/HTML)随 PNG 一起保留在 `imgs/src/`。生成后把正文图位占位替换为 `imgs/<文件名>.png` 真实路径——**终稿不得残留 placeholder**(此时 check_article 不再加 `--allow-placeholder`)。
1. 排版转换。默认走**主题引擎**——6 套实战主题组件库,融合自 gzh-design-skill(AGPL,见 [themes/README.md](references/themes/README.md))。主题以**该账号**画像的「排版偏好」为默认,按文章类型对照 [themes/theme-index.md](references/themes/theme-index.md) 的适用场景校准(教程/测评→摸鱼绿、深度观点→红白色系、设计科技→石墨极简、随笔→留白禅意、对比评测→摸鱼票据、内刊复盘→橄榄手记);选定后按 [themes/ENGINE.md](references/themes/ENGINE.md) 读该主题组件库 + 通用组件库装配 HTML,装配完**校验到全绿才算完成**:
1. 排版转换。默认走**主题引擎**——6 套实战主题组件库,融合自 gzh-design-skill(AGPL,见 [themes/README.md](references/themes/README.md))。主题用**第 2 步停点用户选定的**(画像默认 + 当场调整的结果);若停点被「一口气跑完」豁免跳过,按文章类型对照 [themes/theme-index.md](references/themes/theme-index.md) 的适用场景自动选(教程/测评→摸鱼绿、深度观点→红白色系、设计科技→石墨极简、随笔→留白禅意、对比评测→摸鱼票据、内刊复盘→橄榄手记),并在交付时说明选择理由。选定后按 [themes/ENGINE.md](references/themes/ENGINE.md) 读该主题组件库 + 通用组件库装配 HTML,装配完**校验到全绿才算完成**:
```bash
python3 scripts/validate_gzh_html.py <文章目录>/article.html
@@ -148,7 +155,7 @@ python3 scripts/publish_draft.py <文章目录> --title "标题" --digest "摘
## 失败模式与停手点
- **素材不足以支撑所选类型** → 停下来列出缺什么,让用户补素材或换类型;不编造垫稿。
- **素材不足以支撑所选类型** → 停下来列出缺什么:能检索的先研究;要用户补的用 human-voice「三问」一次问全;研究后仍不足就缩题或压短篇幅,**宁可明显短于目标字数,不用推演和重复解释填满**;不编造垫稿。
- **审读意见互相冲突**(如保真要求与去模板化要求打架)→ 按手册判断顺序裁决:保信息 > 保类型 > 保作者声音 > 改表达;裁决不了的带两案问用户。
- **用户要「直接发表/群发」** → 拒绝:同步止于草稿箱,发表由用户在公众号后台手动完成;凭证缺失时给配置指引并停,不代填。
- `check_article.py` 跑不过且原因不明 → 先修硬规则项;脚本本身出错时报告错误,改为人工对照 style-rules 逐项检查,不假装通过。
@@ -159,7 +166,9 @@ python3 scripts/publish_draft.py <文章目录> --title "标题" --digest "摘
- 不在简报类文章里强加个人故事、情绪曲线或价值升华;不为提高「共鸣」编造经历。
- 不用「随着 AI 的发展」「综上所述」这类零信息套话开头收尾;连接词只在承担真实顺序、转折、总结功能时保留。
- 不把技术术语当 AI 味删掉;技术解析保持应有正式度,不统一改成聊天语气。
- 不在没跑 check_article.py 的情况下宣称「自检通过」。
- 不在没跑 check_article.py 和 check_prose.py 的情况下宣称「自检通过」。
- 不写翻案腔——先立误解再推翻(「不是A,而是B」及一切变形)一次也不行,判断从正面下。
- 不添无来源的精确细节(时间、神态、天气、摆设、突现原话),不穿论坛服装(口头禅、粗口、错别字不是活人感)。
- 不把作者画像、素材中的私人内容复制进交付物或对外文件。
- 未获「一口气跑完」授权时,不把停点之间的多个步骤合并进同一回合执行。
- 不把「在回复中展示了产物」当作「已获得确认」——确认只能来自停点之后用户的下一条消息。
@@ -168,7 +177,8 @@ python3 scripts/publish_draft.py <文章目录> --title "标题" --digest "摘
- 五类型路由与各类型结构:[article-types.md](references/article-types.md)
- 公众号排版硬规则、标点规范、产出配额、标题规范:[style-rules.md](references/style-rules.md)
- 审读手册(去模板化 + AI 味指纹 + 平台合规 + 语义保真):[review-playbook.md](references/review-playbook.md)
- 审读手册(去模板化 + AI 味指纹 + 活人感审读 + 模型腔硬禁 + 平台合规 + 语义保真):[review-playbook.md](references/review-playbook.md)
- 活人感写法(材料关 / 说话位置 / 行文规则 / 分型浓度 / 成稿硬禁):[human-voice.md](references/human-voice.md)
- 排版主题库与装配规则(6 主题 + 通用组件 + 引擎规则,AGPL 来源与协议边界):[themes/README.md](references/themes/README.md) · [theme-index.md](references/themes/theme-index.md) · [ENGINE.md](references/themes/ENGINE.md)
- 作者画像(风格;个人层 + 分账号层,渐进填充):[author-profile.md](references/author-profile.md)
- 常用事实卡(作者本人事实,渐进填充):[topic-facts.md](references/topic-facts.md)
@@ -17,6 +17,7 @@
4. 有没有特别喜欢/不能接受的表达方式?(口语化程度、能否用梗、第一人称习惯)
5. 文末固定要加什么?(关注引导/签名/往期推荐/什么都不加)
6. 排版偏好?(从 themes/theme-index.md 的 6 套主题选默认:摸鱼绿 / 红白色系 / 石墨极简 / 留白禅意 / 摸鱼票据 / 橄榄手记;简报类默认走 md2wechat 轻稿快档)
7. (可选)贴两三段你自己写的旧文或聊天记录作语气样本——只用于校准用词习惯与口语程度(语气层最弱的一层),不模仿口头禅,更不能替代材料与说话位置(见 [human-voice.md](human-voice.md))
## 个人层(全账号通用)
@@ -0,0 +1,66 @@
# 活人感写法(第 4 步写初稿时加载)
> 方法论提炼自 [KKKKhazix/human-writing](https://github.com/KKKKhazix/human-writing)(MIT,活人感写作 skill),按本工坊的文章类型体系适配。核心论点:**活人感第一来自材料,第二来自说话位置,最后才是语气。** 口头禅、玩梗、错别字、粗口不是活人感;无来源的「凌晨三点的冷咖啡」是假细节,越具体 AI 味越重。
## 一、动笔前先过材料关
- 非虚构正文计划超过 1200 字时,先逐条列出**至少 5 件具体材料**(动作、数字、时间、地点、原话、失败、代价、后续结果),注明来自用户哪句话或哪份可靠来源。概括性类别(「一些案例」)不算,五条相邻的道理也不算——要能组成一条实际过程。
- 列不出五件:能检索的先研究再重新计数;依赖用户经历的,用下方「三问」一次问全;用户不让问且研究后仍不足,**缩题或写短稿**,宁可明显短于目标字数,不用重复解释和推演灌字数。
- **三问**(问用户,一次问完):你和这件事实际发生过什么关系?哪个瞬间、数字、动作或原话最让你在意?你现在最想说的判断是什么?
## 二、找到说话位置(内部回答,不交给用户)
1. 谁在说这件事——他凭什么知道,哪些只是在推测。查资料、亲历、行业熟悉、认真研究后形成判断,四种位置都行;**不能把查资料写成亲历,把猜测写成内幕**。没有亲历作者也不消失——仍然可以交代更相信哪种解释、哪条材料改变了看法。
2. 什么事让他现在想说(一条新闻、一次经历、一个想不通的小事)。
3. 手里能托住文章的东西(优先动作、数字、原话、失败、代价、后果)。
4. 他对哪一点有明确判断,依据是什么,到哪里为止。
5. 读者读完上一段,最自然会追问什么。
## 三、最有活人感的东西是信息来路
每篇**挑一两项**自然出现,全部凑齐会变成新模板:
- 哪件事让作者留意到这个问题
- 他一开始怎样理解,后来被哪条材料纠正
- 哪个细节只有做过这件事的人才会注意
- 哪两份资料打架,最后怎么取舍
- 哪个判断只是个人看法,作者愿意承担但不冒充定论(「这块我也没完全弄明白」后面必须真有信息)
## 四、行文规则
- **开头尽快碰到事情**。直答、讲刚发生的一幕、或从一个反常的小事实说起;不预告全文结构。
- **每段新增一件东西**。新事实、新动作、新例子、新区别、新后果都算;同一观点换说法不算推进。后一段接住前一段留下的问题——推进靠材料和因果,不靠「更深一层」「真正的问题」这类路标。
- **句子主干先行**。先让主语和动作出现,时间、原因、条件、例子往后接。一句话三四个「的」时,把做事的人提到前面。长定语拆成小句。
- **松紧呼吸**。句长要有高低差——十个字的句子可以挨着四十字的句子;连续短句像敲桌子,连续长句让人等不到落脚。段落不必等长,普通地方普通地结束;单句成段只留给确实需要停一下的地方。
- **连词减半**。「因为/所以/但是/同时/此外」中文小句靠语序和事理就能相接,删掉一半,读不断的不补回来。
- **动词不名词化**。「对方案进行了调整」写「把方案改了」;「实现了收入的增长」写「多挣了多少」。
- **该重复就重复**。一段里「修表」可以出现四次;为躲重复换同义词(修表→这门手艺→这项技能)文章会油。
- **感情写出来就停**。动作、物件、原话已经传出情绪,不要追在后面替读者解释。比喻落下不翻译含义。
- **允许正常的不完美**。正常的重复、补充、自我修正、短暂岔话都可以——但每处要从眼前内容长出来,不按固定间隔表演随意。
- **判断可以偏、可以有情绪**。把依据放在附近;材料支持七成就说到七成,剩下三成承认不知道,比补五个限定词更可信。幽默从事情自身的荒唐处出来(宏大说法碰到现实小麻烦的落差),不另编笑话,笑点落下继续讲事。
- **知识等走到那里再出现**。背景能解释人物为何只能这样选、改变风险、推翻直觉才放进来;只证明作者知道很多的删。绕出去要带回一件东西,否则是知识展览。
- **写完就停**。不强行升华、首尾呼应、时代意义;不在末段重新摘要全文。
- **判据句**(校准方向用):「他毕业后离开上海,去了成都。那套量化程序已经跑过一段时间,他觉得可以全职试试。收入会不会稳定,当时没人知道。」——给动作、条件与风险;「他关掉一条好走的路,把命运押上赌桌」只有姿势。
## 五、假细节与假口语红线
- **无来源的精确时间、神态、天气、房间摆设、烟酒食物、突现的原话,全部不要**——装饰性细节只负责画面显得真实,不改变理解就删。好细节会改变理解:一笔价格说明风险,一段原话说明关系,一次失败说明旧办法为何行不通。
- **不穿论坛服装**。「老铁」「兄弟们」「咱就是说」「泡杯茶慢慢说」只在说话者真会这样说时留。不为制造活人感主动加粗口、错别字、三连省略号、括号吐槽。
- 用户说「看见」,不能扩成「试过」;用户给截图,不能补注册、付款和成功运行;记不清原话就写大意或请他确认。
## 六、按文章类型分浓度
- **叙事 / 复盘类**:浓度最高。信息来路、自我修正、真实犹豫、带立场的判断都用上;素材里的失败和代价优先保留。
- **技术 / 教程 / 测评类**:克制。保持术语与正式度(不统一改聊天语气),活人感主要来自:亲历过的坑与失败处理、真实的使用代价、明确的个人取舍(「我的看法是值,但只对每周真拍两天的人值」)。
- **简报类**:基本不加。事实与条目本身就成立,强加个人故事违反反例黑名单。
## 七、成稿硬禁令(第 6 步 `check_prose.py` 机器核验,命中即 FAIL)
- **翻案腔零容忍**(一次也不行,禁的是动作不是字面):「不是A,而是B」「并非…而是…」「不在于…而在于…」「与其说…不如说…」「表面…实际…」「看似…实则…」「你以为…其实…」「回头才发现」「A不重要,重要的是B」及一切变形。判断直接从正面下,先给判断再给依据。
- **三项以上同构排比**:留两项,第三项换说法或删。
- **破折号(——、—、–)正文清零**;**冒号只许引出人物直接原话**(「一句话总结:」「核心是:」禁),网址、代码、机器字段除外。
- **洞察路标**:「更微妙的是」「还有一层」「只说对了一半」「值得注意的是」「需要指出的是」「从某种意义上说」「说白了」「说穿了」「先说结论」。
- **抽象名词配具体动词的抒情**(时间保管细节、岁月磨平、焦虑显形);**名词化句式**(进行了/实现了/完成了对/起到了…作用);**商业黑话与借喻**(赋能/抓手/闭环/底层逻辑;仓库、温度、死亡、浪潮、钥匙、底座包装抽象概念)。
- 「不只…还…」是正常递进可以用;出现在翻案位置按翻案腔处理。原话命中禁令时改转述,不靠引号保留。
完整禁令与改法见 [review-playbook.md](review-playbook.md) 的「模型腔硬禁清单」;机器核验见 `scripts/check_prose.py`(改禁词必须同步改脚本,不让脚本偷偷多一套规则)。
@@ -48,7 +48,7 @@ AI 味的本质是**太顺、太均匀、没有取舍**——不是用词高级
| # | 指纹 | 典型样子 | 强度 |
|---|------|---------|------|
| 1 | 万能背景开头 | 「随着 AI 技术的发展」「在当今这个时代」 | 强 |
| 2 | 「不是 X,而是 Y」滥用 | 单次是修辞,**800 字内 ≥3 次**才算命中 | 强 |
| 2 | 翻案腔 | 单次即命中——已升级为硬禁,见下方「模型腔硬禁清单」(check_prose.py 核验) | 强 |
| 3 | 面面俱到没取舍 | 利弊前提例外全讲一遍,什么都不得罪 | 强 |
| 4 | 空洞祝福收尾 | 「愿你…」「你值得…」谁都适用谁都记不住 | 强 |
| 5 | 工整排比成瘾 | 单处排比是好文笔,**几乎每节都排比**才算 | 中 |
@@ -65,6 +65,49 @@ AI 味的本质是**太顺、太均匀、没有取舍**——不是用词高级
**防误判**:拿不准时宁可不报;文章主题本就关于价值观时谈意义是切题;体裁本身工整(清单文)时排比是设计。诊断目的是让稿子看清自己哪里偷懒,不是凑数找毛病。
## 活人感审读(正面检查,与 AI 味指纹互补)
> 方法论改写自 [KKKKhazix/human-writing](https://github.com/KKKKhazix/human-writing)(MIT)。指纹只指出「太平滑」,这一节回答「哪里能看出是一个人在说话」。
**谁在说**——读完回答四件事:
- 作者凭什么知道这件事(亲历 / 做过调查 / 熟悉行业 / 查完资料形成判断,四种位置都行;**查资料不能写成亲历,猜测不能写成内幕**)
- 哪一处能看出他真的在意
- 哪个判断有材料托着、哪个情绪有动作或关系托着
- **哪几段换一个模型也能原样写出来**——这类段落优先处理:补信息来路、作者取舍、具体材料;没东西可补就删。不要用加「我觉得」「说真的」修复匿名感,声音来自取舍
**推进**——给每段标一个主要作用(动作/事实/解释/例子/疑问/判断/背景/情绪):连续几段做同一件事会拖;后段没接住前段留下的问题要调序;只把前段换套说法的直接合并或删。**压缩试验**:删掉三分之一后事实、动作、判断和阅读体验几乎没变,说明原稿在注水,保留删短的版本。
**表演性中文**:
- **假深刻**——单独截图好看、放回正文没增加事实/解释/情绪的句子,大部分删;连续几段用短判断收尾,留最有材料的一处
- **假具体**——精确时间、天气、神态、房间摆设、突现的原话,逐项问来自哪里、会不会改变后文;无来源也无作用就删。好细节改变理解(一笔价格说明风险,一段原话说明关系),装饰性细节只负责画面
- **假口语**——称呼语、粗口、错别字只在说话者真会这样说时留
- **抽象名词**——连续出现时问谁做了什么、影响谁、钱时间责任落在哪;一句话能原样换进十篇商业文章,说明没有当前材料
- **比喻换场**——短距离出现三套借喻(道路/战争/建筑/温度/仓储/海洋/身体)先全部还原本义,本义已清楚的一个也不放回
**结尾**——把最后两段分别删掉再读,删掉更有力就让文章提前结束;最后一段重新概括全文的先删;搜「时代、文明、未来、世界、历史、奇迹、所有人、全人类」——正文没有持续处理这个尺度,结尾回到具体事实、人物动作或当前判断。
**冷读**(暂时忘掉规则只回答):哪里让我相信作者确实知道这件事?哪里让我愿意继续读?哪一段像人在处理一个问题、哪一段像模型在完成写作任务?哪句话说得比材料大?文章在哪一句已经结束?
## 模型腔硬禁清单(命中即必改,与平台合规同级;`check_prose.py` 机器核验)
禁的是修辞动作,不是字面——换一套字做同一个动作仍算命中。改法只有一种:找到这句话原本想下的判断,从正面说出来,依据放旁边;不找另一种漂亮句式替换。
| 类别 | 内容 |
|---|---|
| 翻案腔 | 先立一个读者并没有的误解再推翻抬价,**一次也不行**。外衣:不是A而是B / 并非…而是… / 不在于…而在于… / 与其说…不如说… / 表面…实际… / 看似…实则… / 你以为…其实… / 回头才发现 / A不重要,重要的是B / 答案恰恰相反,及一切变形。文章真用材料走过从误解到修正的过程,可保留一次自我修正,但不穿任何外衣 |
| 同构排比 | 三项以上同一句型/引导词连排,留两项,第三项换说法或删 |
| 名词化 | 进行了 / 实现了 / 完成了对 / 起到了…作用 / 具有…意义,还原成直接动词 |
| 提示性冒号 | 冒号只允许引出人物直接原话;「一句话总结:」「核心是:」禁;网址、代码、机器字段除外 |
| 破折号 | ——、—、– 正文清零(金句卡出处用全角括号) |
| 洞察路标 | 说白了 / 说穿了 / 先说结论 / 更微妙的是 / 还有一层 / 只说对了一半 / 值得注意的是 / 需要指出的是 / 从某种意义上说 |
| 绝对禁词 | 赋能、抓手、商业闭环、价值闭环、能力沉淀、拉通、底层逻辑、顶层设计、认知跃迁、价值释放、能力建设、降本增效、内容矩阵、全链路、组合拳、打开想象空间、结构性机会、关键命题、深层逻辑、技术底座、公共底座、技术主权、单点风险、主脊柱、材料锚点、认知增量、迭代闭环——改成人、动作、钱、时间与后果 |
| 语境词(本义成立保留) | 沉淀、颗粒度、对齐、协同、链路、生态位、心智、范式、方法论、核心变量、打法、想象空间、闭环——给普通事情抬价时才改写 |
| 抒情借喻 | 抽象名词配具体动词(时间保管细节、岁月磨平、焦虑显形);仓库、抽屉、温度、死亡、坍塌、浪潮、钥匙、底座包装抽象概念。写具体事物不受影响。安放、抵达、微光、褶皱、丰盈、滚烫、轻盈等词给抽象概念穿衣服时删。「」自造短语一篇出现三处以上多半在造金句,拆掉 |
「不只…还…」是正常递进可以用;出现在翻案位置、用来给下文抬价时按翻案腔处理。原话命中禁令改成转述,不靠引号保留。第 6 步跑 `scripts/check_prose.py`,fail 项与本清单一致;增删禁词必须同步改脚本,不让脚本偷偷多一套规则。
## 其他审读线索
- **空泛铺垫与伪深度**:只有预告作用的开场、无解释力的格言、没有真实反对者的自我辩护——删掉不损失事实/判断/逻辑的就删
@@ -93,4 +136,4 @@ AI 味的本质是**太顺、太均匀、没有取舍**——不是用词高级
---
致谢:本手册的语义保真框架与「模式不是禁词表」原则改写自 [mp-article-writor](https://github.com/balabalabalading/huuuuuuho-skills/tree/main/skills/mp-article-writor)(MIT);AI 味指纹与阈值化防误判改写自 [wechat-article-skills](https://github.com/aiworkskills/wechat-article-skills)(Apache-2.0)的 ai-flavor-check 方法论。仅借方法,未复制其表达与依赖。
致谢:本手册的语义保真框架与「模式不是禁词表」原则改写自 [mp-article-writor](https://github.com/balabalabalading/huuuuuuho-skills/tree/main/skills/mp-article-writor)(MIT);AI 味指纹与阈值化防误判改写自 [wechat-article-skills](https://github.com/aiworkskills/wechat-article-skills)(Apache-2.0)的 ai-flavor-check 方法论;活人感审读与模型腔硬禁清单改写自 [KKKKhazix/human-writing](https://github.com/KKKKhazix/human-writing)(MIT)。仅借方法与清单,适配与融合为本工坊改动。
@@ -22,6 +22,8 @@
- **专有名词**:按官方写法(macOS、iOS、GitHub、ChatGPT);有通用中文译名的优先中文,译名不通行时首次出现标注 `中文译名(Original Name)`
- **缩略语**:首次出现括注全称(除非广为人知)
- **省略号**:用……,不用三个句点
- **破折号与冒号**:正文不用任何破折号(——、—、–);冒号只允许引出人物直接原话,网址、代码、机器字段除外(模型腔重灾区,`check_prose.py` 核验,详见 [human-voice.md](human-voice.md) 第七节)
- **硬停词与黑话**:说白了/说穿了/先说结论、赋能/抓手/底层逻辑等商业黑话零出现;详见 human-voice.md 硬禁清单
- **日期**:完整句中用 `2026 年 10 月 5 日` 式汉字单位表达;提国外事件标注当地时间换算
- **工具名精度**:素材指向具体产品时用准确名称;素材没说名称时保留「某个模型」「AI 工具」等概括表达,不猜名字装具体
- **链接**:链接文本要能说明目标内容,不用「点击这里」「这篇」;清理短链和跟踪参数
@@ -36,8 +38,8 @@
- 每处加粗要能独立看懂:数字连着它的意思(`**省 88% Token**`),判断连着它的对象(`**打断不等于撤销**`)
- 不加粗整句空泛概括;不在每段首句同一位置加粗;一段最多两处
- **摘要 ≠ 导语**:front matter `digest`(进公众号后台的摘要)与正文开头导语不得逐字相同——读者会同一句读两遍
- **金句卡至多一处**:全文最有转发价值的那句写成 `> 金句。 —— 出处`;两处以上卡片退化成装饰条;不带出处就是普通引用
- **枚举用列表**:三项以上并列写成 `- **标签**:说明`,不压进一个长段落
- **金句卡至多一处**:全文最有转发价值的那句写成 `> 金句。(出处)`;两处以上卡片退化成装饰条;不带出处就是普通引用
- **枚举用列表**:三项以上并列写成 `- **标签**,说明`(标签后用逗号,不用冒号——冒号是 check_prose 硬禁项),不压进一个长段落
- **`---` 分隔**:只在明显转场处用,独占一行
- **图位密度**:大纲阶段按下表选一档(统一词汇,不各说各话),写完自查数量:
@@ -19,13 +19,13 @@
## 智能处理(装配时必须做)
1. **章节自动编号**:按 `##` 顺序编 01/02/03…;末章为结语/总结时用主题库指定的结语编号变体(如 `∞`)。
1. **章节自动编号**:按正文章节标题顺序编 01/02/03…(「融合」本工坊文章的章节是 `###`,按 `###` 计数——见 style-rules 标题层级,不用 `##`);末章为结语/总结时用主题库指定的结语编号变体(如 `∞`)。
2. **英文标签**:据中文章节标题生成(实测→TEST、教程→TUTORIAL、总结→SUMMARY…),主题库有槽位才用。
3. **正文关键词下划线(核心特色)**:对每个正文段落主动挑 1–3 个最重要短语(4–15 字),用 theme-index「正文下划线 CSS」列的值标记——优先核心观点、结论、关键数据、专有名词。原文没有加粗也要标;与写作侧「加粗是扫读落点」互补:md 里的加粗进锚点层,下划线在装配时补标。不整段划线。
4. **引言关键词高亮**:开头金句的核心词用高亮组件。
5. **目录**:主题库有目录组件时,从全部 `##` 挑最重要的 3 个作精选导读,不是全量章节列表。
6. **签名区**:「融合」只在**该账号画像配置了文末固定块**时渲染(对应 style-rules「不自动追加关注引导」红线),文案逐字取自画像;画像没配就不生成签名区。原文末尾已有的签名/引导段并入这唯一一处,不在中间或多处出现。
7. **中文全角标点**:正文标点全角(,。!?:;""''()——…),生成时直接写弯引号;代码块/行内代码/URL/英文标识符内保持原样。
7. **中文全角标点**:正文标点全角(,。!?;()…),生成时直接写全角。「融合」引号保持 article.md 的直角引号「」,不做弯引号转换;破折号与提示性冒号在正文禁用(human-voice 硬禁,装配不得引入);代码块/行内代码/URL/英文标识符内保持原样。
8. **列表**:按主题库映射规则;无专属组件时转带缩进正文段。
## 视觉层级(3 层,防滥用)
@@ -14,7 +14,7 @@ TYPE_RANGES = {
"简报": (1500, 3000), "测评": (2500, 5000), "复盘": (2500, 6000),
"技术": (2500, 6000), "叙事": (2000, 5000),
}
FILLER_PATTERNS = ["在当今", "随着{}的发展", "综上所述", "总而言之", "值得注意的是",
FILLER_PATTERNS = ["在当今", "随着({})的发展", "综上所述", "总而言之", "值得注意的是",
"不难发现", "让我们来看看", "下面让我们", "众所周知"]
HAN = r"\u4e00-\u9fff"
@@ -38,7 +38,8 @@ def split_blocks(md):
def check(path, art_type, strict, allow_placeholder=False):
errors, warns, infos = [], [], []
md = open(path, encoding="utf-8").read()
blocks = split_blocks(md)
fm_pre = re.match(r"^---\n.*?\n---\n", md, re.S)
blocks = split_blocks(md[fm_pre.end():] if fm_pre else md)
# ---- 标题层级 ----
h1 = [ln for ln, f in blocks if not f and re.match(r"^# ", ln)]
+644
View File
@@ -0,0 +1,644 @@
#!/usr/bin/env python3
"""检查中文成稿的硬禁令与常见模型化形状。只报警,不自动改文。
来源:https://github.com/KKKKhazix/human-writing (MIT,版权归 Human Writing Skill
contributors)。本工坊原样引入未改检测规则;禁令清单见 references/human-voice.md
第七节与 review-playbook.md「模型腔硬禁清单」(增删禁词须同步改本脚本)。
"""
from __future__ import annotations
import argparse
import collections
import re
import sys
from dataclasses import dataclass
from pathlib import Path
HARD_STOPS = (
"说白了",
"说穿了",
"先说结论",
)
HARD_JARGON = (
"赋能",
"抓手",
"商业闭环",
"价值闭环",
"能力沉淀",
"拉通",
"底层逻辑",
"顶层设计",
"认知跃迁",
"价值释放",
"能力建设",
"降本增效",
"内容矩阵",
"全链路",
"组合拳",
"打开想象空间",
"结构性机会",
"关键命题",
"深层逻辑",
"技术底座",
"公共底座",
"技术主权",
"单点风险",
"主脊柱",
"材料锚点",
"认知增量",
"迭代闭环",
)
CONTEXT_JARGON = (
"沉淀",
"颗粒度",
"对齐",
"协同",
"链路",
"生态位",
"心智",
"范式",
"方法论",
"核心变量",
"打法",
"想象空间",
"闭环",
"不丢",
)
LYRIC_WORDS = (
"安放",
"抵达",
"微光",
"褶皱",
"丰盈",
"滚烫",
"轻盈",
"赤裸",
"剥开",
)
ROAD_SIGNS = (
"更微妙的是",
"还有一层",
"只说对了一半",
"值得注意的是",
"需要指出的是",
"从某种意义上说",
)
ROAD_STRIP_CHARS = "。!?!? \n"
FORBIDDEN_PUNCTUATION = {
":": "中文冒号",
":": "英文冒号",
"—": "破折号",
"–": "连接号式破折号",
}
PIVOT_PATTERNS = (
re.compile(r"(?:并)?不是[^。!?\n]{0,90}而是"),
re.compile(r"并非[^。!?\n]{0,90}而是"),
re.compile(r"不在于[^。!?\n]{0,90}而在于"),
re.compile(r"与其说[^。!?\n]{0,90}(?:不如|毋宁|倒不如)"),
re.compile(r"[。!?!?]\s*而是"),
re.compile(r"表面(?:上)?[^。!?\n]{0,90}(?:其实|实际|实则)"),
re.compile(r"看似[^。!?\n]{0,90}(?:其实|实际|实则)"),
)
SEMANTIC_PIVOT_PATTERNS = (
re.compile(r"(?:总|一直|曾|都)?以为[^!?\n]{2,60}?(?:其实|才发现|才明白|才知道|后来才)"),
re.compile(r"(?:总|都|一直)以为[^!?\n]{2,60}?[。,](?:可|但|其实)"),
re.compile(r"回头(?:看|一看)?才(?:发现|明白|知道)"),
re.compile(r"(?:并)?不是[^。!?\n]{1,40},(?:更|才)?是[^,。!?\n]"),
re.compile(r"从来(?:都)?(?:不是|与[^。!?,\n]{1,12}无关)"),
re.compile(r"答案(?:是否定的|恰恰相反)|恰恰相反"),
re.compile(r"表面(?:上)?[^!?\n]{0,60}。[^!?\n]{0,12}(?:其实|实际|实则)"),
re.compile(r"看似[^!?\n]{0,60}。[^!?\n]{0,12}(?:其实|实际|实则)"),
re.compile(r"[^,。!?\n]{1,12}不重要,(?:重要|要紧)的是"),
re.compile(r"真正[^,。!?\n]{0,16}的(?:,)?是"),
re.compile(r"不只(?:是)?[^。!?\n]{0,90}(?:还|也)"),
)
NOMINALIZATION_PATTERNS = (
re.compile(r"进行(?:了|一次|一场|着)?[^。,!?\n]{0,10}(?:调整|优化|升级|分析|讨论|沟通|梳理|复盘|迭代|探索|尝试|思考|规划|布局)"),
re.compile(r"实现了?[^。,!?\n]{0,14}的?[^。,!?\n]{0,6}(?:提升|增长|突破|转变|跃升|落地)"),
re.compile(r"完成了?对[^。,!?\n]{0,16}的"),
re.compile(r"起到了?[^。,!?\n]{0,12}的?作用"),
re.compile(r"具有[^。,!?\n]{0,10}(?:意义|价值)"),
)
CONJUNCTIONS = (
"因为",
"所以",
"但是",
"然而",
"同时",
"此外",
"而且",
"并且",
"因此",
"不仅",
)
ROAD_SIGN_PATTERNS = (
re.compile(
rf"(?:^|[。!?!?]\s*){re.escape(ROAD_SIGNS[0])}[^。!?!?\n]{{0,24}}",
re.MULTILINE,
),
re.compile(
rf"(?:^|[。!?!?]\s*){re.escape(ROAD_SIGNS[1])}(?=(?:更|原因|问题|意思|考虑|变化|逻辑|价值|作用|风险|影响|值得|很少|不容易|常被|往往))[^。!?!?\n]{{0,24}}",
re.MULTILINE,
),
*(
re.compile(
rf"(?:^|[。!?!?]\s*){re.escape(phrase)}[^。!?!?\n]{{0,24}}",
re.MULTILINE,
)
for phrase in ROAD_SIGNS[2:]
),
)
SOFT_MARKERS = (
"真正",
"本质上",
"更深层次",
"归根结底",
"换句话说",
"不可否认",
"核心是",
"关键在于",
"这意味着",
)
REPEATED_OPENERS = (
"其实",
"不过",
"当然",
"所以",
"但是",
"后来",
"当时",
"很多人",
"问题是",
"更重要的是",
"说到这里",
)
LEFT_BRANCH_PATTERNS = (
re.compile(r"(?:^|[。!?]\s*)在[^,。!?\n]{12,70}(?:以后|之后|之前|以前|过程中|情况下|背景下),"),
re.compile(r"(?:^|[。!?]\s*)那些[^,。!?\n]{10,60}的[^,。!?\n]{2,30}[,。]"),
re.compile(r"(?:^|[。!?]\s*)(?:真正|最终|最后)让[^,。!?\n]{8,70}的,是"),
)
METAPHOR_FIELDS = {
"温度": ("降温", "升温", "冷却", "余温", "温度最高"),
"生死战争": ("杀死", "死因", "枪响", "开火", "战场", "引爆", "弹药"),
"建筑灾害": ("坍塌", "崩塌", "地基", "砖头", "支柱", "废墟"),
"仓储租赁": ("仓库", "库房", "租金", "取货", "入库", "库存"),
"道路竞赛": ("赛道", "跑道", "岔路", "十字路口", "终点线", "门票"),
"机器器官": ("齿轮", "引擎", "发动机", "血管", "骨架", "肌肉"),
"海洋航行": ("蓝海", "浪潮", "潮水", "航船", "灯塔", "彼岸"),
}
@dataclass
class Paragraph:
position: int
text: str
han: int
sentences: int
def han_count(text: str) -> int:
return len(re.findall(r"[\u4e00-\u9fff]", text))
def line_number(text: str, position: int) -> int:
return text.count("\n", 0, position) + 1
def excerpt(value: str, width: int = 72) -> str:
value = re.sub(r"\s+", " ", value).strip()
return value if len(value) <= width else value[: width - 1] + "…"
def mask_non_prose(text: str) -> str:
"""屏蔽代码、网址和机器元数据,同时保留字符位置与换行。"""
def mask(match: re.Match[str]) -> str:
return "".join("\n" if char == "\n" else " " for char in match.group())
patterns = (
re.compile(r"\A---\s*\n.*?\n---\s*(?:\n|\Z)", re.DOTALL),
re.compile(r"```.*?```", re.DOTALL),
re.compile(r"`[^`\n]*`"),
re.compile(r"\]\([^\n)]*\)"),
re.compile(r"https?://[^\s)>]+"),
re.compile(r"<[^>\n]+>"),
)
masked = text
for pattern in patterns:
masked = pattern.sub(mask, masked)
return masked
def non_overlapping_terms(text: str, terms: tuple[str, ...]):
matches = []
occupied = []
for term in sorted(terms, key=len, reverse=True):
for match in re.finditer(re.escape(term), text):
start, end = match.span()
if any(start < old_end and end > old_start for old_start, old_end in occupied):
continue
matches.append((start, term))
occupied.append((start, end))
return sorted(matches)
def all_matches(text: str, patterns: tuple[re.Pattern[str], ...]):
matches = []
for pattern in patterns:
matches.extend(pattern.finditer(text))
return sorted(matches, key=lambda match: match.start())
def heavy_de_sentences(text: str):
"""找出主干可能被多个“的”压到后面的长句。"""
matches = []
pattern = re.compile(r"[^。!?!?\n]+(?:[。!?!?]|$)")
for match in pattern.finditer(text):
value = match.group()
if han_count(value) >= 38 and value.count("的") >= 4:
matches.append(match)
return matches
def anaphora_runs(text: str, minimum: int = 3):
"""找出同一句里三个以上小句用同一个开头的排比。"""
matches = []
for sentence in re.finditer(r"[^。!?!?\n]+(?:[。!?!?]|$)", text):
clauses = [
clause.strip()
for clause in re.split(r"[,、;,;]", sentence.group())
if han_count(clause) >= 3
]
if len(clauses) < minimum:
continue
run = 1
for previous, current in zip(clauses, clauses[1:]):
if previous[:2] == current[:2] and re.match(r"[一-鿿]{2}", current):
run += 1
if run >= minimum:
matches.append(sentence)
break
else:
run = 1
return matches
def sentence_length_cv(text: str):
"""句长变异系数。人写的长短句差距大,模型的句长彼此接近。"""
lengths = [
han_count(match.group())
for match in re.finditer(r"[^。!?!?\n]+[。!?!?]", text)
if han_count(match.group()) >= 4
]
if len(lengths) < 12:
return None
mean = sum(lengths) / len(lengths)
if mean == 0:
return None
variance = sum((value - mean) ** 2 for value in lengths) / len(lengths)
return (variance ** 0.5) / mean, len(lengths)
def bracket_highlights(text: str):
"""「」括起来的短语。太密说明在批量造金句。"""
return list(re.finditer(r"[「『][^」』\n]{1,6}[」』]", text))
def prose_paragraphs(text: str) -> list[Paragraph]:
paragraphs = []
cursor = 0
for block in re.split(r"\n\s*\n", text):
position = text.find(block, cursor)
cursor = max(position + len(block), cursor)
clean = re.sub(r"[>*_`]", "", block).strip()
if not clean or clean.startswith(("#", "http", "![", "```")):
continue
if re.match(r"^(?:[-+*]|\d+[.、])\s", clean):
continue
count = han_count(clean)
if count < 4:
continue
sentences = max(1, len(re.findall(r"[。!?!?]", clean)))
paragraphs.append(Paragraph(position, clean, count, sentences))
return paragraphs
def metaphor_cluster(text: str, distance: int = 800):
hits = []
for field, words in METAPHOR_FIELDS.items():
for word in words:
for match in re.finditer(re.escape(word), text):
hits.append((match.start(), field, word))
hits.sort()
for index, (start, _, _) in enumerate(hits):
window = [hit for hit in hits[index:] if hit[0] - start <= distance]
fields = {hit[1] for hit in window}
if len(fields) >= 3:
return window, fields
return None
def short_streak(paragraphs: list[Paragraph], limit: int = 4):
streak = []
for paragraph in paragraphs:
if paragraph.han <= 24 and paragraph.sentences <= 1:
streak.append(paragraph)
if len(streak) >= limit:
return streak
else:
streak = []
return None
def opener_counts(paragraphs: list[Paragraph]):
counts = collections.Counter()
examples = {}
for paragraph in paragraphs:
value = paragraph.text.lstrip("“‘\"((")
for opener in REPEATED_OPENERS:
if value.startswith(opener):
counts[opener] += 1
examples.setdefault(opener, paragraph.position)
break
return counts, examples
def read_text(path: str) -> str:
if path == "-":
return sys.stdin.read()
return Path(path).read_text(encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser(description="检查中文成稿的硬禁令与模型化形状")
parser.add_argument("path", help="Markdown 或文本文件路径。使用 - 从标准输入读取")
args = parser.parse_args()
try:
text = read_text(args.path)
except (OSError, UnicodeError) as error:
print(f"无法读取稿件。{error}", file=sys.stderr)
return 2
prose = mask_non_prose(text)
total_han = han_count(prose)
if total_han == 0:
print("没有检测到汉字。", file=sys.stderr)
return 2
failures = []
warnings = []
quote_colons = []
for symbol, label in FORBIDDEN_PUNCTUATION.items():
matches = list(re.finditer(re.escape(symbol), prose))
if symbol in (":", ":"):
hard = []
for match in matches:
tail = prose[match.end() : match.end() + 2].lstrip()
if tail[:1] in ("「", "『", "“", "‘", '"'):
quote_colons.append(match)
else:
hard.append(match)
matches = hard
if matches:
lines = "、".join(str(line_number(text, match.start())) for match in matches[:8])
failures.append(f"{label}共 {len(matches)} 处,出现在第 {lines} 行。")
if quote_colons:
lines = "、".join(
str(line_number(text, match.start())) for match in quote_colons[:8]
)
warnings.append(
f"引出原话的冒号 {len(quote_colons)} 处,第 {lines} 行。确认引号里确实是原话,且不是提示性用法。"
)
stop_matches = non_overlapping_terms(prose, HARD_STOPS)
for position, phrase in stop_matches:
failures.append(f"硬停词,第 {line_number(text, position)} 行,{phrase}")
jargon_matches = non_overlapping_terms(prose, HARD_JARGON)
for position, phrase in jargon_matches:
failures.append(f"黑话,第 {line_number(text, position)} 行,{phrase}")
context_jargon_matches = non_overlapping_terms(prose, CONTEXT_JARGON)
hard_spans = [
(position, position + len(phrase)) for position, phrase in jargon_matches
]
context_jargon_matches = [
(position, phrase)
for position, phrase in context_jargon_matches
if not any(
position < end and position + len(phrase) > start
for start, end in hard_spans
)
]
if context_jargon_matches:
samples = "、".join(
dict.fromkeys(phrase for _, phrase in context_jargon_matches)
)
lines = "、".join(
dict.fromkeys(
str(line_number(text, position))
for position, _ in context_jargon_matches[:8]
)
)
warnings.append(
f"有 {len(context_jargon_matches)} 处词语需要结合语境判断。"
f"第 {lines} 行出现 {samples}。本义准确时保留,用来抬价时改写。"
)
road_signs = all_matches(prose, ROAD_SIGN_PATTERNS)
for match in road_signs:
failures.append(
f"模型路标,第 {line_number(text, match.start())} 行,"
f"“{excerpt(match.group().lstrip(ROAD_STRIP_CHARS))}”"
)
pivots = all_matches(prose, PIVOT_PATTERNS)
for match in pivots:
failures.append(
f"禁用翻案句,第 {line_number(text, match.start())} 行,"
f"“{excerpt(match.group())}”"
)
occupied_spans = [match.span() for match in pivots]
semantic_pivots = []
for match in all_matches(prose, SEMANTIC_PIVOT_PATTERNS):
if any(
match.start() < end and match.end() > start
for start, end in occupied_spans
):
continue
semantic_pivots.append(match)
occupied_spans.append(match.span())
for match in semantic_pivots:
warnings.append(
f"疑似翻案腔变形,第 {line_number(text, match.start())} 行,"
f"“{excerpt(match.group(), 44)}”。先立误解再推翻就改成正面陈述,正常用法保留。"
)
anaphoras = anaphora_runs(prose)
for match in anaphoras[:4]:
warnings.append(
f"三连以上同构排比,第 {line_number(text, match.start())} 行,"
f"“{excerpt(match.group(), 44)}”。留两项,第三项换说法或删掉。"
)
lyric_matches = non_overlapping_terms(prose, LYRIC_WORDS)
if len(lyric_matches) >= 2:
samples = "、".join(dict.fromkeys(term for _, term in lyric_matches))
warnings.append(
f"模型偏爱的抒情词 {len(lyric_matches)} 处。{samples}。"
"写具体事物时保留,给抽象概念穿衣服时删掉。"
)
nominalizations = all_matches(prose, NOMINALIZATION_PATTERNS)
for match in nominalizations[:4]:
warnings.append(
f"名词化句式,第 {line_number(text, match.start())} 行,"
f"“{excerpt(match.group(), 36)}”。还原成直接的动词。"
)
conjunction_hits = non_overlapping_terms(prose, CONJUNCTIONS)
if total_han >= 600 and len(conjunction_hits) * 1000 / total_han > 7:
samples = "、".join(
f"{term} {count} 次"
for term, count in collections.Counter(
term for _, term in conjunction_hits
).most_common(4)
)
warnings.append(
f"连词密度偏高,每千字 {len(conjunction_hits) * 1000 // total_han} 个。{samples}。"
"中文小句靠语序和事理相接,删掉一半试试。"
)
highlights = bracket_highlights(prose)
highlight_limit = max(3, total_han // 700)
if len(highlights) > highlight_limit:
samples = "、".join(dict.fromkeys(match.group() for match in highlights[:6]))
warnings.append(
f"「」括起的短语共 {len(highlights)} 处。{samples}。太密说明在批量造金句。"
)
cv_result = sentence_length_cv(prose)
if cv_result and cv_result[0] < 0.42:
warnings.append(
f"全文 {cv_result[1]} 个句子长度过于接近(变异系数 {cv_result[0]:.2f})。"
"人写的段落里十个字的句子会挨着四十个字的句子,放开几句,压短几句。"
)
marker_matches = non_overlapping_terms(prose, SOFT_MARKERS)
marker_limit = max(2, total_han // 900)
if len(marker_matches) > marker_limit:
samples = "、".join(dict.fromkeys(term for _, term in marker_matches))
warnings.append(
f"洞察路标共 {len(marker_matches)} 处,当前提醒线为 {marker_limit} 处。"
f"重点检查 {samples}。"
)
left_branches = all_matches(prose, LEFT_BRANCH_PATTERNS)
left_limit = max(2, total_han // 1200)
if len(left_branches) > left_limit:
samples = ";".join(
f"第 {line_number(text, match.start())} 行“{excerpt(match.group(), 44)}”"
for match in left_branches[:4]
)
warnings.append(
f"长前置成分共 {len(left_branches)} 处,可能让主干来得太晚。{samples}"
)
dense_de = heavy_de_sentences(prose)
dense_de_limit = max(1, total_han // 1500)
if len(dense_de) > dense_de_limit:
samples = ";".join(
f"第 {line_number(text, match.start())} 行“{excerpt(match.group(), 44)}”"
for match in dense_de[:4]
)
warnings.append(
f"有 {len(dense_de)} 个长句包含四个以上的“的”,可能要先交代人和动作。{samples}"
)
paragraphs = prose_paragraphs(prose)
if len(paragraphs) >= 10:
one_sentence = sum(paragraph.sentences <= 1 for paragraph in paragraphs)
ratio = one_sentence / len(paragraphs)
if ratio >= 0.75:
warnings.append(
f"可识别段落中有 {ratio:.0%} 只有一句话,可能形成统一的短段鼓点。"
)
streak = short_streak(paragraphs)
if streak:
first = streak[0]
warnings.append(
f"从第 {line_number(text, first.position)} 行起连续出现 {len(streak)} 个短促单句段,"
"检查是否在排队喊结论。"
)
counts, opener_examples = opener_counts(paragraphs)
repeated = [(opener, count) for opener, count in counts.items() if count >= 4]
if repeated:
details = "、".join(f"{opener} {count} 次" for opener, count in repeated)
first_position = min(opener_examples[opener] for opener, _ in repeated)
warnings.append(
f"段落开场重复,从第 {line_number(text, first_position)} 行附近开始。{details}。"
)
metaphors = metaphor_cluster(prose)
if metaphors:
window, fields = metaphors
samples = "、".join(dict.fromkeys(hit[2] for hit in window))
warnings.append(
f"八百字内出现 {len(fields)} 套借喻。{'、'.join(sorted(fields))}。"
f"例词有 {samples}。"
)
print(f"汉字数 {total_han}")
print(
f"翻案句 {len(pivots)},翻案腔变形 {len(semantic_pivots)},"
f"同构排比 {len(anaphoras)},名词化 {len(nominalizations)},"
f"黑话 {len(jargon_matches)},硬停词 {len(stop_matches)},"
f"模型路标 {len(road_signs)},需辨语境词 {len(context_jargon_matches)},"
f"抒情词 {len(lyric_matches)},洞察路标 {len(marker_matches)},"
f"长前置成分 {len(left_branches)},重定语句 {len(dense_de)}"
)
if failures:
print("\n需要修改")
for item in failures:
print(f"- {item}")
if warnings:
print("\n需要人工判断")
for item in warnings:
print(f"- {item}")
if not failures and not warnings:
print("\n未发现这份检查器覆盖的问题。")
return 1 if failures else 0
if __name__ == "__main__":
raise SystemExit(main())