feat: 完成 M 群組 — 既有作品考據與輕小說章節管線
共用場景資料庫(Scene/ScenePresence/SceneLine/SceneFact/SceneInteraction, 角色中立客觀記錄)與投影(ProjectionService 寫入 EpisodicMemory/ SemanticMemory/PersonalityTraitSnapshot/CharacterRelationshipEventLog, 逐一可追溯到來源場景)完全分層。歸屬信心分數(明示標記決定性、語言指紋 從已歸屬語料統計、場景名冊硬約束、對話輪替)與隔離區/指紋冷啟動(低信心 或樣本不足一律隔離,指紋精煉後可重新嘗試釋放)。多角色投影與修正(改一處 場景資料庫、reprojectScene 同步更新所有受影響角色,使用者互動記憶因為 從不帶 sourceSceneId 而結構性地不受影響,發現得晚的歸屬錯誤額外記一筆 CORRECTION 事件)。作品名冊建置門檻(達標自動建置,門檻檢查排在投影完成 之後才做,避免用到投影前的舊統計)。錨定點知識邊界(雙向:前進載入、 後退遺忘)與矛盾偵測(同優先序來源待人工確認,跨優先序可依小說原文> 官方設定集>動畫改編自動裁決)。插圖索引與衍生服裝目錄/姿勢語彙表。 刻意簡化:章節→場景切分與互動正負權重皆採結構化輸入(不做自動 NLP 場景邊界偵測與情感分析),比照 F-7 MockProvider 的做法——機制先做對, 之後有真實 NLP/LLM 能力再替換輸入來源。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cd948f0026
commit
c66799ee7c
@@ -0,0 +1,67 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service.js";
|
||||
import { SOURCE_PRIORITY } from "./constants.js";
|
||||
|
||||
// M-8 跨章節整合:共用年表排序(Scene.storyOrder 本身即年表鍵,不需要另外的服務排序)、
|
||||
// 矛盾偵測與裁決優先序(小說原文 > 官方設定集 > 動畫改編),無法裁決者標記待人工確認。
|
||||
@Injectable()
|
||||
export class TimelineService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
// 掃描同一事件標籤下、同一事實鍵卻不同事實值的場景——這是故意置入矛盾最容易驗證的形式。
|
||||
async detectContradictions(workId: string): Promise<number> {
|
||||
const facts = await this.prisma.client.sceneFact.findMany({
|
||||
where: { scene: { workId } },
|
||||
include: { scene: true },
|
||||
});
|
||||
|
||||
const groups = new Map<string, typeof facts>();
|
||||
for (const fact of facts) {
|
||||
const key = `${fact.eventLabel}::${fact.factKey}`;
|
||||
const group = groups.get(key) ?? [];
|
||||
group.push(fact);
|
||||
groups.set(key, group);
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
for (const [key, group] of groups) {
|
||||
const distinctValues = new Set(group.map((fact) => fact.factValue));
|
||||
if (distinctValues.size <= 1) {
|
||||
continue;
|
||||
}
|
||||
const [eventLabel, factKey] = key.split("::");
|
||||
const existing = await this.prisma.client.sceneContradiction.findFirst({ where: { workId, eventLabel, factKey } });
|
||||
if (existing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sorted = [...group].sort(
|
||||
(a, b) => (SOURCE_PRIORITY[a.scene.sourceType] ?? 99) - (SOURCE_PRIORITY[b.scene.sourceType] ?? 99),
|
||||
);
|
||||
const best = sorted[0];
|
||||
const runnerUpDifferentValue = sorted.find((fact) => fact.factValue !== best.factValue);
|
||||
const canAutoResolve =
|
||||
runnerUpDifferentValue !== undefined &&
|
||||
(SOURCE_PRIORITY[best.scene.sourceType] ?? 99) < (SOURCE_PRIORITY[runnerUpDifferentValue.scene.sourceType] ?? 99);
|
||||
|
||||
await this.prisma.client.sceneContradiction.create({
|
||||
data: {
|
||||
workId,
|
||||
eventLabel,
|
||||
factKey,
|
||||
description: `「${eventLabel}」的「${factKey}」出現不同描述:${[...distinctValues].join(" vs ")}`,
|
||||
status: canAutoResolve ? "RESOLVED" : "PENDING_HUMAN_REVIEW",
|
||||
resolution: canAutoResolve
|
||||
? `依裁決優先序(小說原文 > 官方設定集 > 動畫改編)採用「${best.factValue}」(來源:${best.scene.sourceType})`
|
||||
: null,
|
||||
},
|
||||
});
|
||||
created += 1;
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
async listContradictions(workId: string) {
|
||||
return this.prisma.client.sceneContradiction.findMany({ where: { workId } });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user