@lingjingai/scriptctl 0.28.0 → 0.28.2
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/README.md +1 -1
- package/changes/0.27.2.md +13 -0
- package/changes/0.28.0.md +14 -0
- package/changes/0.28.1.md +13 -0
- package/changes/0.28.2.md +13 -0
- package/changes/unreleased.md +2 -3
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/common.js +45 -7
- package/dist/common.js.map +1 -1
- package/dist/domain/direct-core.d.ts +31 -0
- package/dist/domain/direct-core.js +436 -65
- package/dist/domain/direct-core.js.map +1 -1
- package/dist/domain/script/validate.js +23 -1
- package/dist/domain/script/validate.js.map +1 -1
- package/dist/help-text.js +4 -6
- package/dist/help-text.js.map +1 -1
- package/dist/usecases/direct.js +151 -88
- package/dist/usecases/direct.js.map +1 -1
- package/dist/usecases/parse.js +14 -6
- package/dist/usecases/parse.js.map +1 -1
- package/dist/usecases/update.js +6 -6
- package/dist/usecases/update.js.map +1 -1
- package/package.json +1 -1
|
@@ -12,13 +12,31 @@ export const _TECHNICAL_EP_TITLE_RE = /^(?:ep|episode)[\s_\-#:.]*[0-90-9]+$/
|
|
|
12
12
|
const _TIME_MAP = {
|
|
13
13
|
"日": "day", "白天": "day", "上午": "day", "下午": "day", "午后": "day", "中午": "day",
|
|
14
14
|
"晨": "day", "清晨": "day", "早晨": "day", "黎明": "day",
|
|
15
|
-
"夜": "night", "夜晚": "night", "深夜": "night", "凌晨": "night", "黄昏": "night", "傍晚": "night",
|
|
15
|
+
"夜": "night", "夜晚": "night", "夜间": "night", "夜里": "night", "深夜": "night", "凌晨": "night", "黄昏": "night", "傍晚": "night",
|
|
16
|
+
};
|
|
17
|
+
const _LOCATION_TIME_STATE_MAP = {
|
|
18
|
+
"白天": "白天",
|
|
19
|
+
"日间": "白天",
|
|
20
|
+
"上午": "上午",
|
|
21
|
+
"下午": "下午",
|
|
22
|
+
"午后": "午后",
|
|
23
|
+
"中午": "中午",
|
|
24
|
+
"清晨": "清晨",
|
|
25
|
+
"早晨": "早晨",
|
|
26
|
+
"黎明": "黎明",
|
|
27
|
+
"夜晚": "夜晚",
|
|
28
|
+
"夜间": "夜晚",
|
|
29
|
+
"夜里": "夜晚",
|
|
30
|
+
"深夜": "深夜",
|
|
31
|
+
"凌晨": "凌晨",
|
|
32
|
+
"黄昏": "黄昏",
|
|
33
|
+
"傍晚": "黄昏",
|
|
16
34
|
};
|
|
17
35
|
const _SPACE_MAP = {
|
|
18
36
|
"内": "interior", "外": "exterior", "内/外": "interior", "外/内": "exterior",
|
|
19
37
|
};
|
|
20
|
-
const _SCENE_TIME_FIRST_RE = /^(?:场)?(\d+)-(\d+)\s+(
|
|
21
|
-
const _SCENE_LOCATION_FIRST_RE = /^(?:场)?(\d+)-(\d+)\s+(.+?)\s+(
|
|
38
|
+
const _SCENE_TIME_FIRST_RE = /^(?:场)?(\d+)-(\d+)\s+(日|夜|晨|清晨|黄昏|午后|凌晨|夜晚|夜间|夜里|深夜|黎明|白天|傍晚|中午|上午|下午|早晨)\s+((?:内|外|内\/外|外\/内))\s+(.+?)\s*$/;
|
|
39
|
+
const _SCENE_LOCATION_FIRST_RE = /^(?:场)?(\d+)-(\d+)\s+(.+?)\s+(日|夜|晨|清晨|黄昏|午后|凌晨|夜晚|夜间|夜里|深夜|黎明|白天|傍晚|中午|上午|下午|早晨)\s+((?:内|外|内\/外|外\/内))\s*$/;
|
|
22
40
|
const _CHAR_RE = /^(?:登场|出场)?人物[::](.+)$/;
|
|
23
41
|
const _PROP_RE = /^道具[::](.+)$/;
|
|
24
42
|
const _ACTION_RE = /^[▲△]\s*(.+)$/;
|
|
@@ -47,7 +65,7 @@ const _MD_TOP_HEADER_RE = /^#\s+(.+?)\s*$/;
|
|
|
47
65
|
const _MD_SUB_HEADER_RE = /^##\s+(.+?)\s*$/;
|
|
48
66
|
const _MD_SCENE_HEADER_RE = /^##\s*场景\s*(\d+)\s*$/;
|
|
49
67
|
const _MD_SCENE_FIELD_RE = /^-\s*(时空|地点|角色|道具)\s*[::]\s*(.+?)\s*$/;
|
|
50
|
-
const _MD_TIME_SPACE_RE = /^(内|外|interior|exterior|in|out)\s+(
|
|
68
|
+
const _MD_TIME_SPACE_RE = /^(内|外|interior|exterior|in|out)\s+(\S+)$/i;
|
|
51
69
|
const _MD_CHAR_ENTRY_RE = /^([^()(),,]+?)(?:[((]([^))]+)[))])?$/;
|
|
52
70
|
const _MD_ACTION_ANCHOR_RE = /^>\s*\[([A-Za-z]+)(?:\|([^|\]]+?)(?:\|([^|\]]+?))?)?\]\s*(.*)$/;
|
|
53
71
|
const _MD_ASSET_ENTRY_RE = /^-\s*([^::]+?)(?:\s*[::]\s*(.+?))?\s*$/;
|
|
@@ -494,6 +512,17 @@ export function splitLocationState(raw) {
|
|
|
494
512
|
}
|
|
495
513
|
return [locName, states.length > 0 ? states[states.length - 1] : null];
|
|
496
514
|
}
|
|
515
|
+
function locationTimeState(raw) {
|
|
516
|
+
const token = strOf(raw).trim();
|
|
517
|
+
if (!token || token === "日" || token === "夜")
|
|
518
|
+
return null;
|
|
519
|
+
return _LOCATION_TIME_STATE_MAP[token] ?? null;
|
|
520
|
+
}
|
|
521
|
+
function coarseSceneTime(raw) {
|
|
522
|
+
const token = strOf(raw).trim();
|
|
523
|
+
const lower = token.toLowerCase();
|
|
524
|
+
return _MD_TIME_NORM[lower] ?? _MD_TIME_NORM[token] ?? _TIME_MAP[token] ?? "day";
|
|
525
|
+
}
|
|
497
526
|
export function stateLabelError(stateName) {
|
|
498
527
|
const text = strOf(stateName).trim().replace(/\s+/g, "");
|
|
499
528
|
if (!text)
|
|
@@ -538,22 +567,28 @@ export function stripLocationState(location) {
|
|
|
538
567
|
export function parseSceneHeader(line) {
|
|
539
568
|
const timeFirst = reMatch(_SCENE_TIME_FIRST_RE, line);
|
|
540
569
|
if (timeFirst) {
|
|
570
|
+
const timeToken = timeFirst[3];
|
|
541
571
|
const [locName, locState] = stripLocationState(timeFirst[5].trim());
|
|
572
|
+
const locTimeState = locationTimeState(timeToken);
|
|
542
573
|
return {
|
|
543
574
|
space: _SPACE_MAP[timeFirst[4]] ?? "interior",
|
|
544
|
-
time: _TIME_MAP[
|
|
575
|
+
time: _TIME_MAP[timeToken] ?? "day",
|
|
545
576
|
location_name: locName || "未知场景",
|
|
546
577
|
location_state: locState,
|
|
578
|
+
...(locTimeState ? { location_time_state: locTimeState } : {}),
|
|
547
579
|
};
|
|
548
580
|
}
|
|
549
581
|
const locFirst = reMatch(_SCENE_LOCATION_FIRST_RE, line);
|
|
550
582
|
if (locFirst) {
|
|
583
|
+
const timeToken = locFirst[4];
|
|
551
584
|
const [locName, locState] = stripLocationState(locFirst[3].trim());
|
|
585
|
+
const locTimeState = locationTimeState(timeToken);
|
|
552
586
|
return {
|
|
553
587
|
space: _SPACE_MAP[locFirst[5]] ?? "interior",
|
|
554
|
-
time: _TIME_MAP[
|
|
588
|
+
time: _TIME_MAP[timeToken] ?? "day",
|
|
555
589
|
location_name: locName || "未知场景",
|
|
556
590
|
location_state: locState,
|
|
591
|
+
...(locTimeState ? { location_time_state: locTimeState } : {}),
|
|
557
592
|
};
|
|
558
593
|
}
|
|
559
594
|
return null;
|
|
@@ -615,6 +650,7 @@ export function deterministicExtractEpisode(sourceText, episodePlan) {
|
|
|
615
650
|
environment: { space: sceneHeader["space"], time: sceneHeader["time"] },
|
|
616
651
|
location_name: sceneHeader["location_name"],
|
|
617
652
|
location_state: sceneHeader["location_state"],
|
|
653
|
+
location_time_state: sceneHeader["location_time_state"],
|
|
618
654
|
actor_names: [],
|
|
619
655
|
actor_states: {},
|
|
620
656
|
prop_names: [],
|
|
@@ -1538,10 +1574,14 @@ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
|
|
|
1538
1574
|
if (key === "时空") {
|
|
1539
1575
|
const tsMatch = reMatch(_MD_TIME_SPACE_RE, value);
|
|
1540
1576
|
if (tsMatch) {
|
|
1577
|
+
const timeToken = tsMatch[2];
|
|
1541
1578
|
scene["environment"] = {
|
|
1542
1579
|
space: _MD_SPACE_NORM[tsMatch[1].toLowerCase()] ?? "interior",
|
|
1543
|
-
time:
|
|
1580
|
+
time: coarseSceneTime(timeToken),
|
|
1544
1581
|
};
|
|
1582
|
+
const state = locationTimeState(timeToken);
|
|
1583
|
+
if (state)
|
|
1584
|
+
scene["location_time_state"] = state;
|
|
1545
1585
|
}
|
|
1546
1586
|
else {
|
|
1547
1587
|
const env = scene["environment"];
|
|
@@ -1549,8 +1589,12 @@ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
|
|
|
1549
1589
|
const tl = tok.toLowerCase();
|
|
1550
1590
|
if (tl in _MD_SPACE_NORM)
|
|
1551
1591
|
env["space"] = _MD_SPACE_NORM[tl];
|
|
1552
|
-
|
|
1553
|
-
|
|
1592
|
+
const coarse = coarseSceneTime(tok);
|
|
1593
|
+
if (tl in _MD_TIME_NORM || tok in _TIME_MAP)
|
|
1594
|
+
env["time"] = coarse;
|
|
1595
|
+
const state = locationTimeState(tok);
|
|
1596
|
+
if (state)
|
|
1597
|
+
scene["location_time_state"] = state;
|
|
1554
1598
|
}
|
|
1555
1599
|
}
|
|
1556
1600
|
}
|
|
@@ -1665,40 +1709,6 @@ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
|
|
|
1665
1709
|
for (const scene of scenes) {
|
|
1666
1710
|
scene["actions"] = scene["actions"].filter((a) => strOf(a["content"]).trim().length > 0);
|
|
1667
1711
|
}
|
|
1668
|
-
if (!fragmentMode) {
|
|
1669
|
-
const stateLookup = new Map();
|
|
1670
|
-
for (const stateDef of stateDefs) {
|
|
1671
|
-
const key = `${strOf(stateDef["kind"])}::${strOf(stateDef["asset_name"])}`;
|
|
1672
|
-
if (!stateLookup.has(key))
|
|
1673
|
-
stateLookup.set(key, new Set());
|
|
1674
|
-
stateLookup.get(key).add(strOf(stateDef["state_name"]).trim());
|
|
1675
|
-
}
|
|
1676
|
-
for (const scene of scenes) {
|
|
1677
|
-
const actorStates = scene["actor_states"] || {};
|
|
1678
|
-
const filteredActorStates = {};
|
|
1679
|
-
for (const [actorName, stateName] of Object.entries(actorStates)) {
|
|
1680
|
-
const allowed = stateLookup.get(`actor::${actorName.trim()}`) ?? new Set();
|
|
1681
|
-
if (allowed.has(strOf(stateName).trim()))
|
|
1682
|
-
filteredActorStates[actorName] = stateName;
|
|
1683
|
-
}
|
|
1684
|
-
scene["actor_states"] = filteredActorStates;
|
|
1685
|
-
const propStates = scene["prop_states"] || {};
|
|
1686
|
-
const filteredPropStates = {};
|
|
1687
|
-
for (const [propName, stateName] of Object.entries(propStates)) {
|
|
1688
|
-
const allowed = stateLookup.get(`prop::${propName.trim()}`) ?? new Set();
|
|
1689
|
-
if (allowed.has(strOf(stateName).trim()))
|
|
1690
|
-
filteredPropStates[propName] = stateName;
|
|
1691
|
-
}
|
|
1692
|
-
scene["prop_states"] = filteredPropStates;
|
|
1693
|
-
const locName = strOf(scene["location_name"]).trim();
|
|
1694
|
-
const locState = strOf(scene["location_state"]).trim();
|
|
1695
|
-
if (locState) {
|
|
1696
|
-
const allowed = stateLookup.get(`location::${locName}`) ?? new Set();
|
|
1697
|
-
if (!allowed.has(locState))
|
|
1698
|
-
scene["location_state"] = null;
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
1712
|
if (scenes.length === 0) {
|
|
1703
1713
|
const hint = fragmentMode
|
|
1704
1714
|
? "no scenes parsed in fragment; expected at least one '## 场景 N' header under '# 剧本'"
|
|
@@ -2021,6 +2031,7 @@ export function expandCompactEpisodeResult(result, episodePlan) {
|
|
|
2021
2031
|
},
|
|
2022
2032
|
location_name: strOf(rawScene["loc"] || "未知场景").trim() || "未知场景",
|
|
2023
2033
|
location_state: strOf(rawScene["ls"]).trim() || null,
|
|
2034
|
+
location_time_state: strOf(rawScene["lts"]).trim() || null,
|
|
2024
2035
|
actor_names: actorNames,
|
|
2025
2036
|
actor_states: actorStates,
|
|
2026
2037
|
prop_names: propNames,
|
|
@@ -2067,6 +2078,9 @@ export function compactEpisodeResult(result) {
|
|
|
2067
2078
|
const locationState = strOf(scene["location_state"]).trim();
|
|
2068
2079
|
if (locationState)
|
|
2069
2080
|
compactScene["ls"] = locationState;
|
|
2081
|
+
const locationTimeState = strOf(scene["location_time_state"]).trim();
|
|
2082
|
+
if (locationTimeState)
|
|
2083
|
+
compactScene["lts"] = locationTimeState;
|
|
2070
2084
|
const actorStates = isDict(scene["actor_states"]) ? scene["actor_states"] : {};
|
|
2071
2085
|
const compactActors = [];
|
|
2072
2086
|
for (const actorName of asList(scene["actor_names"])) {
|
|
@@ -2412,29 +2426,8 @@ export function normalizeEpisodeResult(result, episodePlan) {
|
|
|
2412
2426
|
normalized["props"] = _normalizeAssetList(normalized["props"]);
|
|
2413
2427
|
normalized["speakers"] = _normalize_speaker_list(normalized["speakers"]);
|
|
2414
2428
|
normalized["state_definitions"] = _normalizeStateDefinitions(normalized["state_definitions"]);
|
|
2415
|
-
const stateLookup = new Map();
|
|
2416
|
-
for (const stateDef of asList(normalized["state_definitions"])) {
|
|
2417
|
-
const key = `${strOf(stateDef["kind"])}::${strOf(stateDef["asset_name"])}`;
|
|
2418
|
-
if (!stateLookup.has(key))
|
|
2419
|
-
stateLookup.set(key, new Set());
|
|
2420
|
-
stateLookup.get(key).add(strOf(stateDef["state_name"]).trim());
|
|
2421
|
-
}
|
|
2422
2429
|
for (const scene of scenes) {
|
|
2423
|
-
const allowedActorStates = {};
|
|
2424
|
-
const sceneActorStates = scene["actor_states"] || {};
|
|
2425
|
-
for (const [actorName, actorState] of Object.entries(sceneActorStates)) {
|
|
2426
|
-
const allowed = stateLookup.get(`actor::${strOf(actorName).trim()}`) ?? new Set();
|
|
2427
|
-
if (allowed.has(strOf(actorState).trim()))
|
|
2428
|
-
allowedActorStates[actorName] = actorState;
|
|
2429
|
-
}
|
|
2430
|
-
scene["actor_states"] = allowedActorStates;
|
|
2431
2430
|
const loc = strOf(scene["location_name"]).trim();
|
|
2432
|
-
const ls = strOf(scene["location_state"]).trim();
|
|
2433
|
-
if (ls) {
|
|
2434
|
-
const allowed = stateLookup.get(`location::${loc}`) ?? new Set();
|
|
2435
|
-
if (!allowed.has(ls))
|
|
2436
|
-
scene["location_state"] = null;
|
|
2437
|
-
}
|
|
2438
2431
|
for (const name of asList(scene["actor_names"])) {
|
|
2439
2432
|
_md_push_asset(normalized["actors"], strOf(name), null);
|
|
2440
2433
|
}
|
|
@@ -3161,16 +3154,372 @@ export function curateScriptAssets(script, rawCuration = null) {
|
|
|
3161
3154
|
locations: locationDecisions,
|
|
3162
3155
|
};
|
|
3163
3156
|
}
|
|
3164
|
-
|
|
3157
|
+
function emptyAssetNormalizationReport() {
|
|
3158
|
+
return {
|
|
3159
|
+
version: 1,
|
|
3160
|
+
mode: "conservative",
|
|
3161
|
+
summary: {
|
|
3162
|
+
asset_merges: 0,
|
|
3163
|
+
location_states_from_names: 0,
|
|
3164
|
+
location_states_from_time: 0,
|
|
3165
|
+
review_items: 0,
|
|
3166
|
+
},
|
|
3167
|
+
decisions: [],
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
function addNormalizationDecision(report, decision) {
|
|
3171
|
+
report.decisions.push(decision);
|
|
3172
|
+
if (decision.action === "canonical_merge")
|
|
3173
|
+
report.summary.asset_merges += 1;
|
|
3174
|
+
else if (decision.action === "location_state_from_name")
|
|
3175
|
+
report.summary.location_states_from_names += 1;
|
|
3176
|
+
else if (decision.action === "location_state_from_time")
|
|
3177
|
+
report.summary.location_states_from_time += 1;
|
|
3178
|
+
else
|
|
3179
|
+
report.summary.review_items += 1;
|
|
3180
|
+
}
|
|
3181
|
+
export function canonicalAssetNameKey(kind, rawName) {
|
|
3182
|
+
const cleaned = cleanName(rawName)
|
|
3183
|
+
.replace(/\s+/g, " ")
|
|
3184
|
+
.trim()
|
|
3185
|
+
.toLowerCase();
|
|
3186
|
+
const folded = cleaned.replace(/[\s_\-–—·.,,、::;;'"“”‘’()[\]{}()【】《》<>]+/g, "");
|
|
3187
|
+
return `${kind}::${folded}`;
|
|
3188
|
+
}
|
|
3189
|
+
function makeAssetNameRegistry(kind) {
|
|
3190
|
+
return { kind, displayByKey: new Map(), aliasesByDisplay: new Map() };
|
|
3191
|
+
}
|
|
3192
|
+
function canonicalAssetDisplay(registry, rawName, report) {
|
|
3193
|
+
const name = cleanName(rawName);
|
|
3194
|
+
if (!name)
|
|
3195
|
+
return "";
|
|
3196
|
+
const key = canonicalAssetNameKey(registry.kind, name);
|
|
3197
|
+
const existing = registry.displayByKey.get(key);
|
|
3198
|
+
if (!existing) {
|
|
3199
|
+
registry.displayByKey.set(key, name);
|
|
3200
|
+
return name;
|
|
3201
|
+
}
|
|
3202
|
+
if (existing !== name) {
|
|
3203
|
+
if (!registry.aliasesByDisplay.has(existing))
|
|
3204
|
+
registry.aliasesByDisplay.set(existing, new Set());
|
|
3205
|
+
const aliases = registry.aliasesByDisplay.get(existing);
|
|
3206
|
+
if (!aliases.has(name)) {
|
|
3207
|
+
aliases.add(name);
|
|
3208
|
+
addNormalizationDecision(report, {
|
|
3209
|
+
action: "canonical_merge",
|
|
3210
|
+
kind: registry.kind,
|
|
3211
|
+
from: name,
|
|
3212
|
+
into: existing,
|
|
3213
|
+
reason: "同类资产 canonical key 相同,保留首个显示名并把后续名字作为 alias。",
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
return existing;
|
|
3218
|
+
}
|
|
3219
|
+
const LOCATION_STATE_PREFIX_RE = /^(白天|日间|上午|下午|午后|中午|清晨|早晨|黎明|夜晚|夜间|夜里|深夜|凌晨|黄昏|傍晚)(?:的|\s+)?(.+)$/;
|
|
3220
|
+
function splitLocationStatePhrase(rawName) {
|
|
3221
|
+
const name = cleanName(rawName);
|
|
3222
|
+
const match = LOCATION_STATE_PREFIX_RE.exec(name);
|
|
3223
|
+
if (!match)
|
|
3224
|
+
return null;
|
|
3225
|
+
const state = _LOCATION_TIME_STATE_MAP[match[1]] ?? match[1];
|
|
3226
|
+
const locationName = cleanName(match[2]);
|
|
3227
|
+
if (!locationName)
|
|
3228
|
+
return null;
|
|
3229
|
+
return { name: locationName, state };
|
|
3230
|
+
}
|
|
3231
|
+
function addStateDefinition(stateDefinitions, seen, kind, assetName, stateName, description = null) {
|
|
3232
|
+
const state = strOf(stateName).trim();
|
|
3233
|
+
if (!assetName || !state || stateRejectionReason(kind, state))
|
|
3234
|
+
return;
|
|
3235
|
+
const key = `${kind}::${assetName}::${state}`;
|
|
3236
|
+
if (seen.has(key))
|
|
3237
|
+
return;
|
|
3238
|
+
seen.add(key);
|
|
3239
|
+
const item = { kind, asset_name: assetName, state_name: state };
|
|
3240
|
+
const desc = strOf(description).trim();
|
|
3241
|
+
if (desc)
|
|
3242
|
+
item["description"] = desc;
|
|
3243
|
+
stateDefinitions.push(item);
|
|
3244
|
+
}
|
|
3245
|
+
function rawStringMap(value) {
|
|
3246
|
+
const out = {};
|
|
3247
|
+
if (!isDict(value))
|
|
3248
|
+
return out;
|
|
3249
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
3250
|
+
const state = strOf(raw).trim();
|
|
3251
|
+
if (state)
|
|
3252
|
+
out[cleanName(key)] = state;
|
|
3253
|
+
}
|
|
3254
|
+
return out;
|
|
3255
|
+
}
|
|
3256
|
+
function normalizeSpeakerNameInAction(action, actorRegistry, locationRegistry, propRegistry, report) {
|
|
3257
|
+
const item = { ...action };
|
|
3258
|
+
const normalizeByKind = (speaker, speakerKind) => {
|
|
3259
|
+
const kind = strOf(speakerKind || "actor").trim() || "actor";
|
|
3260
|
+
if (kind === "actor")
|
|
3261
|
+
return canonicalAssetDisplay(actorRegistry, speaker, report);
|
|
3262
|
+
if (kind === "location")
|
|
3263
|
+
return canonicalAssetDisplay(locationRegistry, speaker, report);
|
|
3264
|
+
if (kind === "prop")
|
|
3265
|
+
return canonicalAssetDisplay(propRegistry, speaker, report);
|
|
3266
|
+
return cleanName(speaker);
|
|
3267
|
+
};
|
|
3268
|
+
if (item["speaker"])
|
|
3269
|
+
item["speaker"] = normalizeByKind(item["speaker"], item["speaker_kind"]);
|
|
3270
|
+
if (isList(item["speakers"])) {
|
|
3271
|
+
item["speakers"] = asList(item["speakers"]).map((speaker) => ({
|
|
3272
|
+
...speaker,
|
|
3273
|
+
speaker: normalizeByKind(speaker["speaker"], speaker["speaker_kind"]),
|
|
3274
|
+
}));
|
|
3275
|
+
}
|
|
3276
|
+
if (isList(item["lines"])) {
|
|
3277
|
+
item["lines"] = asList(item["lines"]).map((line) => ({
|
|
3278
|
+
...line,
|
|
3279
|
+
speaker: normalizeByKind(line["speaker"], line["speaker_kind"]),
|
|
3280
|
+
}));
|
|
3281
|
+
}
|
|
3282
|
+
return item;
|
|
3283
|
+
}
|
|
3284
|
+
function attachRegistryAliases(results, registries) {
|
|
3285
|
+
if (results.length === 0)
|
|
3286
|
+
return;
|
|
3287
|
+
const first = results[0];
|
|
3288
|
+
const pluralByKind = { actor: "actors", location: "locations", prop: "props" };
|
|
3289
|
+
for (const registry of registries) {
|
|
3290
|
+
const key = pluralByKind[registry.kind];
|
|
3291
|
+
if (!isList(first[key]))
|
|
3292
|
+
first[key] = [];
|
|
3293
|
+
for (const [display, aliases] of registry.aliasesByDisplay) {
|
|
3294
|
+
const aliasList = [...aliases].filter((alias) => alias && alias !== display);
|
|
3295
|
+
if (aliasList.length === 0)
|
|
3296
|
+
continue;
|
|
3297
|
+
first[key].push({ name: display, aliases: aliasList });
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
function normalizeEpisodeResultsForAssets(results, opts) {
|
|
3302
|
+
const report = emptyAssetNormalizationReport();
|
|
3303
|
+
if (opts.assetNormalization === "off")
|
|
3304
|
+
return { results, report };
|
|
3305
|
+
const actorRegistry = makeAssetNameRegistry("actor");
|
|
3306
|
+
const locationRegistry = makeAssetNameRegistry("location");
|
|
3307
|
+
const propRegistry = makeAssetNameRegistry("prop");
|
|
3308
|
+
const normalizedResults = [];
|
|
3309
|
+
const globalStateDefinitions = [];
|
|
3310
|
+
const seenStateDefinitions = new Set();
|
|
3311
|
+
for (const result of results) {
|
|
3312
|
+
const normalized = { ...result, actors: [], locations: [], props: [], speakers: [], state_definitions: [] };
|
|
3313
|
+
for (const asset of asList(result["actors"])) {
|
|
3314
|
+
if (!isDict(asset))
|
|
3315
|
+
continue;
|
|
3316
|
+
const rawName = cleanName(asset["name"]);
|
|
3317
|
+
const name = canonicalAssetDisplay(actorRegistry, rawName, report);
|
|
3318
|
+
if (name)
|
|
3319
|
+
normalized["actors"].push({ ...asset, name });
|
|
3320
|
+
}
|
|
3321
|
+
for (const asset of asList(result["locations"])) {
|
|
3322
|
+
if (!isDict(asset))
|
|
3323
|
+
continue;
|
|
3324
|
+
const split = splitLocationStatePhrase(asset["name"]);
|
|
3325
|
+
const name = canonicalAssetDisplay(locationRegistry, split ? split.name : asset["name"], report);
|
|
3326
|
+
if (name)
|
|
3327
|
+
normalized["locations"].push({ ...asset, name });
|
|
3328
|
+
if (split && name) {
|
|
3329
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "location", name, split.state, asset["description"]);
|
|
3330
|
+
addNormalizationDecision(report, {
|
|
3331
|
+
action: "location_state_from_name",
|
|
3332
|
+
kind: "location",
|
|
3333
|
+
from: cleanName(asset["name"]),
|
|
3334
|
+
into: name,
|
|
3335
|
+
state: split.state,
|
|
3336
|
+
reason: "地点名包含时间状态,拆成稳定地点 + 场景状态。",
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
for (const asset of asList(result["props"])) {
|
|
3341
|
+
if (!isDict(asset))
|
|
3342
|
+
continue;
|
|
3343
|
+
const name = canonicalAssetDisplay(propRegistry, asset["name"], report);
|
|
3344
|
+
if (name)
|
|
3345
|
+
normalized["props"].push({ ...asset, name });
|
|
3346
|
+
}
|
|
3347
|
+
for (const speaker of asList(result["speakers"])) {
|
|
3348
|
+
if (!isDict(speaker))
|
|
3349
|
+
continue;
|
|
3350
|
+
const sourceKind = strOf(speaker["source_kind"] || "other").trim();
|
|
3351
|
+
if (sourceKind === "prop") {
|
|
3352
|
+
normalized["speakers"].push({
|
|
3353
|
+
...speaker,
|
|
3354
|
+
name: canonicalAssetDisplay(propRegistry, speaker["name"], report),
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
else if (sourceKind === "location") {
|
|
3358
|
+
normalized["speakers"].push({
|
|
3359
|
+
...speaker,
|
|
3360
|
+
name: canonicalAssetDisplay(locationRegistry, speaker["name"], report),
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
else if (sourceKind === "actor") {
|
|
3364
|
+
normalized["speakers"].push({
|
|
3365
|
+
...speaker,
|
|
3366
|
+
name: canonicalAssetDisplay(actorRegistry, speaker["name"], report),
|
|
3367
|
+
});
|
|
3368
|
+
}
|
|
3369
|
+
else {
|
|
3370
|
+
normalized["speakers"].push(speaker);
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
for (const stateDef of asList(result["state_definitions"])) {
|
|
3374
|
+
if (!isDict(stateDef))
|
|
3375
|
+
continue;
|
|
3376
|
+
const kind = strOf(stateDef["kind"]);
|
|
3377
|
+
if (kind !== "actor" && kind !== "location" && kind !== "prop")
|
|
3378
|
+
continue;
|
|
3379
|
+
if (kind === "actor") {
|
|
3380
|
+
const assetName = canonicalAssetDisplay(actorRegistry, stateDef["asset_name"], report);
|
|
3381
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "actor", assetName, stateDef["state_name"], stateDef["description"]);
|
|
3382
|
+
}
|
|
3383
|
+
else if (kind === "location") {
|
|
3384
|
+
const split = splitLocationStatePhrase(stateDef["asset_name"]);
|
|
3385
|
+
const assetName = canonicalAssetDisplay(locationRegistry, split ? split.name : stateDef["asset_name"], report);
|
|
3386
|
+
if (split) {
|
|
3387
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "location", assetName, split.state, stateDef["description"]);
|
|
3388
|
+
addNormalizationDecision(report, {
|
|
3389
|
+
action: "location_state_from_name",
|
|
3390
|
+
kind: "location",
|
|
3391
|
+
from: cleanName(stateDef["asset_name"]),
|
|
3392
|
+
into: assetName,
|
|
3393
|
+
state: split.state,
|
|
3394
|
+
reason: "地点状态定义的资产名含时间状态,拆成稳定地点 + 状态。",
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "location", assetName, stateDef["state_name"], stateDef["description"]);
|
|
3398
|
+
}
|
|
3399
|
+
else {
|
|
3400
|
+
const assetName = canonicalAssetDisplay(propRegistry, stateDef["asset_name"], report);
|
|
3401
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "prop", assetName, stateDef["state_name"], stateDef["description"]);
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
const scenes = [];
|
|
3405
|
+
for (const scene of asList(result["scenes"])) {
|
|
3406
|
+
const sceneNum = normalizeInt(scene["scene_num"], scenes.length + 1);
|
|
3407
|
+
const splitLocation = splitLocationStatePhrase(scene["location_name"]);
|
|
3408
|
+
const locationName = canonicalAssetDisplay(locationRegistry, splitLocation ? splitLocation.name : scene["location_name"], report) || "未知场景";
|
|
3409
|
+
const explicitLocationState = strOf(scene["location_state"]).trim();
|
|
3410
|
+
let locationState = explicitLocationState;
|
|
3411
|
+
if (!locationState && splitLocation) {
|
|
3412
|
+
locationState = splitLocation.state;
|
|
3413
|
+
addNormalizationDecision(report, {
|
|
3414
|
+
action: "location_state_from_name",
|
|
3415
|
+
kind: "location",
|
|
3416
|
+
from: cleanName(scene["location_name"]),
|
|
3417
|
+
into: locationName,
|
|
3418
|
+
state: locationState,
|
|
3419
|
+
episode: Number(result["episode"] ?? 0),
|
|
3420
|
+
scene_num: sceneNum,
|
|
3421
|
+
reason: "场景地点写成时间 + 地点,拆成稳定地点 + 场景状态。",
|
|
3422
|
+
});
|
|
3423
|
+
}
|
|
3424
|
+
const timeState = strOf(scene["location_time_state"]).trim();
|
|
3425
|
+
if (!locationState && timeState) {
|
|
3426
|
+
locationState = timeState;
|
|
3427
|
+
addNormalizationDecision(report, {
|
|
3428
|
+
action: "location_state_from_time",
|
|
3429
|
+
kind: "location",
|
|
3430
|
+
into: locationName,
|
|
3431
|
+
state: locationState,
|
|
3432
|
+
episode: Number(result["episode"] ?? 0),
|
|
3433
|
+
scene_num: sceneNum,
|
|
3434
|
+
reason: "场景时空包含细粒度时间,作为地点状态绑定。",
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3437
|
+
if (locationState)
|
|
3438
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "location", locationName, locationState);
|
|
3439
|
+
const actorStates = rawStringMap(scene["actor_states"]);
|
|
3440
|
+
const actorNames = [];
|
|
3441
|
+
const normalizedActorStates = {};
|
|
3442
|
+
const propNames = [];
|
|
3443
|
+
const normalizedPropStates = {};
|
|
3444
|
+
for (const rawActor of asList(scene["actor_names"])) {
|
|
3445
|
+
const cleanActor = cleanName(rawActor);
|
|
3446
|
+
const actorName = canonicalAssetDisplay(actorRegistry, cleanActor, report);
|
|
3447
|
+
if (!actorName)
|
|
3448
|
+
continue;
|
|
3449
|
+
uniqueAdd(actorNames, actorName);
|
|
3450
|
+
const state = actorStates[cleanActor] || actorStates[actorName] || "";
|
|
3451
|
+
if (!state)
|
|
3452
|
+
continue;
|
|
3453
|
+
normalizedActorStates[actorName] = state;
|
|
3454
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "actor", actorName, state);
|
|
3455
|
+
}
|
|
3456
|
+
const propStates = rawStringMap(scene["prop_states"]);
|
|
3457
|
+
for (const rawProp of asList(scene["prop_names"])) {
|
|
3458
|
+
const propName = canonicalAssetDisplay(propRegistry, rawProp, report);
|
|
3459
|
+
if (!propName)
|
|
3460
|
+
continue;
|
|
3461
|
+
uniqueAdd(propNames, propName);
|
|
3462
|
+
const state = propStates[cleanName(rawProp)] || propStates[propName] || "";
|
|
3463
|
+
if (state) {
|
|
3464
|
+
normalizedPropStates[propName] = state;
|
|
3465
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "prop", propName, state);
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
const actions = asList(scene["actions"]).map((action) => normalizeSpeakerNameInAction(action, actorRegistry, locationRegistry, propRegistry, report));
|
|
3469
|
+
const normalizedScene = {
|
|
3470
|
+
...scene,
|
|
3471
|
+
scene_num: sceneNum,
|
|
3472
|
+
location_name: locationName,
|
|
3473
|
+
location_state: locationState || null,
|
|
3474
|
+
actor_names: actorNames,
|
|
3475
|
+
actor_states: normalizedActorStates,
|
|
3476
|
+
prop_names: propNames,
|
|
3477
|
+
prop_states: normalizedPropStates,
|
|
3478
|
+
actions,
|
|
3479
|
+
};
|
|
3480
|
+
delete normalizedScene["location_time_state"];
|
|
3481
|
+
scenes.push(normalizedScene);
|
|
3482
|
+
}
|
|
3483
|
+
normalized["scenes"] = scenes;
|
|
3484
|
+
normalizedResults.push(normalized);
|
|
3485
|
+
}
|
|
3486
|
+
if (normalizedResults.length > 0) {
|
|
3487
|
+
normalizedResults[0]["state_definitions"] = globalStateDefinitions;
|
|
3488
|
+
attachRegistryAliases(normalizedResults, [actorRegistry, locationRegistry, propRegistry]);
|
|
3489
|
+
}
|
|
3490
|
+
return { results: normalizedResults, report };
|
|
3491
|
+
}
|
|
3492
|
+
// ---------------------------------------------------------------------------
|
|
3493
|
+
// Merge episode results into a final script
|
|
3494
|
+
// ---------------------------------------------------------------------------
|
|
3495
|
+
function mergeEpisodeResultsInternal(results, title, opts = {}) {
|
|
3165
3496
|
const actorNames = [];
|
|
3166
3497
|
const locationNames = [];
|
|
3167
3498
|
const propNames = [];
|
|
3168
3499
|
const actorDescriptions = new Map();
|
|
3169
3500
|
const locationDescriptions = new Map();
|
|
3170
3501
|
const propDescriptions = new Map();
|
|
3502
|
+
const actorAliases = new Map();
|
|
3503
|
+
const locationAliases = new Map();
|
|
3504
|
+
const propAliases = new Map();
|
|
3171
3505
|
const stateDefinitions = [];
|
|
3172
3506
|
const speakerRecords = new Map();
|
|
3173
3507
|
const speakerOrder = [];
|
|
3508
|
+
const rememberAliases = (aliasesByName, name, aliasesRaw) => {
|
|
3509
|
+
if (!name)
|
|
3510
|
+
return;
|
|
3511
|
+
const existing = aliasesByName.get(name) ?? [];
|
|
3512
|
+
const seen = new Set(existing);
|
|
3513
|
+
for (const rawAlias of asList(aliasesRaw)) {
|
|
3514
|
+
const alias = cleanName(rawAlias);
|
|
3515
|
+
if (!alias || alias === name || seen.has(alias))
|
|
3516
|
+
continue;
|
|
3517
|
+
existing.push(alias);
|
|
3518
|
+
seen.add(alias);
|
|
3519
|
+
}
|
|
3520
|
+
if (existing.length > 0)
|
|
3521
|
+
aliasesByName.set(name, existing);
|
|
3522
|
+
};
|
|
3174
3523
|
const rememberSpeaker = (name, sourceKind = "other", description = "") => {
|
|
3175
3524
|
const speakerName = strOf(name).trim();
|
|
3176
3525
|
if (!speakerName)
|
|
@@ -3199,6 +3548,7 @@ export function mergeEpisodeResults(results, title, opts = {}) {
|
|
|
3199
3548
|
const desc = strOf(asset["description"]).trim();
|
|
3200
3549
|
if (name && desc && !actorDescriptions.has(name))
|
|
3201
3550
|
actorDescriptions.set(name, desc);
|
|
3551
|
+
rememberAliases(actorAliases, name, asset["aliases"]);
|
|
3202
3552
|
}
|
|
3203
3553
|
for (const asset of asList(result["locations"])) {
|
|
3204
3554
|
if (!isDict(asset))
|
|
@@ -3207,6 +3557,7 @@ export function mergeEpisodeResults(results, title, opts = {}) {
|
|
|
3207
3557
|
const desc = strOf(asset["description"]).trim();
|
|
3208
3558
|
if (name && desc && !locationDescriptions.has(name))
|
|
3209
3559
|
locationDescriptions.set(name, desc);
|
|
3560
|
+
rememberAliases(locationAliases, name, asset["aliases"]);
|
|
3210
3561
|
}
|
|
3211
3562
|
for (const asset of asList(result["props"])) {
|
|
3212
3563
|
if (!isDict(asset))
|
|
@@ -3215,6 +3566,7 @@ export function mergeEpisodeResults(results, title, opts = {}) {
|
|
|
3215
3566
|
const desc = strOf(asset["description"]).trim();
|
|
3216
3567
|
if (name && desc && !propDescriptions.has(name))
|
|
3217
3568
|
propDescriptions.set(name, desc);
|
|
3569
|
+
rememberAliases(propAliases, name, asset["aliases"]);
|
|
3218
3570
|
}
|
|
3219
3571
|
for (const speaker of _normalize_speaker_list(result["speakers"])) {
|
|
3220
3572
|
rememberSpeaker(speaker["name"], speaker["source_kind"], speaker["description"]);
|
|
@@ -3345,6 +3697,9 @@ export function mergeEpisodeResults(results, title, opts = {}) {
|
|
|
3345
3697
|
const entry = { actor_id: actorIds.get(name), actor_name: name };
|
|
3346
3698
|
if (actorDescriptions.has(name))
|
|
3347
3699
|
entry["description"] = actorDescriptions.get(name);
|
|
3700
|
+
const aliases = actorAliases.get(name) ?? [];
|
|
3701
|
+
if (aliases.length > 0)
|
|
3702
|
+
entry["aliases"] = aliases;
|
|
3348
3703
|
const states = [];
|
|
3349
3704
|
for (const stateDef of stateDefsByTarget.get(`actor::${name}`) ?? []) {
|
|
3350
3705
|
stateCounter += 1;
|
|
@@ -3367,6 +3722,9 @@ export function mergeEpisodeResults(results, title, opts = {}) {
|
|
|
3367
3722
|
const entry = { location_id: locationIds.get(name), location_name: name, group: name };
|
|
3368
3723
|
if (locationDescriptions.has(name))
|
|
3369
3724
|
entry["description"] = locationDescriptions.get(name);
|
|
3725
|
+
const aliases = locationAliases.get(name) ?? [];
|
|
3726
|
+
if (aliases.length > 0)
|
|
3727
|
+
entry["aliases"] = aliases;
|
|
3370
3728
|
const states = [];
|
|
3371
3729
|
for (const stateDef of stateDefsByTarget.get(`location::${name}`) ?? []) {
|
|
3372
3730
|
stateCounter += 1;
|
|
@@ -3389,6 +3747,9 @@ export function mergeEpisodeResults(results, title, opts = {}) {
|
|
|
3389
3747
|
const entry = { prop_id: propIds.get(name), prop_name: name };
|
|
3390
3748
|
if (propDescriptions.has(name))
|
|
3391
3749
|
entry["description"] = propDescriptions.get(name);
|
|
3750
|
+
const aliases = propAliases.get(name) ?? [];
|
|
3751
|
+
if (aliases.length > 0)
|
|
3752
|
+
entry["aliases"] = aliases;
|
|
3392
3753
|
const states = [];
|
|
3393
3754
|
for (const stateDef of stateDefsByTarget.get(`prop::${name}`) ?? []) {
|
|
3394
3755
|
stateCounter += 1;
|
|
@@ -3572,4 +3933,14 @@ export function mergeEpisodeResults(results, title, opts = {}) {
|
|
|
3572
3933
|
episodes,
|
|
3573
3934
|
};
|
|
3574
3935
|
}
|
|
3936
|
+
export function mergeEpisodeResultsWithReport(results, title, opts = {}) {
|
|
3937
|
+
const normalized = normalizeEpisodeResultsForAssets(results, opts);
|
|
3938
|
+
return {
|
|
3939
|
+
script: mergeEpisodeResultsInternal(normalized.results, title, opts),
|
|
3940
|
+
assetNormalization: normalized.report,
|
|
3941
|
+
};
|
|
3942
|
+
}
|
|
3943
|
+
export function mergeEpisodeResults(results, title, opts = {}) {
|
|
3944
|
+
return mergeEpisodeResultsWithReport(results, title, opts).script;
|
|
3945
|
+
}
|
|
3575
3946
|
//# sourceMappingURL=direct-core.js.map
|