diff --git a/README.md b/README.md index 3bd9d17..5dfa4d9 100644 --- a/README.md +++ b/README.md @@ -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)。 diff --git a/skills/le-wechat-article/README.md b/skills/le-wechat-article/README.md index 85969f6..6c90d69 100644 --- a/skills/le-wechat-article/README.md +++ b/skills/le-wechat-article/README.md @@ -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)。 diff --git a/skills/le-wechat-article/SKILL.md b/skills/le-wechat-article/SKILL.md index 1943511..361bdca 100644 --- a/skills/le-wechat-article/SKILL.md +++ b/skills/le-wechat-article/SKILL.md @@ -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) diff --git a/skills/le-wechat-article/references/author-profile.md b/skills/le-wechat-article/references/author-profile.md index 804964c..cc493a0 100644 --- a/skills/le-wechat-article/references/author-profile.md +++ b/skills/le-wechat-article/references/author-profile.md @@ -17,6 +17,7 @@ 4. 有没有特别喜欢/不能接受的表达方式?(口语化程度、能否用梗、第一人称习惯) 5. 文末固定要加什么?(关注引导/签名/往期推荐/什么都不加) 6. 排版偏好?(从 themes/theme-index.md 的 6 套主题选默认:摸鱼绿 / 红白色系 / 石墨极简 / 留白禅意 / 摸鱼票据 / 橄榄手记;简报类默认走 md2wechat 轻稿快档) +7. (可选)贴两三段你自己写的旧文或聊天记录作语气样本——只用于校准用词习惯与口语程度(语气层最弱的一层),不模仿口头禅,更不能替代材料与说话位置(见 [human-voice.md](human-voice.md)) ## 个人层(全账号通用) diff --git a/skills/le-wechat-article/references/human-voice.md b/skills/le-wechat-article/references/human-voice.md new file mode 100644 index 0000000..1caf2e6 --- /dev/null +++ b/skills/le-wechat-article/references/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`(改禁词必须同步改脚本,不让脚本偷偷多一套规则)。 diff --git a/skills/le-wechat-article/references/review-playbook.md b/skills/le-wechat-article/references/review-playbook.md index ef0db7b..efa45f3 100644 --- a/skills/le-wechat-article/references/review-playbook.md +++ b/skills/le-wechat-article/references/review-playbook.md @@ -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)。仅借方法与清单,适配与融合为本工坊改动。 diff --git a/skills/le-wechat-article/references/style-rules.md b/skills/le-wechat-article/references/style-rules.md index 33e840d..9155df1 100644 --- a/skills/le-wechat-article/references/style-rules.md +++ b/skills/le-wechat-article/references/style-rules.md @@ -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 硬禁项),不压进一个长段落 - **`---` 分隔**:只在明显转场处用,独占一行 - **图位密度**:大纲阶段按下表选一档(统一词汇,不各说各话),写完自查数量: diff --git a/skills/le-wechat-article/references/themes/ENGINE.md b/skills/le-wechat-article/references/themes/ENGINE.md index db2355e..3e68b7f 100644 --- a/skills/le-wechat-article/references/themes/ENGINE.md +++ b/skills/le-wechat-article/references/themes/ENGINE.md @@ -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 层,防滥用) diff --git a/skills/le-wechat-article/scripts/check_article.py b/skills/le-wechat-article/scripts/check_article.py index 0c1f6b2..072b745 100644 --- a/skills/le-wechat-article/scripts/check_article.py +++ b/skills/le-wechat-article/scripts/check_article.py @@ -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)] diff --git a/skills/le-wechat-article/scripts/check_prose.py b/skills/le-wechat-article/scripts/check_prose.py new file mode 100755 index 0000000..014c341 --- /dev/null +++ b/skills/le-wechat-article/scripts/check_prose.py @@ -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())