← dsh-novel-writer 技术文档 / novel_style_report

novel_style_report

风格画像报告。对全书逐章测量六个文笔维度,汇总为基线带 μ±σ,并聚合文风指纹、高频词、题材流派、情感量化、氛围 12 维与语义风格距离,作为「测量数据」交给调用方模型做风格判断。插件只报数、不下结论:模型的判断可通过 aiJudgment 回传存盘,供后续续写时复用。

参数

参数类型必填说明
bookstring书名,即 novels/ 下的子目录名。经 sanitizeSegment 过滤,拒绝路径分隔符与上跳。
rootstring章节库根目录。MCP 形态下必须落在启动参数 --root 指定范围内,越界会被静默回退。
actionstringreport(默认)生成测量报告;get 只读取已保存的 AI 判断,不重算。
briefbooleantrue 返回一句话精简结论,用于省 token 的场景。
aiJudgmentstring模型给出的风格气质判断。传入后与测量数据一同写入 style-reports/<book>.json

输出结构

契约字段(output.schema):bookreport 为必返;其余按需出现。渲染层会在正文之后追加「风格锚·原著段落」与「句式骨架」两段,供生成端照样例写,而不是只盯数字。

字段类型内容
reportstring人类可读的完整报告文本
baselineobject六维基线与每章测量值,见下文「基线带」
baselineChapterCountinteger实际参与基线统计的章数(已剔除过短章节)
dimensionsobject指纹 / 词汇 / 题材 / 情感 / 氛围 12 维 / 语义距离
anchors / skeletonsarray原著代表性段落与句式骨架样例
briefstring精简模式下的单句结论
savedJudgmentstringaction:"get" 时返回的历史判断

六个维度的定义与公式

全部维度零依赖、纯规则统计,不做模型推理。所有计数先按千字归一,量纲统一:

per1000(n) = n / totalChars × 1000

