dsh-novel-writer 3.2.1 → 3.6.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/LICENSE +21 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/cordis.patch.yml +1 -1
- package/lib/analysis.js +63 -28
- package/lib/client.js +131 -48
- package/lib/embedding.js +17 -3
- package/lib/index.js +275 -107
- package/lib/lexicons/markers.js +4 -4
- package/lib/prompts.js +2 -4
- package/lib/style-metrics.js +41 -17
- package/lib/update-check.js +6 -2
- package/lib/vibe.js +29 -15
- package/package.json +3 -3
- package/skills/novel-writing/SKILL.md +14 -11
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 siweina
|
|
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.en.md
CHANGED
|
@@ -8,7 +8,7 @@ English | [**中文**](./README.md)
|
|
|
8
8
|
[](https://github.com/siweina/dsh-novel-writer/releases)
|
|
9
9
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
10
10
|
|
|
11
|
-
A novel-writing assistant plugin for **DeepSeek Harness (DSH)
|
|
11
|
+
A novel-writing assistant plugin for **DeepSeek Harness (DSH)**: chapter library management, sentence-pattern analysis, emotion purification & quantification, **12-axis vibe spectrum**, **style portrait report**, **six-dimension style baseline band**, **writing sentinels (bridge/OOC/outline drift)**, plot & settings management, local semantic search (0 token), webnovel signal detection, **original mode with creation files (bible/dynamic outline/hook backfill)**, batch import, and AI-assisted continuation writing. The semantic engine depends on `onnxruntime-web` and `@huggingface/tokenizers` (auto-installed with the package). **Requires Node ≥ 22.3.**
|
|
12
12
|
|
|
13
13
|
> **A note to non-Chinese users**: This plugin is designed specifically for Chinese-language novel analysis and writing — its core capabilities (sentence-pattern analysis, emotion quantification, imagery detection) and its built-in semantic model are all built and tuned for Chinese text. Fully supporting English or other languages alongside Chinese is beyond my current capability. I sincerely apologize for any inconvenience this may cause.
|
|
14
14
|
|
|
@@ -39,14 +39,14 @@ After installing, **restart the web app** to activate (host registers 16 tools +
|
|
|
39
39
|
|
|
40
40
|
1. **Style portrait report** (`novel_style_report`): 6-dimension measurement — style fingerprint / high-frequency lexicon / genre-theme / emotion quantification / 12-axis vibe spectrum / semantic style distance. **Measurement-judgment separation**: the plugin only reports numbers, never labels; AI judgment can be saved back to `.novel-writer/style-reports/` for consistent continuation writing.
|
|
41
41
|
2. **12-axis vibe spectrum**: nightmare / angst / heartwarming / fluff / tearjerker / dark / mystery / blaze / absurd / lonesome / aesthetic / sensual — with traceable evidence, 0 token.
|
|
42
|
-
3. **Local semantic engine**: bge-small-zh Chinese model (
|
|
42
|
+
3. **Local semantic engine**: bge-small-zh Chinese model (24MB, shipped with the plugin) local CPU inference — `novel_semantic_search` finds semantically related passages with natural language (with chapter location), semantic style comparison, semantic implicit emotion; lazy loading + graceful fallback.
|
|
43
43
|
4. **Sentence-pattern analysis**: 9 categories, arrangement patterns, rhythm, emotion curve, style fingerprint + guidance, with cache & report export.
|
|
44
44
|
5. **Emotion purification & quantification**: strong/weak emotion-word grading, pollution detection, caveat warning + AI re-verification; Valence sliding window → variance V / delta Δ / conflict index C + implicit imagery carriers.
|
|
45
45
|
6. **Worldview & pragmatics detection**: auto cultural-baseline detection with confidence; speechStyle title/honorifics/rituals/tone norms; genre & theme + webnovel signals.
|
|
46
46
|
7. **Writing toolkit**: plot tracking / five settings tables (characters·locations·items·timeline·worldview) / chapter summaries / continuity audit / batch import / style check / continuation writing.
|
|
47
47
|
8. **Per-tool UI toggles**: "Writing Assistant" sidebar panel (master + grouped tool toggles + feature toggles), plain-language labels, data-dir usage & semantic-engine status display.
|
|
48
48
|
9. **Style Baseline**: Six writing metrics (syntactic complexity / modifier density / abstraction / action density / hedging / gap index) + per-chapter μ±σ baseline band; `novel_style_report` outputs the band, `novel_style_check` compares new chapters (in-band ✓ / out-of-band ⚠); per-metric ±% tolerance configurable in the sidebar (**recommended = 1.5× σ of the book's chapter variance**, rounded, clamped to ±10%~100%; leave blank to use recommended) — free theme, writing style kept inside the band.
|
|
49
|
-
10. **Writing sentinels
|
|
49
|
+
10. **Writing sentinels**: `novel_continuity_check` extended — ①**bridge check** (`chapter`: time jumps / semantic distance / character continuity / hook handoff, with quoted evidence) ②**OOC check** (`ooc`: per-character emotion baseline deviation) ③**outline drift** (`outline`: direction vs body keyword overlap); **brief mode** for report tools.
|
|
50
50
|
11. **Original mode & creation files**: fill in creation settings in the sidebar (worldview/characters/forbidden/main conflict/genre/extras, blank = model decides, per-book profile library); novel_outline maintains creation files (bible/characters/outline/hooks/status), enforcing the bible → outline → hook chain with dynamic batches (10→20→30 chapters) to prevent plot jumps and OOC.
|
|
51
51
|
12. **Experience & stats**: main panel **library stats** (per-book chapters/total chars/7-day active chars, 🔥 green), **🎬 demo** (built-in sample, no files, runs the 6-dim baseline), **📊 report history** (analysis/style-reports browsing); actionable error hints; slimmer tool descriptions.
|
|
52
52
|
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://github.com/siweina/dsh-novel-writer/releases)
|
|
9
9
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
10
10
|
|
|
11
|
-
为 **DeepSeek Harness (DSH)**
|
|
11
|
+
为 **DeepSeek Harness (DSH)** 打造的小说写作助手插件:章节库管理、句式分析、情感净化与量化、氛围光谱、**风格画像报告**、**文笔六维基线带**、**写作哨兵三件套(衔接/OOC/大纲走偏)**、伏笔设定管理、本地语义检索(0 token)、网文信号识别、**原创模式与创作资料管理(设定书/动态大纲/钩子回填)**、批量导入与 AI 续写辅助。语义引擎依赖 `onnxruntime-web` 与 `@huggingface/tokenizers`(随包自动安装),浏览器端仅依赖 Web GUI 自带的 react。**要求 Node ≥ 22.3。**
|
|
12
12
|
|
|
13
13
|
> **致非中文用户**:本插件为中文小说分析写作而设计——句式、情感、意象等核心能力以及内置的语义模型,全部针对中文语料构建与调优。在深耕中文的同时兼顾英文等其他语言,确实超出了我目前的能力范围。若因此给您带来不便,我深感抱歉,恳请谅解。
|
|
14
14
|
|
|
@@ -39,14 +39,14 @@ dsh plugin --profile web add github:siweina/dsh-novel-writer#main
|
|
|
39
39
|
|
|
40
40
|
1. **风格画像报告**(novel_style_report):6 维测量报告——文风指纹 / 高频词汇 / 题材流派 / 情感量化 / 氛围光谱 12 轴 / 语义风格距离。**测量与判断分离**:插件只报数不贴标签,AI 判断可回传存盘(`.novel-writer/style-reports/`),续写保持风格一致。
|
|
41
41
|
2. **氛围光谱 12 轴**:噩梦感 / 焦虑压抑 / 温馨治愈 / 甜宠日常 / 催泪虐心 / 黑暗残酷 / 悬疑神秘 / 热血激昂 / 荒诞无厘头 / 孤独疏离 / 文艺唯美 / 情欲暧昧——证据链可追溯,0 token。
|
|
42
|
-
3. **本地语义引擎**:bge-small-zh 中文模型(
|
|
42
|
+
3. **本地语义引擎**:bge-small-zh 中文模型(24MB 随插件分发)本地 CPU 推理——`novel_semantic_search` 自然语言搜全书语义相关段落(带章节定位),语义级风格对比、语义隐性情感,懒加载 + 自动回退。
|
|
43
43
|
4. **句式模式分析**:九类句式分布、排列规律、句长节奏、情感曲线、风格指纹与节奏建议,带缓存与报告导出。
|
|
44
44
|
5. **情感净化 + 量化**:强/弱情绪词分级、污染源检测、caveat 预警 + AI 复核;Valence 滑动窗口 → 方差 V / 斜率 Δ / 矛盾指数 C + 隐性意象载体。
|
|
45
45
|
6. **世界观与语用检测**:文化基准自动判断(西/东/混合)+ 置信度;speechStyle 称谓/客套/仪式/语气规范;题材流派 + 网文信号。
|
|
46
46
|
7. **写作辅助全家桶**:伏笔登记表 / 设定五张表(人物·地点·道具·时间线·世界观)/ 章节摘要 / 连贯性审计 / 批量导入 / 风格自检 / 续写辅助。
|
|
47
47
|
8. **全工具 UI 开关**:侧边栏「写作助手功能」面板(总开关 + 工具开关分组 + 功能开关),大白话文案,显示数据目录占用与语义引擎状态。
|
|
48
48
|
9. **风格基线**:文笔六维测量(句法复杂度/修饰密度/抽象度/动作密度/不确定性/留白指数)+ 按章节 μ±σ 基线带;`novel_style_report` 输出基线带,`novel_style_check` 对照新章偏差(带内 ✓ / 出带 ⚠);侧边栏可自定义每维 ±% 容差(**推荐值 = 原著章节波动的 1.5 倍 σ**,自动取整、限 ±10%~100%;输入框留空即用推荐)——原创/续写时主题自由、写法保持在基线带内。
|
|
49
|
-
10.
|
|
49
|
+
10. **写作哨兵三件套**:`novel_continuity_check` 扩展——①**衔接检查**(chapter 参数:时间硬跳/语义距离/人物延续/钩子承接四路检测,带原文引用)②**OOC 检测**(ooc 参数:角色情绪基线偏离)③**大纲走偏**(outline 参数:方向行 vs 正文关键词重合);报告工具支持 **brief 精简模式**。
|
|
50
50
|
11. **原创模式与创作资料**:侧边栏填写创作设定(世界观/角色/禁忌/主线/题材/额外要求,留空=模型自定,多书独立设定库);novel_outline 维护创作资料(创作设定/人物/剧情大纲/钩子记录/创作状态卡),原创强制「设定书→大纲→钩子」链,动态批次(10→20→30 章)防剧情跳跃与角色 OOC。
|
|
51
51
|
12. **体验与统计**:主面板**书库统计卡**(每本章数/总字数/近 7 天活跃字数,活跃🔥标绿)、**🎬 体验演示**(内置示例不落盘跑六维基线)、**📊 报告历史**(analysis/style-reports 列表浏览);错误提示带解决步骤;工具说明压缩省 token。
|
|
52
52
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-novel-writer
|
|
1
|
+
# dsh-novel-writer v3.5.0 bundle patch: 16 tools (incl. novel_semantic_search) + local embedding engine.
|
|
2
2
|
# dsh-novel-writer bundle patch: mounts the novel-writing assistant plugin
|
|
3
3
|
# into the profile's loader tree. The entry's name is the plugin package
|
|
4
4
|
# itself, resolved from the profile's node_modules.
|
package/lib/analysis.js
CHANGED
|
@@ -55,18 +55,19 @@ const STRONG_EMOTION_WORDS = Object.freeze({
|
|
|
55
55
|
surprise: ["震惊", "目瞪口呆", "难以置信", "不可思议", "惊愕", "骇然", "震撼"]
|
|
56
56
|
});
|
|
57
57
|
const WEAK_EMOTION_WORDS = Object.freeze({
|
|
58
|
-
joy: ["兴奋", "满足", "愉快", "痛快", "爽快", "
|
|
59
|
-
anger: ["生气", "恼火", "
|
|
60
|
-
sorrow: ["难过", "伤心", "痛苦", "哭泣", "
|
|
58
|
+
joy: ["兴奋", "满足", "愉快", "痛快", "爽快", "笑眯眯", "笑容", "微笑", "哈哈", "高兴", "开心", "喜悦"],
|
|
59
|
+
anger: ["生气", "恼火", "气愤", "咬牙", "不满", "发火", "气冲冲"],
|
|
60
|
+
sorrow: ["难过", "伤心", "痛苦", "哭泣", "眼泪", "流泪", "哽咽", "抽泣", "叹息", "叹气", "失落", ],
|
|
61
61
|
fear: ["害怕", "惊慌", "不安", "紧张", "担心", "发抖", "哆嗦", "心慌", "忐忑", "心悸", "心虚", "惊惶"],
|
|
62
62
|
surprise: ["惊讶", "意外", "吃惊", "诧异", "愕然", "愣住", "傻眼", "呆住", "惊奇", "惊呆"]
|
|
63
63
|
});
|
|
64
|
+
// v3.5.0 M1:情感词表构建去重(欣慰/惊惶等同表重复——双计残留)
|
|
64
65
|
const EMOTION_WORDS = Object.freeze({
|
|
65
|
-
joy: [...STRONG_EMOTION_WORDS.joy, ...WEAK_EMOTION_WORDS.joy],
|
|
66
|
-
anger: [...STRONG_EMOTION_WORDS.anger, ...WEAK_EMOTION_WORDS.anger],
|
|
67
|
-
sorrow: [...STRONG_EMOTION_WORDS.sorrow, ...WEAK_EMOTION_WORDS.sorrow],
|
|
68
|
-
fear: [...STRONG_EMOTION_WORDS.fear, ...WEAK_EMOTION_WORDS.fear],
|
|
69
|
-
surprise: [...STRONG_EMOTION_WORDS.surprise, ...WEAK_EMOTION_WORDS.surprise]
|
|
66
|
+
joy: [...new Set([...STRONG_EMOTION_WORDS.joy, ...WEAK_EMOTION_WORDS.joy])],
|
|
67
|
+
anger: [...new Set([...STRONG_EMOTION_WORDS.anger, ...WEAK_EMOTION_WORDS.anger])],
|
|
68
|
+
sorrow: [...new Set([...STRONG_EMOTION_WORDS.sorrow, ...WEAK_EMOTION_WORDS.sorrow])],
|
|
69
|
+
fear: [...new Set([...STRONG_EMOTION_WORDS.fear, ...WEAK_EMOTION_WORDS.fear])],
|
|
70
|
+
surprise: [...new Set([...STRONG_EMOTION_WORDS.surprise, ...WEAK_EMOTION_WORDS.surprise])]
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
/** v1.5.0 情绪污染源词表:检测到高密度时降低情感可信度并提示 AI 复核。 */
|
|
@@ -131,6 +132,8 @@ export function splitBlocks(text) {
|
|
|
131
132
|
let current = [];
|
|
132
133
|
for (const line of lines) {
|
|
133
134
|
const trimmed = line.trim();
|
|
135
|
+
// v3.5.0 #51:Markdown 标题(# 开头)不是句子——跳过(含空行场景)
|
|
136
|
+
if (/^#{1,6}\s/.test(trimmed)) continue;
|
|
134
137
|
if (trimmed === "") {
|
|
135
138
|
if (current.length > 0) {
|
|
136
139
|
blocks.push(current.join("\n").trim());
|
|
@@ -154,6 +157,13 @@ export function splitSentences(block) {
|
|
|
154
157
|
let buffer = "";
|
|
155
158
|
for (let i = 0; i < block.length; i += 1) {
|
|
156
159
|
const ch = block[i];
|
|
160
|
+
// v3.5.0 #50b:无标点换行也是句边界(逐行排版正确分句,整篇不再算 1 句)
|
|
161
|
+
if (ch === "\n" || ch === "\r") {
|
|
162
|
+
const lineSentence = buffer.trim();
|
|
163
|
+
if (lineSentence !== "") sentences.push(lineSentence);
|
|
164
|
+
buffer = "";
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
157
167
|
buffer += ch;
|
|
158
168
|
if (TERMINATORS.includes(ch)) {
|
|
159
169
|
while (i + 1 < block.length && TERMINATORS.includes(block[i + 1])) {
|
|
@@ -267,7 +277,7 @@ export function classifySentence(raw) {
|
|
|
267
277
|
/** 单字情感词的多字搭配排除("叹气"不算"气"、"笑道"不算"笑")。 */
|
|
268
278
|
const EMOTION_EXCLUDE = {
|
|
269
279
|
joy: { "笑": ["笑道", "说笑", "玩笑", "搞笑", "笑话", "苦笑", "冷笑", "嘲笑", "嬉笑", "傻笑", "微笑", "笑死", "笑呵呵", "笑眯眯"] },
|
|
270
|
-
anger: { "气": ["叹气", "运气", "客气", "一口气", "服气", "脾气", "争气", "节气", "淘气", "景气", "暖气", "口气", "名气", "勇气", "底气", "俗气", "风气", "香气", "水气", "湿气", "潮气", "和气", "生气勃勃"] },
|
|
280
|
+
anger: { "气": ["叹气", "运气", "客气", "一口气", "服气", "脾气", "争气", "节气", "淘气", "景气", "暖气", "口气", "名气", "勇气", "底气", "俗气", "风气", "香气", "水气", "湿气", "潮气", "和气", "生气勃勃"], "生气": ["生气勃勃"] },
|
|
271
281
|
sorrow: { "悲": ["慈悲"], "哭": ["哭丧"] },
|
|
272
282
|
fear: { "怕": ["哪怕", "只怕", "生怕"] },
|
|
273
283
|
surprise: { "愣": ["愣住"] }
|
|
@@ -296,10 +306,12 @@ function emotionOf(text) {
|
|
|
296
306
|
// 单字词的搭配排除(如"叹气"中的"气")
|
|
297
307
|
const exclude = EMOTION_EXCLUDE[emotion]?.[word];
|
|
298
308
|
if (exclude !== void 0) {
|
|
309
|
+
// v3.5.0 #54:窗口按最长排除词取("生气勃勃"=4 字,旧窗口永远命不中)
|
|
310
|
+
const maxLen = Math.max(4, ...exclude.map(function (x) { return x.length; }));
|
|
299
311
|
const two = text.slice(Math.max(0, idx - 1), idx + 2);
|
|
300
312
|
const prevTwo = text.slice(Math.max(0, idx - 1), idx + 1);
|
|
301
313
|
const nextTwo = text.slice(idx, idx + 2);
|
|
302
|
-
const prevFour = text.slice(Math.max(0, idx -
|
|
314
|
+
const prevFour = text.slice(Math.max(0, idx - maxLen), idx + maxLen);
|
|
303
315
|
let excluded = false;
|
|
304
316
|
for (const bad of exclude) {
|
|
305
317
|
if (two.includes(bad) || prevTwo === bad || nextTwo === bad || prevFour.includes(bad)) {
|
|
@@ -433,22 +445,20 @@ export function buildGuidance(ratios, topPatterns, chapterSequences) {
|
|
|
433
445
|
*/
|
|
434
446
|
const VALENCE_WORDS = Object.freeze({
|
|
435
447
|
"欣喜": 0.9, "狂喜": 0.9, "欢天喜地": 0.9, "雀跃": 0.7, "高兴": 0.7, "开心": 0.7, "快乐": 0.7,
|
|
436
|
-
"喜悦": 0.7, "愉快": 0.5, "欢喜": 0.7, "兴奋": 0.7, "愉悦": 0.5, "欢快": 0.7, "
|
|
437
|
-
"
|
|
438
|
-
"欣慰": 0.5, "满足": 0.3, "痛快": 0.5, "爽快": 0.3, "甜": 0.5, "甜蜜": 0.5, "幸福": 0.9,
|
|
448
|
+
"喜悦": 0.7, "愉快": 0.5, "欢喜": 0.7, "兴奋": 0.7, "愉悦": 0.5, "欢快": 0.7, "微笑": 0.3, "笑容": 0.3, "笑眯眯": 0.3, "哈哈": 0.3,
|
|
449
|
+
"欣慰": 0.5, "满足": 0.3, "痛快": 0.5, "爽快": 0.3, "甜蜜": 0.5, "幸福": 0.9,
|
|
439
450
|
"美满": 0.9, "温馨": 0.5, "温暖": 0.3, "踏实": 0.1, "平静": 0.1, "释然": 0.4, "解脱": 0.4,
|
|
440
|
-
"喜欢": 0.7, "喜爱": 0.7, "欣赏": 0.5, "
|
|
451
|
+
"喜欢": 0.7, "喜爱": 0.7, "欣赏": 0.5, "疼爱": 0.7, "宠爱": 0.7, "仰慕": 0.7,
|
|
441
452
|
"尊敬": 0.5, "敬仰": 0.7, "崇拜": 0.7, "心动": 0.5, "眷恋": 0.7, "依恋": 0.7, "思念": 0.3,
|
|
442
453
|
"心疼": 0.3, "怜爱": 0.5, "温柔": 0.3, "珍惜": 0.5, "信赖": 0.5, "感恩": 0.7,
|
|
443
454
|
"愤怒": -0.9, "暴怒": -0.9, "怒发冲冠": -0.9, "火冒三丈": -0.9, "恼羞成怒": -0.9,
|
|
444
|
-
"生气": -0.7, "恼火": -0.7, "气愤": -0.7, "
|
|
445
|
-
"厌恶": -0.7, "憎恶": -0.9, "不满": -0.5, "
|
|
455
|
+
"生气": -0.7, "恼火": -0.7, "气愤": -0.7, "怨恨": -0.9, "憎恨": -0.9,
|
|
456
|
+
"厌恶": -0.7, "憎恶": -0.9, "不满": -0.5, "发火": -0.7, "咬牙": -0.3,
|
|
446
457
|
"怒意": -0.7, "怒火": -0.9, "愤恨": -0.9, "恼羞": -0.7, "气冲冲": -0.7, "咬牙切齿": -0.5,
|
|
447
458
|
"悲伤": -0.7, "悲痛": -0.9, "悲痛欲绝": -0.9, "悲哀": -0.7, "哀伤": -0.7, "难过": -0.5,
|
|
448
|
-
"伤心": -0.7, "痛苦": -0.7, "心碎": -0.9, "绝望": -0.9, "哭泣": -0.7, "
|
|
449
|
-
"眼泪": -0.3, "流泪": -0.5, "哽咽": -0.5, "抽泣": -0.5, "叹息": -0.3, "叹气": -0.3,
|
|
459
|
+
"伤心": -0.7, "痛苦": -0.7, "心碎": -0.9, "绝望": -0.9, "哭泣": -0.7, "眼泪": -0.3, "流泪": -0.5, "哽咽": -0.5, "抽泣": -0.5, "叹息": -0.3, "叹气": -0.3,
|
|
450
460
|
"惆怅": -0.5, "失落": -0.5, "忧伤": -0.5, "黯然": -0.5, "心酸": -0.7, "辛酸": -0.7,
|
|
451
|
-
"
|
|
461
|
+
"凄凉": -0.7, "苦涩": -0.7, "苦闷": -0.5, "沮丧": -0.7, "消沉": -0.7,
|
|
452
462
|
"落寞": -0.5, "孤寂": -0.5, "郁闷": -0.3, "低落": -0.3, "压抑": -0.5, "心灰意冷": -0.9,
|
|
453
463
|
"恐惧": -0.9, "害怕": -0.7, "惊慌": -0.7, "不安": -0.3, "紧张": -0.3, "担心": -0.3,
|
|
454
464
|
"畏惧": -0.7, "惊恐": -0.9, "胆怯": -0.5, "发抖": -0.3, "哆嗦": -0.3, "心慌": -0.5,
|
|
@@ -592,9 +602,11 @@ export function implicitEmotionScan(text, semResolver = null) {
|
|
|
592
602
|
else { posHits += n * valence; if (false) fragileHits += n; }
|
|
593
603
|
carrierCounts.set(label + ":" + word, (carrierCounts.get(label + ":" + word) ?? 0) + n);
|
|
594
604
|
};
|
|
595
|
-
// ①
|
|
605
|
+
// ① 单方向表(原有)——v3.5.0 M2:跳过与多方向表重叠的词(雨/黄昏/烛火等),防同一出现计 2-3 次(语境裁决交给 ②)
|
|
606
|
+
const ambWords = new Set(Object.keys(AMBIGUOUS_CARRIERS));
|
|
596
607
|
for (const carrier of IMPLICIT_CARRIERS) {
|
|
597
608
|
for (const word of carrier.words) {
|
|
609
|
+
if (ambWords.has(word)) continue;
|
|
598
610
|
const n = text.split(word).length - 1;
|
|
599
611
|
if (n > 0) {
|
|
600
612
|
see(carrier.label, word, n, carrier.valence);
|
|
@@ -682,11 +694,13 @@ export function valenceSeries(text, winChars = 100) {
|
|
|
682
694
|
|
|
683
695
|
/** v1.6.0 三指标:方差 V + 相邻撕裂 V_adj + 斜率 Δ(最小二乘+鲁棒版)+ 矛盾指数 C。 */
|
|
684
696
|
export function valenceStats(text) {
|
|
685
|
-
|
|
697
|
+
// v3.5.0 #57:一次 valenceSeries 取全部(旧版窗口统计二次调用,大书白扫一遍词表)
|
|
698
|
+
const { series, posWords, negWords, windowPosNeg } = valenceSeries(text);
|
|
686
699
|
const n = series.length;
|
|
687
700
|
if (n === 0) return { windows: 0 };
|
|
688
701
|
const mean = series.reduce((x, y) => x + y, 0) / n;
|
|
689
|
-
|
|
702
|
+
// v3.6.0:样本方差口径 /(n-1)(n=1 时无方差=0)
|
|
703
|
+
const variance = n > 1 ? series.reduce((s, x) => s + (x - mean) ** 2, 0) / (n - 1) : 0;
|
|
690
704
|
let adjSum = 0;
|
|
691
705
|
for (let i = 1; i < n; i += 1) adjSum += Math.abs(series[i] - series[i - 1]);
|
|
692
706
|
const adjVariance = n > 1 ? adjSum / (n - 1) : 0;
|
|
@@ -701,7 +715,7 @@ export function valenceStats(text) {
|
|
|
701
715
|
const third = Math.max(1, Math.floor(n / 3));
|
|
702
716
|
const headMean = series.slice(0, third).reduce((x, y) => x + y, 0) / third;
|
|
703
717
|
const tailMean = series.slice(-third).reduce((x, y) => x + y, 0) / third;
|
|
704
|
-
|
|
718
|
+
// v3.5.0 #57:复用上面的 windowPosNeg(不再二次扫描)
|
|
705
719
|
const totalWords = posWords + negWords;
|
|
706
720
|
const posRatio = totalWords === 0 ? 0 : posWords / totalWords;
|
|
707
721
|
const negRatio = totalWords === 0 ? 0 : negWords / totalWords;
|
|
@@ -855,8 +869,11 @@ export function emotionalQuantification(text, perChapter, blocks, semResolver =
|
|
|
855
869
|
* @returns 结构化分析结果(与 novel_sentence_analysis 输出 schema 一致)。
|
|
856
870
|
*/
|
|
857
871
|
export function analyzeText(text, options = {}) {
|
|
872
|
+
// v3.5.0 M6:入口容错(null/undefined 不崩溃)
|
|
873
|
+
text = String(text ?? "");
|
|
858
874
|
const top = Number.isInteger(options.top) ? options.top : 8;
|
|
859
|
-
|
|
875
|
+
// v3.5.0 #63:maxSentences 钳制 ≥1(0/负数会让 slice(0,-5) 产生错误语义)
|
|
876
|
+
const maxSentences = Math.max(1, Number.isInteger(options.maxSentences) ? options.maxSentences : 20000);
|
|
860
877
|
const curveSegments = Number.isInteger(options.curveSegments) ? Math.min(Math.max(options.curveSegments, 1), 50) : 20;
|
|
861
878
|
const blocks = splitBlocks(text);
|
|
862
879
|
const sentences = [];
|
|
@@ -1097,7 +1114,21 @@ export function analyzeText(text, options = {}) {
|
|
|
1097
1114
|
// 主观性指数(启发式 0-100)
|
|
1098
1115
|
const psychRatio = totalSentences === 0 ? 0 : counts.psychology / totalSentences;
|
|
1099
1116
|
const exclaimRatio = totalSentences === 0 ? 0 : counts.exclamation / totalSentences;
|
|
1100
|
-
|
|
1117
|
+
// v3.6.0:第一人称最长匹配优先("我们"不拆成 我+我们 双计)
|
|
1118
|
+
const firstPersonCount = (function () {
|
|
1119
|
+
let fpSum = 0;
|
|
1120
|
+
const fpSorted = FIRST_PERSON_WORDS.slice().sort((x, y) => y.length - x.length);
|
|
1121
|
+
let fpText = text;
|
|
1122
|
+
for (const w of fpSorted) {
|
|
1123
|
+
if (w.length < 2) continue;
|
|
1124
|
+
const parts = fpText.split(w);
|
|
1125
|
+
fpSum += parts.length - 1;
|
|
1126
|
+
fpText = parts.join("\u0000".repeat(w.length));
|
|
1127
|
+
}
|
|
1128
|
+
// 剩余孤立单字"我"
|
|
1129
|
+
fpSum += (fpText.match(/我/g) || []).length;
|
|
1130
|
+
return fpSum;
|
|
1131
|
+
})();
|
|
1101
1132
|
const firstPersonDensity = totalChars === 0 ? 0 : (firstPersonCount / totalChars) * 1000;
|
|
1102
1133
|
const subjectivityIndex = Math.min(100, Math.round(
|
|
1103
1134
|
psychRatio * 50 + exclaimRatio * 60 + Math.min(emotionDensity * 2.5, 25) + Math.min(firstPersonDensity * 1.2, 20)
|
|
@@ -1191,6 +1222,8 @@ function emotionCurve(blockMeta, maxSegments) {
|
|
|
1191
1222
|
intensity: chars === 0 ? 0 : round((total / chars) * 1000, 2)
|
|
1192
1223
|
});
|
|
1193
1224
|
}
|
|
1225
|
+
// v3.6.0 复审修正:不裁剪——强度 0 段是"平缓段"(跨章节曲线对齐需要固定段数契约);
|
|
1226
|
+
// segmentCount=min(blocks, maxSegments) 已保证每段至少 1 块,无空 slice
|
|
1194
1227
|
return curve;
|
|
1195
1228
|
}
|
|
1196
1229
|
|
|
@@ -1269,7 +1302,8 @@ const DENSITY_SENSE = {
|
|
|
1269
1302
|
/** 细节密度统计(v0.8.0):动作链/物件名词/感官词,按千字归一。 */
|
|
1270
1303
|
export function densityOf(text) {
|
|
1271
1304
|
const sentences = splitSentences(text);
|
|
1272
|
-
|
|
1305
|
+
// v3.5.0 #62:分母口径统一(含空白,与 style-metrics 的 per1000 一致,跨模块数值可比)
|
|
1306
|
+
const totalChars = String(text).length;
|
|
1273
1307
|
let actionVerbs = 0;
|
|
1274
1308
|
let actionChainSentences = 0;
|
|
1275
1309
|
let objectHits = 0;
|
|
@@ -1316,7 +1350,7 @@ function loadDutir() {
|
|
|
1316
1350
|
}
|
|
1317
1351
|
return DUTIR;
|
|
1318
1352
|
}
|
|
1319
|
-
const DUTIR_TO_EMOTION = { 乐: "joy", 好: "joy", 怒: "anger", 哀: "sorrow", 惧: "fear", 恶: "
|
|
1353
|
+
const DUTIR_TO_EMOTION = { 乐: "joy", 好: "joy", 怒: "anger", 哀: "sorrow", 惧: "fear", 恶: "anger", 惊: "surprise" }; // v3.5.0 H4:恶(厌恶) 归 anger 而非 sorrow
|
|
1320
1354
|
const dutirLookup = new Map();
|
|
1321
1355
|
function dutirEmotion(word) {
|
|
1322
1356
|
if (dutirLookup.size === 0) {
|
|
@@ -1324,7 +1358,8 @@ function dutirEmotion(word) {
|
|
|
1324
1358
|
for (const [cat, words] of Object.entries(dutir)) {
|
|
1325
1359
|
const emo = DUTIR_TO_EMOTION[cat];
|
|
1326
1360
|
if (!emo) continue;
|
|
1327
|
-
|
|
1361
|
+
// v3.5.0 H4:跨类冲突词首见保留(后写不再覆盖——"开心"不会被 恶 类覆盖成 sorrow)
|
|
1362
|
+
for (const w of words) if (typeof w === "string" && w.length >= 2 && !dutirLookup.has(w)) dutirLookup.set(w, emo);
|
|
1328
1363
|
}
|
|
1329
1364
|
}
|
|
1330
1365
|
return dutirLookup.get(word);
|