@lingjingai/scriptctl 0.27.5 → 0.28.1

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.
@@ -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+(日|夜|晨|清晨|黄昏|午后|凌晨|夜晚|深夜|黎明|白天|傍晚|中午|上午|下午|早晨)\s+((?:内|外|内\/外|外\/内))\s+(.+?)\s*$/;
21
- const _SCENE_LOCATION_FIRST_RE = /^(?:场)?(\d+)-(\d+)\s+(.+?)\s+(日|夜|晨|清晨|黄昏|午后|凌晨|夜晚|深夜|黎明|白天|傍晚|中午|上午|下午|早晨)\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+(日|夜|day|night)$/i;
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[timeFirst[3]] ?? "day",
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[locFirst[4]] ?? "day",
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: _MD_TIME_NORM[tsMatch[2].toLowerCase()] ?? "day",
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
- if (tl in _MD_TIME_NORM)
1553
- env["time"] = _MD_TIME_NORM[tl];
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
- export function mergeEpisodeResults(results, title, opts = {}) {
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