测量前先剥离 Markdown 标题行(^#{1,6}[ \t]+[^\n]*),章节标题不参与任何统计。

1 · 句法复杂度 complexity

complexity = Σ max(小句数ᵢ, 1) / 句数

切句用与 analysis.js 共用的句末符集合(。!?…!?),正则为「句末符后紧跟非句末符处切分,或遇换行切分」;句末符后的闭合引号会被吞掉(她说:「走吧。」 不产生孤儿引号句),纯标点残片(!!!)丢弃。小句按 [,,;;::] 切分后计非空段。该维度是「平均每句由几个小句构成」,近似嵌套深度。

2 · 修饰密度 modifierDensity

modifierDensity = per1000(adjMods + advMods) adjMods:/[\u4e00-\u9fff]{1,3}(?<!目)(?<!的)的/g → 再按 2 字窗口排除词尾/词首假阳性 advMods:/[\u4e00-\u9fff]{1,4}地/g → 再排除「地」结尾与「地X」名词

「的」作为词尾(真的 / 似的 / 目的 / 有的 / 是的)或词首(的确 / 的士)时都不是修饰助词,用两张停用词表按命中位置的 2 字窗口排除;刻意收录「中的 / 的话」——它们在「梦中的场景」「他的话很多」里是真修饰语。X地 侧按「地」的位置分两张名词表:以「地」结尾的(土地 / 原地 / 目的地…)用后缀判定,以「地」开头的(地上 / 地下 / 地方…)用命中之后的 2 字窗口判定。

3 · 抽象度 abstractDensity

abstractDensity = per1000(abstractCount) 逐位扫描:对每个未消费位置,按 4 → 3 → 2 字尝试最长匹配 命中条件:整词为纯汉字 且 末字 ∈ 抽象后缀表(57 项:感/性/度/情/绪/念/命/运/缘/罪/魂/魄…) 排除:假阳性停用词 12 项(知道/味道/街道/心里/哪里/事情/睡觉/地道/门道/过道/生意/注意) 命中后按命中长度做区间消费,避免同一段文字被重复计数

采用「最长匹配 + 区间消费」而非正则前瞻:旧写法要求抽象词后必须是非汉字,导致「她的感情很深。」计 0 而「她的感情,很深。」计 1——同词只因后随标点不同而结果相反。已知取舍:相邻两个抽象词若被一个 4 字窗口跨越(如「感情和理性」),只计 1 个。

4 · 动作密度 actionDensity

actionDensity = per1000(actionCount) 单字动作词表 625 条(去重 29 条 + 清洗 11 条名词/形容词后) 2 字搭配黑名单 28 项(十分/高兴/不行/行李/方法/瓶子/太阳/平静/安静/墙壁…) 对每个命中位置依次尝试: ① 后接动态助词「了着过起住上下进出开完掉得」→ actionCount++; 若其后仍是动作词,视为动补合并(走出去 / 跑回来)整体跳 3 字 ② 后接名词性汉字(排除 的了着过在是把被给跟从向对于和与以及就都也很又再)→ actionCount++,跳 2 字 ③ 位于句末(后接标点/空白/文本结尾)→ actionCount++,跳 1 字

词表本身是文档:单字表无法区分「分(分开)」与「十分」、「行(行走)」与「行李」,因此用 2 字黑名单在命中处兜底,窗口取命中字的前一字与后一字两种组合。第 ③ 条修的是越过文本末位与句末动词的漏检。

5 · 不确定性 hedgeDensity

hedgeDensity = per1000(hedgeCount) 模糊限制语表 20 项:似乎 仿佛 好像 大概 也许 或许 可能 像是 隐约 依稀 差不多 八成 兴许 恍若 貌似 如同 好似 疑似 感觉 像是要 匹配策略:按词长降序,命中后用占位符消费该区间

最长优先 + 占位符是为了避免重叠计数:像是要 不再被算成 像是 两次加 像是要 两次。

6 · 留白指数 gapIndex

gapIndex = per1000(省略号数 × 2 + 破折号数 × 2) + per1000(未完句数) 省略号 = 「……」+「...」出现次数;破折号 = 「——」出现次数 未完句判定:以省略号/破折号结尾 → 是;以 。!?!? 结尾 → 否; 否则其后不是换行/结尾 → 是(行末无标点按排版分行处理,不计)

两项都按千字归一是硬性要求:早期版本把千字密度与「未完句百分比」(0–100 量级)直接相加,15 字文本能算出 222.22,量纲不可比。未完句定位用顺序游标推进,重复文本不会误判。

基线带 μ±σ 的计算

逐章测量后汇总,跳过正文长度不足 40 字的章节(不足则不参与统计,也不计入 baselineChapterCount):

μ = Σ vᵢ / n σ_measured = sqrt( Σ (vᵢ − μ)² / n ) ← 总体标准差 minSigma = max( μ × 0.15 , μ = 0 ? 0 : 1e-4 ) σ_used = max( σ_measured , minSigma ) ← 小书容差带过窄的保护 low / high = μ ∓ σ_used recTol = clamp( round(1.5 × σ_used / μ × 100 / 5) × 5 , 10 , 100 )

minSigmaRatio 默认 0.15,即 σ 下限为均值的 15%。推荐容差取「作者自身章节波动的 1.5 倍 σ」,四舍五入到 5 的倍数,下限 10%、上限 100%;μ=0 时回退 15 以避免除零。

σ 为什么要分两个字段。返回体同时给出 sigmaMeasured(原始样本标准差)、sigmaUsed(参与 low/high/recTol 的钳制值)与 sigmaClamped(是否发生钳制)。早期版本只返回钳制后的值,展示层把 0.15μ 的下限当成「作者自身波动」念给用户——实测真实 σ=0.0084 被报成 0.15,差 18 倍。

偏离判定

给新章指标 v 与基线 μ

devPct = round( (v − μ) / μ × 1000 ) / 10 ← 相对均值的百分比 μ ≠ 0:status = devPct < tol.low ‖ devPct > tol.high → "out" devPct < tol.low/2 ‖ devPct > tol.high/2 → "warn" 否则 → "ok" μ = 0:σ > 0 且 |v| > 1.5σ → "out"(basis: "absolute",devPct 记 null) 否则整维登记进 skippedDims,由 summary 披露

tol 默认取 { low: −recTol, high: +recTol },也接受调用方逐维自定义。容差语义有明确区分:显式 0 表示零容差(任何偏差都判 out),只有缺失 / null / 空串 / 非有限值才回退推荐值——早期用 Number(x) || -recT 把 null 也当 0,单侧缺失时那一侧会退化成零容差。

约束与边界(对调用方模型)

  • book 必填,且经 sanitizeSegment 过滤;root 在 MCP 形态下必须落在启动时的 --root 之内——越界时由 MCP 层静默回退--root 并只写 stderr 日志,调用方不会收到错误,因此不要把 root 当作可靠的越权探测手段。
  • 测量与判断分离:本工具不返回风格结论。需要「这本书的文风是什么气质」这类判断时,由模型阅读报告后给出,并通过 aiJudgment 回传,插件负责存盘与复用。
  • 过短章节被排除:正文 < 40 字的章节不参与基线;baselineChapterCount 会小于实际章数,这是预期行为。
  • 维度可能被跳过:当某维基线 μ=0 且 σ=0 时无法做相对判定,该维进入 skippedDimssummary 会写明「另有 N 个维度基线均值为 0、未做相对判定」。调用方不应把「无维度超出容差带」读成「全部维度合格」。
  • 数值不可跨版本横比:v4.3.0 统一了句末符口径(省略号单独成句)、清洗了动作词表(去重 29 + 剔除 11 个非动词)、抽象度改为最长匹配、修饰密度增加停用词表——同一本书在新旧版本下的报告数值不可直接比较。
  • 落盘位置:报告写入 <root>/.novel-writer/style-reports/<book>.jsonaction:"get" 只读取该文件,不重算。

相关工具

  • novel_style_check —— 用同一套维度做「新章 vs 全书基线」的对照判定;基线会先剔除目标章再计算。
  • novel_sentence_analysis —— 九类句式分布、情感曲线与风格指纹,是本报告「维度」部分的原始来源。
  • novel_semantic_search —— 语义风格距离所依赖的本地向量引擎。

源码位置

  • lib/style-metrics.js —— 六维测量、基线计算、偏离判定(measureStyleMetrics / computeBaselineFromPerChapter / judgeAgainstBaseline
  • lib/analysis.js —— TERMINATORS 句末符常量、句式与情感分析
  • lib/index.js —— 工具注册:参数、输出契约与 execute 实现
  • lib/vibe.js —— 氛围光谱 12 维