130 lines
3.8 KiB
JavaScript
130 lines
3.8 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════════════
|
||
// Entity Recognition & Relation Graph
|
||
// 实体识别与关系扩散
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* 从文本中匹配已知实体
|
||
* @param {string} text - 待匹配文本
|
||
* @param {Set<string>} knownEntities - 已知实体集合
|
||
* @returns {string[]} - 匹配到的实体
|
||
*/
|
||
export function matchEntities(text, knownEntities) {
|
||
if (!text || !knownEntities?.size) return [];
|
||
|
||
const matched = new Set();
|
||
|
||
for (const entity of knownEntities) {
|
||
// 精确包含
|
||
if (text.includes(entity)) {
|
||
matched.add(entity);
|
||
continue;
|
||
}
|
||
|
||
// 处理简称:如果实体是"林黛玉",文本包含"黛玉"
|
||
if (entity.length >= 3) {
|
||
const shortName = entity.slice(-2); // 取后两字
|
||
if (text.includes(shortName)) {
|
||
matched.add(entity);
|
||
}
|
||
}
|
||
}
|
||
|
||
return Array.from(matched);
|
||
}
|
||
|
||
/**
|
||
* 从角色数据和事件中收集所有已知实体
|
||
*/
|
||
export function collectKnownEntities(characters, events) {
|
||
const entities = new Set();
|
||
|
||
// 从主要角色
|
||
(characters?.main || []).forEach(m => {
|
||
const name = typeof m === 'string' ? m : m.name;
|
||
if (name) entities.add(name);
|
||
});
|
||
|
||
// 从关系
|
||
(characters?.relationships || []).forEach(r => {
|
||
if (r.from) entities.add(r.from);
|
||
if (r.to) entities.add(r.to);
|
||
});
|
||
|
||
// 从事件参与者
|
||
(events || []).forEach(e => {
|
||
(e.participants || []).forEach(p => {
|
||
if (p) entities.add(p);
|
||
});
|
||
});
|
||
|
||
return entities;
|
||
}
|
||
|
||
/**
|
||
* 构建关系邻接表
|
||
* @param {Array} relationships - 关系数组
|
||
* @returns {Map<string, Array<{target: string, weight: number}>>}
|
||
*/
|
||
export function buildRelationGraph(relationships) {
|
||
const graph = new Map();
|
||
|
||
const trendWeight = {
|
||
'交融': 1.0,
|
||
'亲密': 0.9,
|
||
'投缘': 0.7,
|
||
'陌生': 0.3,
|
||
'反感': 0.5,
|
||
'厌恶': 0.6,
|
||
'破裂': 0.7,
|
||
};
|
||
|
||
for (const rel of relationships || []) {
|
||
if (!rel.from || !rel.to) continue;
|
||
|
||
const weight = trendWeight[rel.trend] || 0.5;
|
||
|
||
// 双向
|
||
if (!graph.has(rel.from)) graph.set(rel.from, []);
|
||
if (!graph.has(rel.to)) graph.set(rel.to, []);
|
||
|
||
graph.get(rel.from).push({ target: rel.to, weight });
|
||
graph.get(rel.to).push({ target: rel.from, weight });
|
||
}
|
||
|
||
return graph;
|
||
}
|
||
|
||
/**
|
||
* 关系扩散(1跳)
|
||
* @param {string[]} focusEntities - 焦点实体
|
||
* @param {Map} graph - 关系图
|
||
* @param {number} decayFactor - 衰减因子
|
||
* @returns {Map<string, number>} - 实体 -> 激活分数
|
||
*/
|
||
export function spreadActivation(focusEntities, graph, decayFactor = 0.5) {
|
||
const activation = new Map();
|
||
|
||
// 焦点实体初始分数 1.0
|
||
for (const entity of focusEntities) {
|
||
activation.set(entity, 1.0);
|
||
}
|
||
|
||
// 1跳扩散
|
||
for (const entity of focusEntities) {
|
||
const neighbors = graph.get(entity) || [];
|
||
|
||
for (const { target, weight } of neighbors) {
|
||
const spreadScore = weight * decayFactor;
|
||
const existing = activation.get(target) || 0;
|
||
|
||
// 取最大值,不累加
|
||
if (spreadScore > existing) {
|
||
activation.set(target, spreadScore);
|
||
}
|
||
}
|
||
}
|
||
|
||
return activation;
|
||
}
|