@lingjingai/scriptctl 0.32.0 → 0.34.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/changes/0.33.0.md +17 -0
- package/changes/unreleased.md +5 -3
- package/dist/common.js +29 -6
- package/dist/common.js.map +1 -1
- package/dist/domain/direct/runner.js +1 -1
- package/dist/domain/direct/runner.js.map +1 -1
- package/dist/domain/direct/stage.d.ts +1 -1
- package/dist/domain/direct/stage.js +1 -0
- package/dist/domain/direct/stage.js.map +1 -1
- package/dist/domain/direct/stages/index.d.ts +2 -1
- package/dist/domain/direct/stages/index.js +3 -1
- package/dist/domain/direct/stages/index.js.map +1 -1
- package/dist/domain/direct/stages/state-curation.d.ts +2 -0
- package/dist/domain/direct/stages/state-curation.js +10 -0
- package/dist/domain/direct/stages/state-curation.js.map +1 -0
- package/dist/domain/direct-core.d.ts +25 -1
- package/dist/domain/direct-core.js +1310 -111
- package/dist/domain/direct-core.js.map +1 -1
- package/dist/domain/script/validate.js +11 -0
- package/dist/domain/script/validate.js.map +1 -1
- package/dist/help-text.js +6 -1
- package/dist/help-text.js.map +1 -1
- package/dist/infra/providers.d.ts +3 -0
- package/dist/infra/providers.js +212 -12
- package/dist/infra/providers.js.map +1 -1
- package/dist/usecases/direct.d.ts +1 -0
- package/dist/usecases/direct.js +277 -14
- package/dist/usecases/direct.js.map +1 -1
- package/package.json +1 -1
|
@@ -104,6 +104,9 @@ const _MD_ASSET_SUBSECTION_MAP = {
|
|
|
104
104
|
"场景状态": "location_states", "地点状态": "location_states",
|
|
105
105
|
"道具状态": "prop_states",
|
|
106
106
|
};
|
|
107
|
+
const _MD_STATE_REASON_RE = /^(?:提取缘由|抽取缘由|提取理由|状态缘由|状态理由)\s*[::]\s*(.+)$/;
|
|
108
|
+
const STATE_REASON_MAX_CHARS = 360;
|
|
109
|
+
const MERGED_FROM_MAX_ITEMS = 12;
|
|
107
110
|
// ---------------------------------------------------------------------------
|
|
108
111
|
// Generic helpers (Python builtin equivalents)
|
|
109
112
|
// ---------------------------------------------------------------------------
|
|
@@ -411,8 +414,19 @@ export function extractEpisodeTitleCandidate(header) {
|
|
|
411
414
|
// ---------------------------------------------------------------------------
|
|
412
415
|
// Episode plan
|
|
413
416
|
// ---------------------------------------------------------------------------
|
|
417
|
+
const _SCREENPLAY_BODY_MARKER_RE = /^[\t ]*(?:剧本正文|正文|短剧正文|正片剧本|剧本内容)[\t ]*[::]?[\t ]*$/gim;
|
|
418
|
+
function screenplayBodyStartOffset(sourceText) {
|
|
419
|
+
for (const match of reFindAllMatches(_SCREENPLAY_BODY_MARKER_RE, sourceText)) {
|
|
420
|
+
const start = match.index + match[0].length;
|
|
421
|
+
if (reFindAllMatches(_EP_HEADER_RE, sourceText.slice(start)).length > 0)
|
|
422
|
+
return start;
|
|
423
|
+
}
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
414
426
|
export function buildEpisodePlan(sourceText) {
|
|
415
|
-
const
|
|
427
|
+
const bodyStart = screenplayBodyStartOffset(sourceText);
|
|
428
|
+
const bodyText = bodyStart > 0 ? sourceText.slice(bodyStart) : sourceText;
|
|
429
|
+
const matches = reFindAllMatches(_EP_HEADER_RE, bodyText);
|
|
416
430
|
const episodes = [];
|
|
417
431
|
let confidence;
|
|
418
432
|
let strategy;
|
|
@@ -436,7 +450,8 @@ export function buildEpisodePlan(sourceText) {
|
|
|
436
450
|
epNum = next;
|
|
437
451
|
}
|
|
438
452
|
usedEpNums.add(epNum);
|
|
439
|
-
const
|
|
453
|
+
const start = bodyStart + match.index;
|
|
454
|
+
const end = idx + 1 < matches.length ? bodyStart + matches[idx + 1].index : sourceText.length;
|
|
440
455
|
const header = match[0].trim();
|
|
441
456
|
const rawTitle = extractEpisodeTitleCandidate(header);
|
|
442
457
|
const title = formatEpisodeTitle(epNum, rawTitle);
|
|
@@ -448,13 +463,13 @@ export function buildEpisodePlan(sourceText) {
|
|
|
448
463
|
raw_title: rawTitle,
|
|
449
464
|
title_status: titleStatus,
|
|
450
465
|
title_source: title ? "source" : titleStatus,
|
|
451
|
-
source_span: { start
|
|
452
|
-
header_span: { start
|
|
453
|
-
char_count: end -
|
|
466
|
+
source_span: { start, end },
|
|
467
|
+
header_span: { start, end: start + match[0].length },
|
|
468
|
+
char_count: end - start,
|
|
454
469
|
});
|
|
455
470
|
}
|
|
456
471
|
confidence = "high";
|
|
457
|
-
strategy = "explicit_episode_markers";
|
|
472
|
+
strategy = bodyStart > 0 ? "body_marker_episode_markers" : "explicit_episode_markers";
|
|
458
473
|
}
|
|
459
474
|
else {
|
|
460
475
|
episodes.push({
|
|
@@ -475,6 +490,7 @@ export function buildEpisodePlan(sourceText) {
|
|
|
475
490
|
version: 1,
|
|
476
491
|
strategy,
|
|
477
492
|
boundary_confidence: confidence,
|
|
493
|
+
body_start_offset: bodyStart,
|
|
478
494
|
total_episodes: episodes.length,
|
|
479
495
|
episodes,
|
|
480
496
|
};
|
|
@@ -492,6 +508,11 @@ export function splitEntityState(raw) {
|
|
|
492
508
|
if (!value)
|
|
493
509
|
return ["", null];
|
|
494
510
|
const states = [];
|
|
511
|
+
for (const item of reFindAll(_LOC_STATE_RE, value)) {
|
|
512
|
+
const v = item.trim();
|
|
513
|
+
if (v)
|
|
514
|
+
states.push(v);
|
|
515
|
+
}
|
|
495
516
|
for (const item of reFindAll(_STATE_RE, value)) {
|
|
496
517
|
const v = item.trim();
|
|
497
518
|
if (v)
|
|
@@ -502,7 +523,7 @@ export function splitEntityState(raw) {
|
|
|
502
523
|
if (v)
|
|
503
524
|
states.push(v);
|
|
504
525
|
}
|
|
505
|
-
return [cleanName(value), states.length > 0 ? states[states.length - 1] : null];
|
|
526
|
+
return [cleanName(reReplace(_LOC_STATE_RE, value, "")), states.length > 0 ? states[states.length - 1] : null];
|
|
506
527
|
}
|
|
507
528
|
export function splitLocationState(raw) {
|
|
508
529
|
const value = strOf(raw).trim();
|
|
@@ -1189,19 +1210,21 @@ export function isRecoverableBatchCliError(exc) {
|
|
|
1189
1210
|
export function isRetryableProviderError(exc) {
|
|
1190
1211
|
const anyExc = exc;
|
|
1191
1212
|
const statusCode = normalizeInt(anyExc?.status_code ?? anyExc?.statusCode ?? anyExc?.status, 0);
|
|
1192
|
-
if ([408, 409, 429, 500, 502, 503, 504, 529].includes(statusCode))
|
|
1193
|
-
return true;
|
|
1194
|
-
if ([400, 401, 403, 404].includes(statusCode))
|
|
1195
|
-
return false;
|
|
1196
1213
|
const name = (anyExc?.name ?? "").toLowerCase();
|
|
1197
1214
|
const message = (typeof exc === "object" && exc !== null && "message" in exc ? String(exc.message) : String(exc)).toLowerCase();
|
|
1198
1215
|
const retryMarkers = [
|
|
1199
1216
|
"apierror", "apiconnectionerror", "apitimeouterror", "ratelimiterror",
|
|
1200
1217
|
"remoteprotocolerror", "timeout", "overloaded", "rate_limit", "temporarily",
|
|
1201
1218
|
"service unavailable", "bad gateway", "gateway timeout", "connection reset",
|
|
1202
|
-
"peer closed", "incomplete chunked read",
|
|
1219
|
+
"peer closed", "incomplete chunked read", "all connection attempts failed",
|
|
1203
1220
|
];
|
|
1204
|
-
|
|
1221
|
+
if (retryMarkers.some((m) => name.includes(m) || message.includes(m)))
|
|
1222
|
+
return true;
|
|
1223
|
+
if ([408, 409, 429, 500, 502, 503, 504, 529].includes(statusCode))
|
|
1224
|
+
return true;
|
|
1225
|
+
if ([400, 401, 403, 404].includes(statusCode))
|
|
1226
|
+
return false;
|
|
1227
|
+
return false;
|
|
1205
1228
|
}
|
|
1206
1229
|
export function providerAttempts() {
|
|
1207
1230
|
const raw = (process.env.SCRIPTCTL_PROVIDER_ATTEMPTS ?? "").trim();
|
|
@@ -1276,20 +1299,35 @@ export function _md_push_asset(items, rawName, description) {
|
|
|
1276
1299
|
}
|
|
1277
1300
|
items.push({ name, description: description || null });
|
|
1278
1301
|
}
|
|
1279
|
-
function
|
|
1302
|
+
function cleanStateReason(raw) {
|
|
1303
|
+
const text = strOf(raw).replace(/\s+/g, " ").trim();
|
|
1304
|
+
if (!text)
|
|
1305
|
+
return "";
|
|
1306
|
+
return text.length > STATE_REASON_MAX_CHARS ? `${text.slice(0, STATE_REASON_MAX_CHARS - 1)}…` : text;
|
|
1307
|
+
}
|
|
1308
|
+
function setStateReason(item, reason) {
|
|
1309
|
+
const text = cleanStateReason(reason);
|
|
1310
|
+
if (text && !item["state_reason"])
|
|
1311
|
+
item["state_reason"] = text;
|
|
1312
|
+
}
|
|
1313
|
+
function _md_push_state_def(items, kind, rawName, description, stateReason = "") {
|
|
1280
1314
|
const [namePart, sep, statePart] = partition(strOf(rawName), "|");
|
|
1281
1315
|
const assetName = cleanName(namePart);
|
|
1282
1316
|
const stateName = statePart.trim();
|
|
1283
1317
|
if (!sep || !assetName || !stateName)
|
|
1284
|
-
return;
|
|
1318
|
+
return null;
|
|
1285
1319
|
for (const item of items) {
|
|
1286
1320
|
if (item["kind"] === kind && item["asset_name"] === assetName && item["state_name"] === stateName) {
|
|
1287
1321
|
if (description && !item["description"])
|
|
1288
1322
|
item["description"] = description;
|
|
1289
|
-
|
|
1323
|
+
setStateReason(item, stateReason);
|
|
1324
|
+
return item;
|
|
1290
1325
|
}
|
|
1291
1326
|
}
|
|
1292
|
-
|
|
1327
|
+
const item = { kind, asset_name: assetName, state_name: stateName, description: description || null };
|
|
1328
|
+
setStateReason(item, stateReason);
|
|
1329
|
+
items.push(item);
|
|
1330
|
+
return item;
|
|
1293
1331
|
}
|
|
1294
1332
|
export function parseSpeakerRef(raw) {
|
|
1295
1333
|
const value = strOf(raw).trim();
|
|
@@ -1330,7 +1368,7 @@ export function inferNonActorSpeakerKind(name, scene = null) {
|
|
|
1330
1368
|
return "system";
|
|
1331
1369
|
if (reSearch(/广播(?!员)|喇叭|电台|播报|通知|警报/, value))
|
|
1332
1370
|
return "broadcast";
|
|
1333
|
-
if (reSearch(
|
|
1371
|
+
if (reSearch(/众人|众|群体|全体|大家|人群|宾客|观众|群众|围观者/, value))
|
|
1334
1372
|
return "group";
|
|
1335
1373
|
if (scene) {
|
|
1336
1374
|
const propSet = new Set(asList(scene["prop_names"]).map((p) => strOf(p).trim()));
|
|
@@ -1500,6 +1538,7 @@ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
|
|
|
1500
1538
|
const speakers = [];
|
|
1501
1539
|
const stateDefs = [];
|
|
1502
1540
|
let lastAction = null;
|
|
1541
|
+
let lastStateDef = null;
|
|
1503
1542
|
// synopsis (## 梗概 sub-header → per-episode; # 梗概 top header → whole-script)
|
|
1504
1543
|
// and the episode title carried as the first non-section top header.
|
|
1505
1544
|
const synopsisLines = [];
|
|
@@ -1515,6 +1554,7 @@ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
|
|
|
1515
1554
|
if (subMatch && !sceneMatch && subMatch[1].trim() === "梗概") {
|
|
1516
1555
|
capturingSynopsis = true;
|
|
1517
1556
|
lastAction = null;
|
|
1557
|
+
lastStateDef = null;
|
|
1518
1558
|
continue;
|
|
1519
1559
|
}
|
|
1520
1560
|
if (sceneMatch && section === "script") {
|
|
@@ -1531,6 +1571,7 @@ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
|
|
|
1531
1571
|
actions: [],
|
|
1532
1572
|
});
|
|
1533
1573
|
lastAction = null;
|
|
1574
|
+
lastStateDef = null;
|
|
1534
1575
|
continue;
|
|
1535
1576
|
}
|
|
1536
1577
|
if (subMatch && section === "asset") {
|
|
@@ -1538,11 +1579,13 @@ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
|
|
|
1538
1579
|
const heading = subMatch[1].trim();
|
|
1539
1580
|
assetSubsection = _MD_ASSET_SUBSECTION_MAP[heading] ?? null;
|
|
1540
1581
|
lastAction = null;
|
|
1582
|
+
lastStateDef = null;
|
|
1541
1583
|
continue;
|
|
1542
1584
|
}
|
|
1543
1585
|
if (subMatch) {
|
|
1544
1586
|
capturingSynopsis = false;
|
|
1545
1587
|
lastAction = null;
|
|
1588
|
+
lastStateDef = null;
|
|
1546
1589
|
continue;
|
|
1547
1590
|
}
|
|
1548
1591
|
const topMatch = reMatch(_MD_TOP_HEADER_RE, stripped);
|
|
@@ -1571,6 +1614,7 @@ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
|
|
|
1571
1614
|
section = null;
|
|
1572
1615
|
}
|
|
1573
1616
|
lastAction = null;
|
|
1617
|
+
lastStateDef = null;
|
|
1574
1618
|
continue;
|
|
1575
1619
|
}
|
|
1576
1620
|
// Inside a synopsis block, capture prose lines until the next header.
|
|
@@ -1651,6 +1695,7 @@ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
|
|
|
1651
1695
|
}
|
|
1652
1696
|
}
|
|
1653
1697
|
lastAction = null;
|
|
1698
|
+
lastStateDef = null;
|
|
1654
1699
|
continue;
|
|
1655
1700
|
}
|
|
1656
1701
|
const actionMatch = reMatch(_MD_ACTION_ANCHOR_RE, stripped);
|
|
@@ -1674,10 +1719,12 @@ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
|
|
|
1674
1719
|
}
|
|
1675
1720
|
scenes[scenes.length - 1]["actions"].push(action);
|
|
1676
1721
|
lastAction = action;
|
|
1722
|
+
lastStateDef = null;
|
|
1677
1723
|
continue;
|
|
1678
1724
|
}
|
|
1679
1725
|
if (!stripped) {
|
|
1680
1726
|
lastAction = null;
|
|
1727
|
+
lastStateDef = null;
|
|
1681
1728
|
continue;
|
|
1682
1729
|
}
|
|
1683
1730
|
if (lastAction !== null && !stripped.startsWith("-")) {
|
|
@@ -1693,8 +1740,14 @@ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
|
|
|
1693
1740
|
else if (section === "asset") {
|
|
1694
1741
|
if (assetSubsection === null || !stripped)
|
|
1695
1742
|
continue;
|
|
1743
|
+
const reasonMatch = reMatch(_MD_STATE_REASON_RE, stripped);
|
|
1744
|
+
if (reasonMatch && lastStateDef !== null && /_states$/.test(assetSubsection)) {
|
|
1745
|
+
setStateReason(lastStateDef, reasonMatch[1]);
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1696
1748
|
const entryMatch = reMatch(_MD_ASSET_ENTRY_RE, stripped);
|
|
1697
1749
|
if (entryMatch) {
|
|
1750
|
+
lastStateDef = null;
|
|
1698
1751
|
const name = entryMatch[1].trim();
|
|
1699
1752
|
const description = (entryMatch[2] || "").trim() || null;
|
|
1700
1753
|
if (assetSubsection === "actors")
|
|
@@ -1709,15 +1762,18 @@ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
|
|
|
1709
1762
|
speakers.push({ name: displayName, source_kind: sourceKind, description });
|
|
1710
1763
|
}
|
|
1711
1764
|
else if (assetSubsection === "actor_states") {
|
|
1712
|
-
_md_push_state_def(stateDefs, "actor", name, description);
|
|
1765
|
+
lastStateDef = _md_push_state_def(stateDefs, "actor", name, description);
|
|
1713
1766
|
}
|
|
1714
1767
|
else if (assetSubsection === "location_states") {
|
|
1715
|
-
_md_push_state_def(stateDefs, "location", name, description);
|
|
1768
|
+
lastStateDef = _md_push_state_def(stateDefs, "location", name, description);
|
|
1716
1769
|
}
|
|
1717
1770
|
else if (assetSubsection === "prop_states") {
|
|
1718
|
-
_md_push_state_def(stateDefs, "prop", name, description);
|
|
1771
|
+
lastStateDef = _md_push_state_def(stateDefs, "prop", name, description);
|
|
1719
1772
|
}
|
|
1720
1773
|
}
|
|
1774
|
+
else {
|
|
1775
|
+
lastStateDef = null;
|
|
1776
|
+
}
|
|
1721
1777
|
}
|
|
1722
1778
|
}
|
|
1723
1779
|
for (const scene of scenes) {
|
|
@@ -1771,15 +1827,23 @@ export function parseAssetDoc(text, kind) {
|
|
|
1771
1827
|
const stateDefs = [];
|
|
1772
1828
|
const stateKind = kind === "actors" ? "actor" : kind === "locations" ? "location" : "prop";
|
|
1773
1829
|
let currentAsset = null;
|
|
1830
|
+
let currentStateDef = null;
|
|
1774
1831
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
1775
1832
|
const stripped = rawLine.trim();
|
|
1776
1833
|
if (!stripped)
|
|
1777
1834
|
continue;
|
|
1778
1835
|
if (reMatch(_MD_SUB_HEADER_RE, stripped) || reMatch(_MD_TOP_HEADER_RE, stripped))
|
|
1779
1836
|
continue; // headers ignored
|
|
1837
|
+
const reasonMatch = reMatch(_MD_STATE_REASON_RE, stripped);
|
|
1838
|
+
if (reasonMatch && currentStateDef !== null) {
|
|
1839
|
+
setStateReason(currentStateDef, reasonMatch[1]);
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1780
1842
|
const m = reMatch(_MD_ASSET_ENTRY_RE, stripped);
|
|
1781
|
-
if (!m)
|
|
1843
|
+
if (!m) {
|
|
1844
|
+
currentStateDef = null;
|
|
1782
1845
|
continue;
|
|
1846
|
+
}
|
|
1783
1847
|
const name = m[1].trim();
|
|
1784
1848
|
const description = (m[2] || "").trim() || null;
|
|
1785
1849
|
if (!name)
|
|
@@ -1788,15 +1852,14 @@ export function parseAssetDoc(text, kind) {
|
|
|
1788
1852
|
if (kind !== "speakers") {
|
|
1789
1853
|
// Flat self-contained state: `- name|state: desc`.
|
|
1790
1854
|
if (name.includes("|")) {
|
|
1791
|
-
_md_push_state_def(stateDefs, stateKind, name, description);
|
|
1855
|
+
currentStateDef = _md_push_state_def(stateDefs, stateKind, name, description);
|
|
1792
1856
|
continue;
|
|
1793
1857
|
}
|
|
1794
1858
|
// An indented bullet is always a state sub-bullet — never a top-level
|
|
1795
1859
|
// asset. Attach to the most recent asset; if there is none yet (malformed
|
|
1796
1860
|
// input), skip it rather than registering a spurious asset.
|
|
1797
1861
|
if (indented) {
|
|
1798
|
-
|
|
1799
|
-
_md_push_state_def(stateDefs, stateKind, `${currentAsset}|${name}`, description);
|
|
1862
|
+
currentStateDef = currentAsset ? _md_push_state_def(stateDefs, stateKind, `${currentAsset}|${name}`, description) : null;
|
|
1800
1863
|
continue;
|
|
1801
1864
|
}
|
|
1802
1865
|
}
|
|
@@ -1810,9 +1873,11 @@ export function parseAssetDoc(text, kind) {
|
|
|
1810
1873
|
const [sourceKind, displayName] = parseSpeakerAsset(name);
|
|
1811
1874
|
if (displayName)
|
|
1812
1875
|
speakers.push({ name: displayName, source_kind: sourceKind, description });
|
|
1876
|
+
currentStateDef = null;
|
|
1813
1877
|
continue;
|
|
1814
1878
|
}
|
|
1815
1879
|
currentAsset = name;
|
|
1880
|
+
currentStateDef = null;
|
|
1816
1881
|
}
|
|
1817
1882
|
return { actors, locations, props, speakers, state_definitions: stateDefs };
|
|
1818
1883
|
}
|
|
@@ -2074,6 +2139,24 @@ export function expandCompactEpisodeResult(result, episodePlan) {
|
|
|
2074
2139
|
expanded["synopsis"] = synopsis;
|
|
2075
2140
|
return expanded;
|
|
2076
2141
|
}
|
|
2142
|
+
const COMPACT_EPISODE_CARRIED_LIST_KEYS = ["actors", "locations", "props", "speakers", "state_definitions"];
|
|
2143
|
+
function carryCompactEpisodeLists(result, compact) {
|
|
2144
|
+
for (const key of COMPACT_EPISODE_CARRIED_LIST_KEYS) {
|
|
2145
|
+
const items = result[key];
|
|
2146
|
+
if (!isList(items) || items.length === 0)
|
|
2147
|
+
continue;
|
|
2148
|
+
compact[key] = items.map((item) => {
|
|
2149
|
+
if (!isDict(item))
|
|
2150
|
+
return item;
|
|
2151
|
+
const cleaned = {};
|
|
2152
|
+
for (const [k, v] of Object.entries(item)) {
|
|
2153
|
+
if (v !== null && v !== undefined)
|
|
2154
|
+
cleaned[k] = v;
|
|
2155
|
+
}
|
|
2156
|
+
return cleaned;
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2077
2160
|
export function compactEpisodeResult(result) {
|
|
2078
2161
|
const compactScenes = [];
|
|
2079
2162
|
for (const scene of asList(result["scenes"])) {
|
|
@@ -2174,6 +2257,7 @@ export function compactEpisodeResult(result) {
|
|
|
2174
2257
|
compactScenes.push(compactScene);
|
|
2175
2258
|
}
|
|
2176
2259
|
const compacted = { sc: compactScenes };
|
|
2260
|
+
carryCompactEpisodeLists(result, compacted);
|
|
2177
2261
|
// Carry the synopsis through the compact round-trip (short key `syn`). Without
|
|
2178
2262
|
// this, any synopsis written upstream (## 梗概 → batch result, or the episode
|
|
2179
2263
|
// reduce) would silently evaporate the moment the result is persisted.
|
|
@@ -2198,21 +2282,6 @@ export function compactBatchResult(result) {
|
|
|
2198
2282
|
compactActions[j]["r"] = refs.map((r) => strOf(r).trim()).filter((r) => r);
|
|
2199
2283
|
}
|
|
2200
2284
|
}
|
|
2201
|
-
for (const key of ["actors", "locations", "props", "speakers", "state_definitions"]) {
|
|
2202
|
-
const items = result[key];
|
|
2203
|
-
if (isList(items) && items.length > 0) {
|
|
2204
|
-
compact[key] = items.map((item) => {
|
|
2205
|
-
if (!isDict(item))
|
|
2206
|
-
return item;
|
|
2207
|
-
const cleaned = {};
|
|
2208
|
-
for (const [k, v] of Object.entries(item)) {
|
|
2209
|
-
if (v !== null && v !== undefined)
|
|
2210
|
-
cleaned[k] = v;
|
|
2211
|
-
}
|
|
2212
|
-
return cleaned;
|
|
2213
|
-
});
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
2285
|
return compact;
|
|
2217
2286
|
}
|
|
2218
2287
|
/**
|
|
@@ -2435,9 +2504,9 @@ export function normalizeEpisodeResult(result, episodePlan) {
|
|
|
2435
2504
|
}
|
|
2436
2505
|
scene["actions"] = actions;
|
|
2437
2506
|
}
|
|
2438
|
-
normalized["actors"] = _normalizeAssetList(normalized["actors"]);
|
|
2439
|
-
normalized["locations"] = _normalizeAssetList(normalized["locations"]);
|
|
2440
|
-
normalized["props"] = _normalizeAssetList(normalized["props"]);
|
|
2507
|
+
normalized["actors"] = _normalizeAssetList(normalized["actors"], "actor");
|
|
2508
|
+
normalized["locations"] = _normalizeAssetList(normalized["locations"], "location");
|
|
2509
|
+
normalized["props"] = _normalizeAssetList(normalized["props"], "prop");
|
|
2441
2510
|
normalized["speakers"] = _normalize_speaker_list(normalized["speakers"]);
|
|
2442
2511
|
normalized["state_definitions"] = _normalizeStateDefinitions(normalized["state_definitions"]);
|
|
2443
2512
|
for (const scene of scenes) {
|
|
@@ -2453,7 +2522,7 @@ export function normalizeEpisodeResult(result, episodePlan) {
|
|
|
2453
2522
|
}
|
|
2454
2523
|
return normalized;
|
|
2455
2524
|
}
|
|
2456
|
-
function _normalizeAssetList(items) {
|
|
2525
|
+
function _normalizeAssetList(items, kind) {
|
|
2457
2526
|
if (!isList(items))
|
|
2458
2527
|
return [];
|
|
2459
2528
|
const out = [];
|
|
@@ -2462,11 +2531,22 @@ function _normalizeAssetList(items) {
|
|
|
2462
2531
|
let name = "";
|
|
2463
2532
|
let description = null;
|
|
2464
2533
|
if (isDict(item)) {
|
|
2465
|
-
|
|
2534
|
+
const rawName = strOf(item["name"]);
|
|
2535
|
+
if (kind === "actor")
|
|
2536
|
+
[name] = splitEntityState(rawName);
|
|
2537
|
+
else if (kind === "location" || kind === "prop")
|
|
2538
|
+
[name] = splitLocationState(rawName);
|
|
2539
|
+
else
|
|
2540
|
+
name = cleanName(rawName);
|
|
2466
2541
|
description = strOf(item["description"]).trim() || null;
|
|
2467
2542
|
}
|
|
2468
2543
|
else if (typeof item === "string") {
|
|
2469
|
-
|
|
2544
|
+
if (kind === "actor")
|
|
2545
|
+
[name] = splitEntityState(item);
|
|
2546
|
+
else if (kind === "location" || kind === "prop")
|
|
2547
|
+
[name] = splitLocationState(item);
|
|
2548
|
+
else
|
|
2549
|
+
name = cleanName(item);
|
|
2470
2550
|
}
|
|
2471
2551
|
else
|
|
2472
2552
|
continue;
|
|
@@ -2506,7 +2586,13 @@ export function _normalize_speaker_list(items) {
|
|
|
2506
2586
|
const [parsedKind, parsedName] = parseSpeakerAsset(rawName);
|
|
2507
2587
|
let kind = sourceKind || parsedKind;
|
|
2508
2588
|
kind = _MD_SPEAKER_KIND_NORM[kind.toLowerCase()] ?? _MD_SPEAKER_KIND_NORM[kind] ?? "other";
|
|
2509
|
-
|
|
2589
|
+
let name = "";
|
|
2590
|
+
if (kind === "actor")
|
|
2591
|
+
[name] = splitEntityState(parsedName);
|
|
2592
|
+
else if (kind === "location" || kind === "prop")
|
|
2593
|
+
[name] = splitLocationState(parsedName);
|
|
2594
|
+
else
|
|
2595
|
+
name = cleanName(parsedName);
|
|
2510
2596
|
if (!name || seen.has(name))
|
|
2511
2597
|
continue;
|
|
2512
2598
|
seen.add(name);
|
|
@@ -2525,7 +2611,14 @@ function _normalizeStateDefinitions(items) {
|
|
|
2525
2611
|
const kind = strOf(item["kind"] || item["target_kind"]).trim();
|
|
2526
2612
|
if (!SCRIPT_TARGET_KINDS.has(kind))
|
|
2527
2613
|
continue;
|
|
2528
|
-
const
|
|
2614
|
+
const rawAssetName = strOf(item["asset_name"] || item["name"]);
|
|
2615
|
+
let assetName = "";
|
|
2616
|
+
if (kind === "actor")
|
|
2617
|
+
[assetName] = splitEntityState(rawAssetName);
|
|
2618
|
+
else if (kind === "location" || kind === "prop")
|
|
2619
|
+
[assetName] = splitLocationState(rawAssetName);
|
|
2620
|
+
else
|
|
2621
|
+
assetName = cleanName(rawAssetName);
|
|
2529
2622
|
const stateName = strOf(item["state_name"] || item["state"]).trim();
|
|
2530
2623
|
if (!assetName || !stateName || stateRejectionReason(kind, stateName))
|
|
2531
2624
|
continue;
|
|
@@ -2533,12 +2626,16 @@ function _normalizeStateDefinitions(items) {
|
|
|
2533
2626
|
if (seen.has(key))
|
|
2534
2627
|
continue;
|
|
2535
2628
|
seen.add(key);
|
|
2536
|
-
|
|
2629
|
+
const normalized = {
|
|
2537
2630
|
kind,
|
|
2538
2631
|
asset_name: assetName,
|
|
2539
2632
|
state_name: stateName,
|
|
2540
2633
|
description: strOf(item["description"]).trim() || null,
|
|
2541
|
-
}
|
|
2634
|
+
};
|
|
2635
|
+
const stateReason = cleanStateReason(item["state_reason"]);
|
|
2636
|
+
if (stateReason)
|
|
2637
|
+
normalized["state_reason"] = stateReason;
|
|
2638
|
+
out.push(normalized);
|
|
2542
2639
|
}
|
|
2543
2640
|
return out;
|
|
2544
2641
|
}
|
|
@@ -3819,17 +3916,31 @@ export function buildPrimaryAssetCurationChunks(script, kind, options = { maxIte
|
|
|
3819
3916
|
}
|
|
3820
3917
|
export function extractAssetGroupingExpressions(rawText) {
|
|
3821
3918
|
const groups = [];
|
|
3919
|
+
const blocks = [];
|
|
3920
|
+
let currentBlock = [];
|
|
3822
3921
|
const re = /<group>([\s\S]*?)<\/group>/g;
|
|
3823
3922
|
let outside = "";
|
|
3824
3923
|
let lastIndex = 0;
|
|
3825
3924
|
let match;
|
|
3826
3925
|
while ((match = re.exec(rawText)) !== null) {
|
|
3827
|
-
|
|
3828
|
-
|
|
3926
|
+
const gap = rawText.slice(lastIndex, match.index);
|
|
3927
|
+
outside += gap;
|
|
3928
|
+
if (gap.trim() && currentBlock.length > 0) {
|
|
3929
|
+
blocks.push(currentBlock);
|
|
3930
|
+
currentBlock = [];
|
|
3931
|
+
}
|
|
3932
|
+
const expression = strOf(match[1]).trim();
|
|
3933
|
+
groups.push(expression);
|
|
3934
|
+
currentBlock.push(expression);
|
|
3829
3935
|
lastIndex = match.index + match[0].length;
|
|
3830
3936
|
}
|
|
3831
3937
|
outside += rawText.slice(lastIndex);
|
|
3938
|
+
if (currentBlock.length > 0)
|
|
3939
|
+
blocks.push(currentBlock);
|
|
3832
3940
|
if (outside.trim()) {
|
|
3941
|
+
const lastBlock = blocks[blocks.length - 1] ?? [];
|
|
3942
|
+
if (blocks.length > 1 && lastBlock.length > 0)
|
|
3943
|
+
return lastBlock;
|
|
3833
3944
|
curationBlocked("Asset grouping response must contain only <group>...</group> lines.", [outside.trim().slice(0, 200)]);
|
|
3834
3945
|
}
|
|
3835
3946
|
if (groups.length === 0) {
|
|
@@ -3927,7 +4038,7 @@ export function validateAssetGroupingPlan(script, kind, rawPlan) {
|
|
|
3927
4038
|
retainedByName.set(key, list);
|
|
3928
4039
|
}
|
|
3929
4040
|
const duplicateMerges = [];
|
|
3930
|
-
const
|
|
4041
|
+
const omittedSingletons = [];
|
|
3931
4042
|
for (const id of inputIds) {
|
|
3932
4043
|
if (mentioned.has(id))
|
|
3933
4044
|
continue;
|
|
@@ -3937,7 +4048,13 @@ export function validateAssetGroupingPlan(script, kind, rawPlan) {
|
|
|
3937
4048
|
const key = assetGroupingNameKey(kind, asset[assetNameKey(kind)]);
|
|
3938
4049
|
const targets = key ? retainedByName.get(key) ?? [] : [];
|
|
3939
4050
|
if (targets.length === 0) {
|
|
3940
|
-
|
|
4051
|
+
groups.push({
|
|
4052
|
+
kind,
|
|
4053
|
+
ids: [id],
|
|
4054
|
+
reason: "omitted by grouping; review as standalone asset",
|
|
4055
|
+
});
|
|
4056
|
+
mentioned.add(id);
|
|
4057
|
+
omittedSingletons.push(id);
|
|
3941
4058
|
continue;
|
|
3942
4059
|
}
|
|
3943
4060
|
duplicateMerges.push({
|
|
@@ -3949,9 +4066,6 @@ export function validateAssetGroupingPlan(script, kind, rawPlan) {
|
|
|
3949
4066
|
reason: "name-level duplicate omitted by asset grouping",
|
|
3950
4067
|
});
|
|
3951
4068
|
}
|
|
3952
|
-
if (omittedInvalid.length > 0) {
|
|
3953
|
-
curationBlocked("Asset grouping omitted non-duplicate asset ids.", omittedInvalid.slice(0, 40));
|
|
3954
|
-
}
|
|
3955
4069
|
return {
|
|
3956
4070
|
version: 1,
|
|
3957
4071
|
format: "asset-grouping-v1",
|
|
@@ -3966,6 +4080,7 @@ export function validateAssetGroupingPlan(script, kind, rawPlan) {
|
|
|
3966
4080
|
group_count: groups.length,
|
|
3967
4081
|
mentioned_count: mentioned.size,
|
|
3968
4082
|
omitted_duplicate_count: duplicateMerges.length,
|
|
4083
|
+
omitted_singleton_count: omittedSingletons.length,
|
|
3969
4084
|
normalization_count: normalizations.length,
|
|
3970
4085
|
},
|
|
3971
4086
|
};
|
|
@@ -4295,8 +4410,9 @@ function auditCandidateReason(script, decision, evidence, relatedById) {
|
|
|
4295
4410
|
return "speaking_actor_removed_or_speaker";
|
|
4296
4411
|
}
|
|
4297
4412
|
if (action === "keep" && asset) {
|
|
4298
|
-
if (hasEnumeratedActorName(strOf(asset["actor_name"])))
|
|
4413
|
+
if (hasEnumeratedActorName(strOf(asset["actor_name"])) && item.dialogueCount === 0 && item.speakingScenes.size === 0) {
|
|
4299
4414
|
return "kept_enumerated_actor_review";
|
|
4415
|
+
}
|
|
4300
4416
|
const identity = actorIdentityCandidates(script, sourceId, strOf(asset["actor_name"]), item);
|
|
4301
4417
|
if (identity.length > 0)
|
|
4302
4418
|
return "kept_actor_identity_or_form_review";
|
|
@@ -4375,7 +4491,8 @@ export function buildAssetCurationAuditContextText(script, primaryPlan, selected
|
|
|
4375
4491
|
lines.push("- Output expressions only for listed candidates when the primary decision should be corrected or when the candidate is marked primary=MISSING.");
|
|
4376
4492
|
lines.push("- Otherwise output exactly <exp>NOOP # no asset curation changes</exp>.");
|
|
4377
4493
|
lines.push("- Audit expressions must reference source ids listed here. Existing primary sources are overridden; primary=MISSING and primary=IMPLICIT_KEEP sources are added.");
|
|
4378
|
-
lines.push("- For primary KEEP actor candidates, output SPEAKER/MERGE/RENAME only when the kept actor is
|
|
4494
|
+
lines.push("- For primary KEEP actor candidates, output SPEAKER/MERGE/RENAME only when the kept actor is an anonymous crowd, pure voice label, duplicate identity, or a generic numbered role without dialogue.");
|
|
4495
|
+
lines.push("- Do not downgrade a numbered/title-like actor to SPEAKER when it has dialogue or speaking-scene evidence; keep it as an actor unless it is truly a crowd label such as 众人/宾客们/围观者.");
|
|
4379
4496
|
lines.push("- For primary KEEP location candidates, output RENAME/MERGE only when canonical scope or duplicate/parent-child granularity is wrong; otherwise leave it unchanged.");
|
|
4380
4497
|
lines.push(`- For primary KEEP prop candidates, output DROP/MERGE/STATE/RENAME when the prop is too broad; output KEEP only to add/replace a valid category reason from ${propKeepCategoryList()}.`);
|
|
4381
4498
|
lines.push("- For primary DROP/MERGE/STATE prop candidates, output KEEP/RENAME/MERGE only when standalone hero-prop continuity is stronger than the primary decision.");
|
|
@@ -4563,7 +4680,7 @@ export function assertLocationCanonicalizationPlan(rawPlan) {
|
|
|
4563
4680
|
const invalid = [];
|
|
4564
4681
|
for (const decision of normalizedCurationDecisions(rawPlan)) {
|
|
4565
4682
|
const kind = strOf(decision["kind"]);
|
|
4566
|
-
|
|
4683
|
+
let action = strOf(decision["decision"]);
|
|
4567
4684
|
const sourceId = strOf(decision["source_id"]);
|
|
4568
4685
|
if (kind !== "location" || (action !== "rename" && action !== "merge")) {
|
|
4569
4686
|
invalid.push(`${kind || "<empty>"}:${sourceId || "<empty>"} ${action || "<empty>"}`);
|
|
@@ -5049,6 +5166,14 @@ function parseCurationExpression(expression) {
|
|
|
5049
5166
|
match = /^PROP\s+(actor|location|prop)\s+(\S+)\s+->\s+(\S+)\s+"((?:\\.|[^"\\])*)"$/i.exec(command);
|
|
5050
5167
|
if (match) {
|
|
5051
5168
|
const target = match[3];
|
|
5169
|
+
const normalizedTarget = target.toLowerCase();
|
|
5170
|
+
const targetPrefix = /^([A-Za-z]+):(.+)$/.exec(target);
|
|
5171
|
+
if ((targetPrefix && targetPrefix[1].toLowerCase() !== "prop") ||
|
|
5172
|
+
normalizedTarget === "actor" ||
|
|
5173
|
+
normalizedTarget === "location" ||
|
|
5174
|
+
normalizedTarget === "prop") {
|
|
5175
|
+
curationBlocked("PROP target must be NEW or an existing prop id.", [expression]);
|
|
5176
|
+
}
|
|
5052
5177
|
const decision = {
|
|
5053
5178
|
kind: match[1].toLowerCase(),
|
|
5054
5179
|
source_id: match[2],
|
|
@@ -5058,7 +5183,7 @@ function parseCurationExpression(expression) {
|
|
|
5058
5183
|
reason,
|
|
5059
5184
|
};
|
|
5060
5185
|
if (target.toUpperCase() !== "NEW")
|
|
5061
|
-
decision["target_id"] = target;
|
|
5186
|
+
decision["target_id"] = targetPrefix ? targetPrefix[2] : target;
|
|
5062
5187
|
return decision;
|
|
5063
5188
|
}
|
|
5064
5189
|
curationBlocked("Unknown or malformed curation expression.", [expression]);
|
|
@@ -5266,34 +5391,44 @@ export function combinePrimaryAssetCurationPlans(plans) {
|
|
|
5266
5391
|
primary_part_expression_text: expressionTextByPlan.join("\n"),
|
|
5267
5392
|
};
|
|
5268
5393
|
}
|
|
5269
|
-
export function combineAssetCurationPlans(primaryPlan, auditPlan, allowedNewAuditSources = []) {
|
|
5394
|
+
export function combineAssetCurationPlans(primaryPlan, auditPlan, allowedNewAuditSources = [], options = {}) {
|
|
5270
5395
|
const primaryDecisions = asList(primaryPlan["decisions"]).filter(isDict);
|
|
5271
|
-
const
|
|
5396
|
+
const rawAuditDecisions = asList(auditPlan["decisions"]).filter(isDict);
|
|
5272
5397
|
const primaryKeys = curationDecisionSourceSet(primaryDecisions);
|
|
5273
5398
|
const allowedNewKeys = new Set(allowedNewAuditSources);
|
|
5274
5399
|
const invalidAuditSources = [];
|
|
5275
5400
|
let addedAuditDecisions = 0;
|
|
5276
|
-
|
|
5401
|
+
const auditDecisions = [];
|
|
5402
|
+
for (const decision of rawAuditDecisions) {
|
|
5277
5403
|
const key = curationSourceKey(strOf(decision["kind"]), strOf(decision["source_id"]));
|
|
5278
|
-
if (primaryKeys.has(key))
|
|
5404
|
+
if (primaryKeys.has(key)) {
|
|
5405
|
+
auditDecisions.push(decision);
|
|
5279
5406
|
continue;
|
|
5407
|
+
}
|
|
5280
5408
|
if (allowedNewKeys.has(key)) {
|
|
5281
5409
|
addedAuditDecisions += 1;
|
|
5410
|
+
auditDecisions.push(decision);
|
|
5282
5411
|
continue;
|
|
5283
5412
|
}
|
|
5284
5413
|
invalidAuditSources.push(key);
|
|
5285
5414
|
}
|
|
5286
|
-
if (invalidAuditSources.length > 0) {
|
|
5415
|
+
if (invalidAuditSources.length > 0 && !options.salvageInvalidAuditSources) {
|
|
5287
5416
|
curationBlocked("Audit curation expressions may only override primary decision sources or listed missing required sources.", invalidAuditSources);
|
|
5288
5417
|
}
|
|
5289
5418
|
const primaryExpressions = asList(primaryPlan["expressions"]);
|
|
5290
|
-
const auditExpressions =
|
|
5419
|
+
const auditExpressions = invalidAuditSources.length > 0 && options.salvageInvalidAuditSources
|
|
5420
|
+
? auditDecisions.map(renderAssetCurationDecisionExpression)
|
|
5421
|
+
: asList(auditPlan["expressions"]);
|
|
5422
|
+
const auditExpressionText = invalidAuditSources.length > 0 && options.salvageInvalidAuditSources
|
|
5423
|
+
? formatAssetCurationDecisionExpressions({ decisions: auditDecisions })
|
|
5424
|
+
: strOf(auditPlan["expression_text"]);
|
|
5291
5425
|
return {
|
|
5292
5426
|
version: 3,
|
|
5293
5427
|
format: "asset-curation-exp-v1",
|
|
5294
5428
|
primary_expression_text: strOf(primaryPlan["expression_text"]),
|
|
5295
|
-
audit_expression_text:
|
|
5296
|
-
|
|
5429
|
+
audit_expression_text: auditExpressionText,
|
|
5430
|
+
audit_raw_expression_text: invalidAuditSources.length > 0 && options.salvageInvalidAuditSources ? strOf(auditPlan["expression_text"]) : undefined,
|
|
5431
|
+
expression_text: [strOf(primaryPlan["expression_text"]).trim(), auditExpressionText.trim()].filter((text) => text).join("\n"),
|
|
5297
5432
|
primary_expressions: primaryExpressions,
|
|
5298
5433
|
audit_expressions: auditExpressions,
|
|
5299
5434
|
expressions: [...primaryExpressions, ...auditExpressions],
|
|
@@ -5301,7 +5436,10 @@ export function combineAssetCurationPlans(primaryPlan, auditPlan, allowedNewAudi
|
|
|
5301
5436
|
audit_summary: {
|
|
5302
5437
|
primary_decisions: primaryDecisions.length,
|
|
5303
5438
|
audit_decisions: auditDecisions.length,
|
|
5439
|
+
audit_raw_decisions: rawAuditDecisions.length,
|
|
5304
5440
|
audit_added_decisions: addedAuditDecisions,
|
|
5441
|
+
audit_rejected_decisions: invalidAuditSources.length,
|
|
5442
|
+
audit_rejected_sources: invalidAuditSources,
|
|
5305
5443
|
audit_noop: auditDecisions.length === 0,
|
|
5306
5444
|
},
|
|
5307
5445
|
};
|
|
@@ -5667,7 +5805,7 @@ function normalizeCurationPlanTargets(script, decisions) {
|
|
|
5667
5805
|
normalizeMergeCycles(script, decisions);
|
|
5668
5806
|
const decisionsBySource = buildCurationDecisionIndex(decisions);
|
|
5669
5807
|
for (const decision of decisions) {
|
|
5670
|
-
|
|
5808
|
+
let action = strOf(decision["decision"]);
|
|
5671
5809
|
if (action !== "merge" && action !== "move_to_state" && action !== "move_to_prop")
|
|
5672
5810
|
continue;
|
|
5673
5811
|
const targetId = strOf(decision["target_id"]);
|
|
@@ -5817,6 +5955,71 @@ function rewriteCurationRefs(script, replacementsByKind, droppedByKind, stateRep
|
|
|
5817
5955
|
}
|
|
5818
5956
|
}
|
|
5819
5957
|
}
|
|
5958
|
+
function normalizeNonActorSpeakersToActorSpeakers(script) {
|
|
5959
|
+
const actorIdByName = new Map();
|
|
5960
|
+
for (const actor of asList(script["actors"])) {
|
|
5961
|
+
if (!isDict(actor))
|
|
5962
|
+
continue;
|
|
5963
|
+
const name = strOf(actor["actor_name"]).trim();
|
|
5964
|
+
const id = strOf(actor["actor_id"]).trim();
|
|
5965
|
+
if (name && id)
|
|
5966
|
+
actorIdByName.set(name, id);
|
|
5967
|
+
}
|
|
5968
|
+
const actorSpeakerByActorId = new Map();
|
|
5969
|
+
for (const speaker of asList(script["speakers"])) {
|
|
5970
|
+
if (!isDict(speaker))
|
|
5971
|
+
continue;
|
|
5972
|
+
if (strOf(speaker["source_kind"]) !== "actor")
|
|
5973
|
+
continue;
|
|
5974
|
+
const sourceId = strOf(speaker["source_id"]).trim();
|
|
5975
|
+
const speakerId = strOf(speaker["speaker_id"]).trim();
|
|
5976
|
+
if (sourceId && speakerId)
|
|
5977
|
+
actorSpeakerByActorId.set(sourceId, speakerId);
|
|
5978
|
+
}
|
|
5979
|
+
const speakerReplacements = new Map();
|
|
5980
|
+
for (const speaker of asList(script["speakers"])) {
|
|
5981
|
+
if (!isDict(speaker))
|
|
5982
|
+
continue;
|
|
5983
|
+
if (strOf(speaker["source_kind"]) === "actor")
|
|
5984
|
+
continue;
|
|
5985
|
+
const speakerId = strOf(speaker["speaker_id"]).trim();
|
|
5986
|
+
const displayName = strOf(speaker["display_name"]).trim();
|
|
5987
|
+
const actorId = actorIdByName.get(displayName);
|
|
5988
|
+
const actorSpeakerId = actorId ? actorSpeakerByActorId.get(actorId) : "";
|
|
5989
|
+
if (speakerId && actorSpeakerId && speakerId !== actorSpeakerId)
|
|
5990
|
+
speakerReplacements.set(speakerId, actorSpeakerId);
|
|
5991
|
+
}
|
|
5992
|
+
if (speakerReplacements.size === 0)
|
|
5993
|
+
return { speaker_refs_normalized: 0, speakers_removed: 0 };
|
|
5994
|
+
let speakerRefsNormalized = 0;
|
|
5995
|
+
const rewriteSpeakerId = (value) => {
|
|
5996
|
+
const speakerId = strOf(value).trim();
|
|
5997
|
+
const replacement = speakerReplacements.get(speakerId);
|
|
5998
|
+
if (!replacement)
|
|
5999
|
+
return speakerId;
|
|
6000
|
+
speakerRefsNormalized += 1;
|
|
6001
|
+
return replacement;
|
|
6002
|
+
};
|
|
6003
|
+
for (const episode of asList(script["episodes"])) {
|
|
6004
|
+
for (const scene of asList(episode["scenes"])) {
|
|
6005
|
+
for (const action of asList(scene["actions"])) {
|
|
6006
|
+
if (!isDict(action))
|
|
6007
|
+
continue;
|
|
6008
|
+
const rewrittenSpeakerId = rewriteSpeakerId(action["speaker_id"]);
|
|
6009
|
+
if (rewrittenSpeakerId) {
|
|
6010
|
+
action["speaker_id"] = rewrittenSpeakerId;
|
|
6011
|
+
const actorId = rewrittenSpeakerId.startsWith("spk_act_") ? rewrittenSpeakerId.slice(4) : "";
|
|
6012
|
+
if (actorId)
|
|
6013
|
+
action["actor_id"] = actorId;
|
|
6014
|
+
}
|
|
6015
|
+
action["speakers"] = rewriteSpeakerRefs(action["speakers"], speakerReplacements);
|
|
6016
|
+
action["lines"] = rewriteSpeakerRefs(action["lines"], speakerReplacements);
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
script["speakers"] = asList(script["speakers"]).filter((speaker) => isDict(speaker) && !speakerReplacements.has(strOf(speaker["speaker_id"])));
|
|
6021
|
+
return { speaker_refs_normalized: speakerRefsNormalized, speakers_removed: speakerReplacements.size };
|
|
6022
|
+
}
|
|
5820
6023
|
function copyStatesForMerge(script, kind, source, target, stateReplacements) {
|
|
5821
6024
|
for (const state of asList(source["states"])) {
|
|
5822
6025
|
const sourceStateId = strOf(state["state_id"]);
|
|
@@ -6007,6 +6210,7 @@ function applyAssetCurationDecisions(script, decisions, coverageSummary, default
|
|
|
6007
6210
|
}
|
|
6008
6211
|
}
|
|
6009
6212
|
}
|
|
6213
|
+
const speakerNormalizationSummary = normalizeNonActorSpeakersToActorSpeakers(script);
|
|
6010
6214
|
const actorsAfter = asList(script["actors"]).length;
|
|
6011
6215
|
const locationsAfter = asList(script["locations"]).length;
|
|
6012
6216
|
const propsAfter = asList(script["props"]).length;
|
|
@@ -6030,6 +6234,8 @@ function applyAssetCurationDecisions(script, decisions, coverageSummary, default
|
|
|
6030
6234
|
default_policy: defaultPolicy,
|
|
6031
6235
|
coverage: coverageSummary,
|
|
6032
6236
|
actions: summarizeActionableCuration(decisions),
|
|
6237
|
+
speaker_refs_normalized: speakerNormalizationSummary["speaker_refs_normalized"],
|
|
6238
|
+
speakers_normalized_removed: speakerNormalizationSummary["speakers_removed"],
|
|
6033
6239
|
},
|
|
6034
6240
|
decisions,
|
|
6035
6241
|
};
|
|
@@ -6171,6 +6377,907 @@ export function curateScriptAssets(script, rawCuration = null) {
|
|
|
6171
6377
|
};
|
|
6172
6378
|
}
|
|
6173
6379
|
// ---------------------------------------------------------------------------
|
|
6380
|
+
// State curation
|
|
6381
|
+
// ---------------------------------------------------------------------------
|
|
6382
|
+
export const STATE_CURATION_MAX_ASSETS = 4;
|
|
6383
|
+
export const STATE_CURATION_MAX_CONTEXT_CHARS = 28000;
|
|
6384
|
+
function stateCurationBlocked(message, received) {
|
|
6385
|
+
throw new CliError("DIRECT STATE CURATION BLOCKED: invalid provider plan", message, {
|
|
6386
|
+
exitCode: EXIT_NEEDS_AGENT,
|
|
6387
|
+
required: ["valid state curation expressions referencing existing asset/state ids"],
|
|
6388
|
+
received,
|
|
6389
|
+
nextSteps: ["Inspect state_curation.json, adjust the draft manually, or rerun direct init after provider plan fixes."],
|
|
6390
|
+
});
|
|
6391
|
+
}
|
|
6392
|
+
function stateCurationSourceKey(kind, assetId, stateId) {
|
|
6393
|
+
return `${kind}:${assetId}/${stateId}`;
|
|
6394
|
+
}
|
|
6395
|
+
function stateCurationDecisionSourceKey(decision) {
|
|
6396
|
+
return stateCurationSourceKey(strOf(decision["kind"]), strOf(decision["asset_id"]), strOf(decision["state_id"]));
|
|
6397
|
+
}
|
|
6398
|
+
function stateCurationAssetSourceKey(kind, assetId) {
|
|
6399
|
+
return `${kind}:${assetId}`;
|
|
6400
|
+
}
|
|
6401
|
+
function stateCurationDefaultSelectionSourceKey(selection) {
|
|
6402
|
+
return stateCurationAssetSourceKey(strOf(selection["kind"]), strOf(selection["asset_id"]));
|
|
6403
|
+
}
|
|
6404
|
+
function stateCurationUsageFor(script) {
|
|
6405
|
+
const usage = new Map();
|
|
6406
|
+
const ensure = (kind, assetId, stateId) => {
|
|
6407
|
+
const key = stateCurationSourceKey(kind, assetId, stateId);
|
|
6408
|
+
const current = usage.get(key);
|
|
6409
|
+
if (current)
|
|
6410
|
+
return current;
|
|
6411
|
+
const next = { sceneRefs: new Set(), stateChanges: new Set() };
|
|
6412
|
+
usage.set(key, next);
|
|
6413
|
+
return next;
|
|
6414
|
+
};
|
|
6415
|
+
for (const ep of asList(script["episodes"])) {
|
|
6416
|
+
const episodeId = strOf(ep["episode_id"]);
|
|
6417
|
+
for (const scene of asList(ep["scenes"])) {
|
|
6418
|
+
const sceneId = strOf(scene["scene_id"]);
|
|
6419
|
+
const sceneRef = [episodeId, sceneId].filter((item) => item).join("/");
|
|
6420
|
+
const ctx = sceneContext(scene);
|
|
6421
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
6422
|
+
const idKey = assetIdKey(kind);
|
|
6423
|
+
const refKey = assetListKey(kind);
|
|
6424
|
+
for (const ref of asList(ctx[refKey])) {
|
|
6425
|
+
const assetId = strOf(ref[idKey]);
|
|
6426
|
+
const stateId = strOf(ref["state_id"]);
|
|
6427
|
+
if (assetId && stateId && sceneRef)
|
|
6428
|
+
ensure(kind, assetId, stateId).sceneRefs.add(sceneRef);
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
for (const [actionIndex, action] of asList(scene["actions"]).entries()) {
|
|
6432
|
+
if (!isDict(action))
|
|
6433
|
+
continue;
|
|
6434
|
+
for (const change of asList(action["state_changes"])) {
|
|
6435
|
+
if (!isDict(change))
|
|
6436
|
+
continue;
|
|
6437
|
+
const kind = strOf(change["target_kind"]);
|
|
6438
|
+
if (!isAssetKindValue(kind))
|
|
6439
|
+
continue;
|
|
6440
|
+
const assetId = strOf(change["target_id"]);
|
|
6441
|
+
if (!assetId)
|
|
6442
|
+
continue;
|
|
6443
|
+
const actionRef = `${sceneRef}#${actionIndex}`;
|
|
6444
|
+
const fromStateId = strOf(change["from_state_id"]);
|
|
6445
|
+
const toStateId = strOf(change["to_state_id"]);
|
|
6446
|
+
if (fromStateId)
|
|
6447
|
+
ensure(kind, assetId, fromStateId).stateChanges.add(actionRef);
|
|
6448
|
+
if (toStateId)
|
|
6449
|
+
ensure(kind, assetId, toStateId).stateChanges.add(actionRef);
|
|
6450
|
+
}
|
|
6451
|
+
}
|
|
6452
|
+
}
|
|
6453
|
+
}
|
|
6454
|
+
return usage;
|
|
6455
|
+
}
|
|
6456
|
+
function statefulAssetsForCuration(script) {
|
|
6457
|
+
const refs = [];
|
|
6458
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
6459
|
+
for (const asset of asList(script[assetListKey(kind)])) {
|
|
6460
|
+
if (!isDict(asset))
|
|
6461
|
+
continue;
|
|
6462
|
+
const assetId = strOf(asset[assetIdKey(kind)]).trim();
|
|
6463
|
+
if (!assetId || asList(asset["states"]).filter(isDict).length === 0)
|
|
6464
|
+
continue;
|
|
6465
|
+
refs.push({ kind, asset_id: assetId });
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
return refs;
|
|
6469
|
+
}
|
|
6470
|
+
function stateCurationAssetByRef(script, ref) {
|
|
6471
|
+
return assetMapById(script, ref.kind).get(ref.asset_id) ?? null;
|
|
6472
|
+
}
|
|
6473
|
+
function compactStateCurationText(value, maxChars = 120) {
|
|
6474
|
+
const text = strOf(value).replace(/\s+/g, " ").replace(/\|/g, "/").trim();
|
|
6475
|
+
return text.length > maxChars ? `${text.slice(0, maxChars - 1)}…` : text;
|
|
6476
|
+
}
|
|
6477
|
+
function formatStateCurationUsage(usage) {
|
|
6478
|
+
if (!usage)
|
|
6479
|
+
return "refs=0 changes=0";
|
|
6480
|
+
return `refs=${usage.sceneRefs.size} changes=${usage.stateChanges.size}`;
|
|
6481
|
+
}
|
|
6482
|
+
function stateCurationClusterKey(stateName) {
|
|
6483
|
+
const name = strOf(stateName).replace(/[默认基础普通日常]/g, "").replace(/(?:状态|形态|造型|姿态|阶段|版本|版|态|装)$/g, "").trim();
|
|
6484
|
+
const cjk = /[\u4e00-\u9fff]{2,}/.exec(name);
|
|
6485
|
+
if (cjk)
|
|
6486
|
+
return cjk[0].slice(0, 2);
|
|
6487
|
+
const ascii = /[A-Za-z0-9]{3,}/.exec(name);
|
|
6488
|
+
return ascii ? ascii[0].slice(0, 6).toLowerCase() : "";
|
|
6489
|
+
}
|
|
6490
|
+
function formatStateCurationClusterHints(states) {
|
|
6491
|
+
const buckets = new Map();
|
|
6492
|
+
for (const state of states) {
|
|
6493
|
+
const stateId = compactStateCurationText(state["state_id"], 40);
|
|
6494
|
+
const stateName = compactStateCurationText(state["state_name"], 80);
|
|
6495
|
+
if (!stateId || !stateName || stateName === "默认")
|
|
6496
|
+
continue;
|
|
6497
|
+
const key = stateCurationClusterKey(stateName);
|
|
6498
|
+
if (!key)
|
|
6499
|
+
continue;
|
|
6500
|
+
if (!buckets.has(key))
|
|
6501
|
+
buckets.set(key, []);
|
|
6502
|
+
buckets.get(key).push(`${stateId}:${stateName}`);
|
|
6503
|
+
}
|
|
6504
|
+
const lines = [];
|
|
6505
|
+
for (const [key, members] of buckets) {
|
|
6506
|
+
if (members.length < 2)
|
|
6507
|
+
continue;
|
|
6508
|
+
lines.push(` C | similar_name_cluster=${key} | ${members.slice(0, 12).join(" ~ ")}`);
|
|
6509
|
+
}
|
|
6510
|
+
return lines;
|
|
6511
|
+
}
|
|
6512
|
+
function stateCurationPrefix(kind) {
|
|
6513
|
+
if (kind === "actor")
|
|
6514
|
+
return "A";
|
|
6515
|
+
if (kind === "location")
|
|
6516
|
+
return "L";
|
|
6517
|
+
return "P";
|
|
6518
|
+
}
|
|
6519
|
+
function formatStateCurationAssetBlock(script, ref, usageByKey) {
|
|
6520
|
+
const asset = stateCurationAssetByRef(script, ref);
|
|
6521
|
+
if (!asset)
|
|
6522
|
+
return [];
|
|
6523
|
+
const name = compactStateCurationText(asset[assetNameKey(ref.kind)], 80);
|
|
6524
|
+
const description = compactStateCurationText(asset["description"], 160);
|
|
6525
|
+
const states = asList(asset["states"]).filter(isDict);
|
|
6526
|
+
const lines = [
|
|
6527
|
+
`${stateCurationPrefix(ref.kind)} | ${ref.kind} | ${ref.asset_id} | ${name || "-"} | states=${states.length} | desc=${description || "-"}`,
|
|
6528
|
+
];
|
|
6529
|
+
for (const state of states) {
|
|
6530
|
+
const stateId = compactStateCurationText(state["state_id"], 40);
|
|
6531
|
+
const stateName = compactStateCurationText(state["state_name"], 80);
|
|
6532
|
+
if (!stateId || !stateName)
|
|
6533
|
+
continue;
|
|
6534
|
+
const usage = usageByKey.get(stateCurationSourceKey(ref.kind, ref.asset_id, stateId));
|
|
6535
|
+
const stateReason = compactStateCurationText(state["state_reason"], 220);
|
|
6536
|
+
lines.push(` S | ${stateId} | ${stateName} | ${formatStateCurationUsage(usage)} | desc=${compactStateCurationText(state["description"], 180) || "-"} | reason=${stateReason || "-"}`);
|
|
6537
|
+
}
|
|
6538
|
+
lines.push(...formatStateCurationClusterHints(states));
|
|
6539
|
+
return lines;
|
|
6540
|
+
}
|
|
6541
|
+
export function buildStateCurationContextText(script, refs) {
|
|
6542
|
+
const selectedRefs = refs && refs.length > 0 ? refs : statefulAssetsForCuration(script);
|
|
6543
|
+
const usage = stateCurationUsageFor(script);
|
|
6544
|
+
const lines = [];
|
|
6545
|
+
lines.push("# State Curation Context");
|
|
6546
|
+
lines.push(`title: ${compactStateCurationText(script["title"], 120) || "-"}`);
|
|
6547
|
+
lines.push("");
|
|
6548
|
+
lines.push("Goal: compress each asset's states to reusable production visual states.");
|
|
6549
|
+
lines.push("");
|
|
6550
|
+
lines.push("Policy:");
|
|
6551
|
+
lines.push("- A state is a large reusable visual asset form, not a plot/action/emotion/pose label.");
|
|
6552
|
+
lines.push("- KEEP only macro states that require a visibly different reusable asset: major form transformation, Q version, non-human/beast/god/giant form, durable outfit/appearance redesign, major damage/repair condition, a prop/location physical condition that must recur, or a location time/lighting state from a scene header such as 黄昏/夜晚/清晨.");
|
|
6553
|
+
lines.push("- Actor VFX/power intensity is not a separate state by itself: glowing, flashing, aura, light wall, brightness, totem spreading, transparent/solidifying/recovery frames, and one-action power bursts should MERGE into the nearest broad macro form or become ACTION_STATE.");
|
|
6554
|
+
lines.push("- Actor base human/humanoid shape is not a separate state when the default actor already covers it. Keep human-form states only when they carry a distinct age period, disguise, outfit, or major body redesign.");
|
|
6555
|
+
lines.push("- Prefer one broad macro state over many process frames. MERGE states that are only degree/progress variants of the same major appearance.");
|
|
6556
|
+
lines.push("- DROP or ACTION_STATE transient states: emotions, poses, unconscious/asleep/lying/sitting, holding/carrying an item, temporary glow/VFX intensity, fading/transparent/solidifying progress frames, minor mark brightness/thickness/spread, one-off ordinary clothing, camera effects, or scene-only actions.");
|
|
6557
|
+
lines.push("- Treat each `reason=` field as evidence text, not as a keep decision. Phrases like `needs a separate image`, `reusable`, or `stable` in the extraction reason are not enough to KEEP unless the name/description show a distinct macro form.");
|
|
6558
|
+
lines.push("- For one continuous transformation/recovery process, keep at most one broad representative state for the asset form. MERGE/DROP intermediate frames such as half-transparent body, lower body vanished, only head remains, partial solidifying, brightness increasing, or mark spreading.");
|
|
6559
|
+
lines.push("- For actor states, held objects are not actor states. If the state is just holding/using an item, use ACTION_STATE unless the item must remain a separate prop.");
|
|
6560
|
+
lines.push("- State descriptions must contain only durable visible appearance. Remove camera/staging/story wording such as back-to-camera, face hidden, close-up, initial pose, entering, taking hands, kissing, or standing in a scene.");
|
|
6561
|
+
lines.push("- When keeping a useful state whose description contains camera/staging/story wording, keep the state and rewrite the description with DESC.");
|
|
6562
|
+
lines.push("- Every S line below must receive exactly one expression.");
|
|
6563
|
+
lines.push("- Every asset block with at least one S line must also receive exactly one DEFAULT_STATE expression.");
|
|
6564
|
+
lines.push("- DEFAULT_STATE selects the most common normal appearance for that asset. Keep the selected state's readable state_name; only its state_id will become `default` during deterministic apply.");
|
|
6565
|
+
lines.push("- Use only state_id values listed under the same asset block. Do not invent ids or cross-asset merge targets.");
|
|
6566
|
+
lines.push("- If a broad canonical state name is needed, RENAME_STATE one existing representative state and MERGE_STATE the variants into it.");
|
|
6567
|
+
lines.push("");
|
|
6568
|
+
lines.push("Expression grammar:");
|
|
6569
|
+
lines.push("- <exp>KEEP_STATE <kind> <asset_id> <state_id> [DESC=\"<clean visual description>\"] # reason</exp>");
|
|
6570
|
+
lines.push("- <exp>MERGE_STATE <kind> <asset_id> <state_id> -> <target_state_id> # reason</exp>");
|
|
6571
|
+
lines.push("- <exp>DROP_STATE <kind> <asset_id> <state_id> # reason</exp>");
|
|
6572
|
+
lines.push("- <exp>ACTION_STATE <kind> <asset_id> <state_id> # reason</exp>");
|
|
6573
|
+
lines.push("- <exp>RENAME_STATE <kind> <asset_id> <state_id> \"<new_name>\" [DESC=\"<clean visual description>\"] # reason</exp>");
|
|
6574
|
+
lines.push("- <exp>DEFAULT_STATE <kind> <asset_id> <state_id> # reason</exp>");
|
|
6575
|
+
lines.push("- <kind> is one of actor, location, prop. Use double quotes for names; escape only \\\" and \\\\.");
|
|
6576
|
+
lines.push("");
|
|
6577
|
+
lines.push("Assets:");
|
|
6578
|
+
for (const ref of selectedRefs) {
|
|
6579
|
+
const block = formatStateCurationAssetBlock(script, ref, usage);
|
|
6580
|
+
if (block.length > 0)
|
|
6581
|
+
lines.push(...block);
|
|
6582
|
+
}
|
|
6583
|
+
lines.push("");
|
|
6584
|
+
lines.push("required_state_ids:");
|
|
6585
|
+
for (const ref of selectedRefs) {
|
|
6586
|
+
const asset = stateCurationAssetByRef(script, ref);
|
|
6587
|
+
if (!asset)
|
|
6588
|
+
continue;
|
|
6589
|
+
for (const state of asList(asset["states"])) {
|
|
6590
|
+
const stateId = strOf(state["state_id"]);
|
|
6591
|
+
if (stateId)
|
|
6592
|
+
lines.push(`- ${stateCurationSourceKey(ref.kind, ref.asset_id, stateId)}`);
|
|
6593
|
+
}
|
|
6594
|
+
}
|
|
6595
|
+
lines.push("");
|
|
6596
|
+
lines.push("required_default_assets:");
|
|
6597
|
+
for (const ref of selectedRefs) {
|
|
6598
|
+
const asset = stateCurationAssetByRef(script, ref);
|
|
6599
|
+
if (!asset || asList(asset["states"]).filter(isDict).length === 0)
|
|
6600
|
+
continue;
|
|
6601
|
+
lines.push(`- ${stateCurationAssetSourceKey(ref.kind, ref.asset_id)}`);
|
|
6602
|
+
}
|
|
6603
|
+
return `${lines.join("\n")}\n`;
|
|
6604
|
+
}
|
|
6605
|
+
export function buildStateCurationChunks(script, options = {}) {
|
|
6606
|
+
const maxAssets = Math.max(1, Math.floor(options.maxAssets ?? STATE_CURATION_MAX_ASSETS));
|
|
6607
|
+
const maxContextChars = Math.max(2000, Math.floor(options.maxContextChars ?? STATE_CURATION_MAX_CONTEXT_CHARS));
|
|
6608
|
+
const refs = statefulAssetsForCuration(script);
|
|
6609
|
+
const chunks = [];
|
|
6610
|
+
let current = [];
|
|
6611
|
+
const flush = () => {
|
|
6612
|
+
if (current.length === 0)
|
|
6613
|
+
return;
|
|
6614
|
+
chunks.push({ part_id: `part.${String(chunks.length + 1).padStart(3, "0")}`, refs: current });
|
|
6615
|
+
current = [];
|
|
6616
|
+
};
|
|
6617
|
+
for (const ref of refs) {
|
|
6618
|
+
const singletonLength = buildStateCurationContextText(script, [ref]).length;
|
|
6619
|
+
if (current.length === 0) {
|
|
6620
|
+
current.push(ref);
|
|
6621
|
+
if (singletonLength > maxContextChars)
|
|
6622
|
+
flush();
|
|
6623
|
+
continue;
|
|
6624
|
+
}
|
|
6625
|
+
const candidate = [...current, ref];
|
|
6626
|
+
if (candidate.length > maxAssets || buildStateCurationContextText(script, candidate).length > maxContextChars) {
|
|
6627
|
+
flush();
|
|
6628
|
+
current.push(ref);
|
|
6629
|
+
if (singletonLength > maxContextChars)
|
|
6630
|
+
flush();
|
|
6631
|
+
}
|
|
6632
|
+
else {
|
|
6633
|
+
current = candidate;
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
flush();
|
|
6637
|
+
return chunks;
|
|
6638
|
+
}
|
|
6639
|
+
function splitStateCurationExpressionReason(expression) {
|
|
6640
|
+
let inQuote = false;
|
|
6641
|
+
let escaped = false;
|
|
6642
|
+
for (let index = 0; index < expression.length; index += 1) {
|
|
6643
|
+
const ch = expression[index];
|
|
6644
|
+
if (escaped) {
|
|
6645
|
+
escaped = false;
|
|
6646
|
+
continue;
|
|
6647
|
+
}
|
|
6648
|
+
if (ch === "\\") {
|
|
6649
|
+
escaped = true;
|
|
6650
|
+
continue;
|
|
6651
|
+
}
|
|
6652
|
+
if (ch === "\"") {
|
|
6653
|
+
inQuote = !inQuote;
|
|
6654
|
+
continue;
|
|
6655
|
+
}
|
|
6656
|
+
if (ch === "#" && !inQuote) {
|
|
6657
|
+
const command = expression.slice(0, index).trim();
|
|
6658
|
+
const reason = expression.slice(index + 1).trim();
|
|
6659
|
+
if (!command)
|
|
6660
|
+
stateCurationBlocked("State curation expression is missing an operation before # reason.", [expression]);
|
|
6661
|
+
if (!reason)
|
|
6662
|
+
stateCurationBlocked("State curation expression is missing reason text after #.", [expression]);
|
|
6663
|
+
return { command, reason };
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6666
|
+
stateCurationBlocked("State curation expression must include `# reason`.", [expression]);
|
|
6667
|
+
}
|
|
6668
|
+
function unquoteStateCurationExpressionString(value) {
|
|
6669
|
+
let out = "";
|
|
6670
|
+
let escaped = false;
|
|
6671
|
+
for (const ch of value) {
|
|
6672
|
+
if (escaped) {
|
|
6673
|
+
if (ch !== "\\" && ch !== "\"")
|
|
6674
|
+
stateCurationBlocked("Unsupported escape sequence in state curation expression string.", [`\\${ch}`]);
|
|
6675
|
+
out += ch;
|
|
6676
|
+
escaped = false;
|
|
6677
|
+
}
|
|
6678
|
+
else if (ch === "\\") {
|
|
6679
|
+
escaped = true;
|
|
6680
|
+
}
|
|
6681
|
+
else {
|
|
6682
|
+
out += ch;
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
if (escaped)
|
|
6686
|
+
stateCurationBlocked("Unterminated escape sequence in state curation expression string.", [value]);
|
|
6687
|
+
return out;
|
|
6688
|
+
}
|
|
6689
|
+
function parseStateCurationExpression(expression) {
|
|
6690
|
+
if (expression.includes("\n") || expression.includes("\r")) {
|
|
6691
|
+
stateCurationBlocked("Each state curation expression must be a single line.", [expression]);
|
|
6692
|
+
}
|
|
6693
|
+
const { command, reason } = splitStateCurationExpressionReason(expression.trim());
|
|
6694
|
+
if (/^NOOP$/i.test(command))
|
|
6695
|
+
return null;
|
|
6696
|
+
let match = /^DEFAULT_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)$/i.exec(command);
|
|
6697
|
+
if (match) {
|
|
6698
|
+
return {
|
|
6699
|
+
kind: match[1].toLowerCase(),
|
|
6700
|
+
asset_id: match[2],
|
|
6701
|
+
state_id: match[3],
|
|
6702
|
+
decision: "default",
|
|
6703
|
+
reason,
|
|
6704
|
+
};
|
|
6705
|
+
}
|
|
6706
|
+
match = /^KEEP_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)(?:\s+DESC="(.*)")?$/i.exec(command);
|
|
6707
|
+
if (match) {
|
|
6708
|
+
const decision = { kind: match[1].toLowerCase(), asset_id: match[2], state_id: match[3], decision: "keep", reason };
|
|
6709
|
+
if (match[4] !== undefined)
|
|
6710
|
+
decision["new_description"] = unquoteStateCurationExpressionString(match[4]);
|
|
6711
|
+
return decision;
|
|
6712
|
+
}
|
|
6713
|
+
match = /^MERGE_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)\s+->\s+(\S+)$/i.exec(command);
|
|
6714
|
+
if (match) {
|
|
6715
|
+
return {
|
|
6716
|
+
kind: match[1].toLowerCase(),
|
|
6717
|
+
asset_id: match[2],
|
|
6718
|
+
state_id: match[3],
|
|
6719
|
+
decision: "merge",
|
|
6720
|
+
target_state_id: match[4],
|
|
6721
|
+
reason,
|
|
6722
|
+
};
|
|
6723
|
+
}
|
|
6724
|
+
match = /^DROP_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)(?:\s+DESC=".*")?$/i.exec(command);
|
|
6725
|
+
if (match) {
|
|
6726
|
+
return { kind: match[1].toLowerCase(), asset_id: match[2], state_id: match[3], decision: "drop", reason };
|
|
6727
|
+
}
|
|
6728
|
+
match = /^ACTION_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)(?:\s+DESC=".*")?$/i.exec(command);
|
|
6729
|
+
if (match) {
|
|
6730
|
+
return { kind: match[1].toLowerCase(), asset_id: match[2], state_id: match[3], decision: "action_desc", reason };
|
|
6731
|
+
}
|
|
6732
|
+
match = /^RENAME_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)\s+"((?:\\.|[^"\\])*)"(?:\s+DESC="(.*)")?$/i.exec(command);
|
|
6733
|
+
if (match) {
|
|
6734
|
+
const decision = {
|
|
6735
|
+
kind: match[1].toLowerCase(),
|
|
6736
|
+
asset_id: match[2],
|
|
6737
|
+
state_id: match[3],
|
|
6738
|
+
decision: "rename",
|
|
6739
|
+
new_name: unquoteStateCurationExpressionString(match[4]),
|
|
6740
|
+
reason,
|
|
6741
|
+
};
|
|
6742
|
+
if (match[5] !== undefined)
|
|
6743
|
+
decision["new_description"] = unquoteStateCurationExpressionString(match[5]);
|
|
6744
|
+
return decision;
|
|
6745
|
+
}
|
|
6746
|
+
stateCurationBlocked("Unknown or malformed state curation expression.", [expression]);
|
|
6747
|
+
}
|
|
6748
|
+
function extractStateCurationExpressions(rawText) {
|
|
6749
|
+
const expressions = [];
|
|
6750
|
+
const re = /<exp>([\s\S]*?)<\/exp>/g;
|
|
6751
|
+
let match;
|
|
6752
|
+
while ((match = re.exec(rawText)) !== null) {
|
|
6753
|
+
expressions.push(strOf(match[1]).trim());
|
|
6754
|
+
}
|
|
6755
|
+
if (expressions.length === 0) {
|
|
6756
|
+
stateCurationBlocked("State curation response did not contain any <exp> expressions.", [rawText.slice(0, 200)]);
|
|
6757
|
+
}
|
|
6758
|
+
return expressions;
|
|
6759
|
+
}
|
|
6760
|
+
export function parseStateCurationExpressions(rawText) {
|
|
6761
|
+
const decisions = [];
|
|
6762
|
+
const defaultSelections = [];
|
|
6763
|
+
const expressions = extractStateCurationExpressions(rawText);
|
|
6764
|
+
let noop = false;
|
|
6765
|
+
for (const expression of expressions) {
|
|
6766
|
+
const decision = parseStateCurationExpression(expression);
|
|
6767
|
+
if (decision === null)
|
|
6768
|
+
noop = true;
|
|
6769
|
+
else if (strOf(decision["decision"]) === "default")
|
|
6770
|
+
defaultSelections.push(decision);
|
|
6771
|
+
else
|
|
6772
|
+
decisions.push(decision);
|
|
6773
|
+
}
|
|
6774
|
+
if (noop && (decisions.length > 0 || defaultSelections.length > 0)) {
|
|
6775
|
+
stateCurationBlocked("NOOP cannot be combined with actionable state curation expressions.", [rawText.slice(0, 200)]);
|
|
6776
|
+
}
|
|
6777
|
+
return {
|
|
6778
|
+
version: 1,
|
|
6779
|
+
format: "state-curation-exp-v1",
|
|
6780
|
+
expression_text: rawText,
|
|
6781
|
+
expressions,
|
|
6782
|
+
decisions,
|
|
6783
|
+
default_selections: defaultSelections,
|
|
6784
|
+
};
|
|
6785
|
+
}
|
|
6786
|
+
function renderStateCurationDecisionExpression(decision) {
|
|
6787
|
+
const kind = strOf(decision["kind"]);
|
|
6788
|
+
const assetId = strOf(decision["asset_id"]);
|
|
6789
|
+
const stateId = strOf(decision["state_id"]);
|
|
6790
|
+
const reason = strOf(decision["reason"]).trim() || "state curation decision";
|
|
6791
|
+
const action = strOf(decision["decision"]);
|
|
6792
|
+
if (action === "default")
|
|
6793
|
+
return `DEFAULT_STATE ${kind} ${assetId} ${stateId} # ${reason}`;
|
|
6794
|
+
if (action === "merge")
|
|
6795
|
+
return `MERGE_STATE ${kind} ${assetId} ${stateId} -> ${strOf(decision["target_state_id"])} # ${reason}`;
|
|
6796
|
+
if (action === "drop")
|
|
6797
|
+
return `DROP_STATE ${kind} ${assetId} ${stateId} # ${reason}`;
|
|
6798
|
+
if (action === "action_desc")
|
|
6799
|
+
return `ACTION_STATE ${kind} ${assetId} ${stateId} # ${reason}`;
|
|
6800
|
+
if (action === "keep") {
|
|
6801
|
+
const desc = Object.prototype.hasOwnProperty.call(decision, "new_description")
|
|
6802
|
+
? ` DESC=${quoteCurationExpressionString(decision["new_description"])}`
|
|
6803
|
+
: "";
|
|
6804
|
+
return `KEEP_STATE ${kind} ${assetId} ${stateId}${desc} # ${reason}`;
|
|
6805
|
+
}
|
|
6806
|
+
if (action === "rename") {
|
|
6807
|
+
const desc = Object.prototype.hasOwnProperty.call(decision, "new_description")
|
|
6808
|
+
? ` DESC=${quoteCurationExpressionString(decision["new_description"])}`
|
|
6809
|
+
: "";
|
|
6810
|
+
return `RENAME_STATE ${kind} ${assetId} ${stateId} ${quoteCurationExpressionString(decision["new_name"])}${desc} # ${reason}`;
|
|
6811
|
+
}
|
|
6812
|
+
stateCurationBlocked("Cannot render unknown state curation decision.", [`decision: ${action || "<empty>"}`]);
|
|
6813
|
+
}
|
|
6814
|
+
export function formatStateCurationDecisionExpressions(plan) {
|
|
6815
|
+
const decisions = asList(plan["decisions"]).filter(isDict);
|
|
6816
|
+
const defaultSelections = asList(plan["default_selections"]).filter(isDict);
|
|
6817
|
+
const expressions = [...decisions, ...defaultSelections].map((decision) => `<exp>${renderStateCurationDecisionExpression(decision)}</exp>`);
|
|
6818
|
+
if (expressions.length === 0)
|
|
6819
|
+
return "<exp>NOOP # no state curation changes</exp>";
|
|
6820
|
+
return `${expressions.join("\n")}\n`;
|
|
6821
|
+
}
|
|
6822
|
+
function stateCurationAllSourceKeys(script) {
|
|
6823
|
+
const keys = [];
|
|
6824
|
+
for (const ref of statefulAssetsForCuration(script)) {
|
|
6825
|
+
const asset = stateCurationAssetByRef(script, ref);
|
|
6826
|
+
if (!asset)
|
|
6827
|
+
continue;
|
|
6828
|
+
for (const state of asList(asset["states"])) {
|
|
6829
|
+
const stateId = strOf(state["state_id"]);
|
|
6830
|
+
if (stateId)
|
|
6831
|
+
keys.push(stateCurationSourceKey(ref.kind, ref.asset_id, stateId));
|
|
6832
|
+
}
|
|
6833
|
+
}
|
|
6834
|
+
return keys;
|
|
6835
|
+
}
|
|
6836
|
+
export function stateCurationSourceKeysForRefs(script, refs) {
|
|
6837
|
+
const keys = [];
|
|
6838
|
+
for (const ref of refs) {
|
|
6839
|
+
const asset = stateCurationAssetByRef(script, ref);
|
|
6840
|
+
if (!asset)
|
|
6841
|
+
continue;
|
|
6842
|
+
for (const state of asList(asset["states"])) {
|
|
6843
|
+
const stateId = strOf(state["state_id"]);
|
|
6844
|
+
if (stateId)
|
|
6845
|
+
keys.push(stateCurationSourceKey(ref.kind, ref.asset_id, stateId));
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
return keys;
|
|
6849
|
+
}
|
|
6850
|
+
function stateCurationDecisionMap(decisions) {
|
|
6851
|
+
const map = new Map();
|
|
6852
|
+
for (const decision of decisions) {
|
|
6853
|
+
map.set(stateCurationDecisionSourceKey(decision), decision);
|
|
6854
|
+
}
|
|
6855
|
+
return map;
|
|
6856
|
+
}
|
|
6857
|
+
export function missingStateCurationRequiredSourceKeys(script, plan, requiredKeys) {
|
|
6858
|
+
const keys = requiredKeys && requiredKeys.length > 0 ? [...requiredKeys] : stateCurationAllSourceKeys(script);
|
|
6859
|
+
const decisions = stateCurationDecisionMap(asList(plan["decisions"]).filter(isDict));
|
|
6860
|
+
return keys.filter((key) => !decisions.has(key));
|
|
6861
|
+
}
|
|
6862
|
+
function stateCurationRequiredDefaultAssetKeys(script, requiredKeys) {
|
|
6863
|
+
if (requiredKeys && requiredKeys.length > 0) {
|
|
6864
|
+
const keys = new Set();
|
|
6865
|
+
for (const key of requiredKeys) {
|
|
6866
|
+
const match = /^(actor|location|prop):([^/]+)\//.exec(key);
|
|
6867
|
+
if (match)
|
|
6868
|
+
keys.add(stateCurationAssetSourceKey(match[1], match[2]));
|
|
6869
|
+
}
|
|
6870
|
+
return [...keys];
|
|
6871
|
+
}
|
|
6872
|
+
return statefulAssetsForCuration(script).map((ref) => stateCurationAssetSourceKey(ref.kind, ref.asset_id));
|
|
6873
|
+
}
|
|
6874
|
+
function stateCurationDefaultSelectionMap(selections) {
|
|
6875
|
+
const map = new Map();
|
|
6876
|
+
for (const selection of selections) {
|
|
6877
|
+
const key = stateCurationDefaultSelectionSourceKey(selection);
|
|
6878
|
+
if (map.has(key))
|
|
6879
|
+
stateCurationBlocked("State curation plan selects more than one default state for an asset.", [key]);
|
|
6880
|
+
map.set(key, selection);
|
|
6881
|
+
}
|
|
6882
|
+
return map;
|
|
6883
|
+
}
|
|
6884
|
+
function assertStateExistsForCuration(script, kind, assetId, stateId) {
|
|
6885
|
+
const asset = assertAssetExists(script, kind, assetId);
|
|
6886
|
+
for (const state of asList(asset["states"])) {
|
|
6887
|
+
if (strOf(state["state_id"]) === stateId)
|
|
6888
|
+
return state;
|
|
6889
|
+
}
|
|
6890
|
+
stateCurationBlocked("State curation decision references an unknown state.", [stateCurationSourceKey(kind, assetId, stateId)]);
|
|
6891
|
+
}
|
|
6892
|
+
function validateStateCurationDecision(script, decision) {
|
|
6893
|
+
const kind = strOf(decision["kind"]);
|
|
6894
|
+
if (!isAssetKindValue(kind))
|
|
6895
|
+
stateCurationBlocked("Unknown state curation kind.", [`kind: ${kind || "<empty>"}`]);
|
|
6896
|
+
const assetId = strOf(decision["asset_id"]);
|
|
6897
|
+
const stateId = strOf(decision["state_id"]);
|
|
6898
|
+
const action = strOf(decision["decision"]);
|
|
6899
|
+
const reason = strOf(decision["reason"]);
|
|
6900
|
+
const rewritesDescription = Object.prototype.hasOwnProperty.call(decision, "new_description");
|
|
6901
|
+
if (!assetId || !stateId)
|
|
6902
|
+
stateCurationBlocked("State curation decision is missing asset_id/state_id.", [`${kind}:${assetId}/${stateId}`]);
|
|
6903
|
+
if (!reason)
|
|
6904
|
+
stateCurationBlocked("State curation decision is missing reason.", [`${kind}:${assetId}/${stateId}`]);
|
|
6905
|
+
assertStateExistsForCuration(script, kind, assetId, stateId);
|
|
6906
|
+
if (action === "default") {
|
|
6907
|
+
if (rewritesDescription)
|
|
6908
|
+
stateCurationBlocked("DEFAULT_STATE cannot rewrite descriptions.", [`${kind}:${assetId}/${stateId}`]);
|
|
6909
|
+
return;
|
|
6910
|
+
}
|
|
6911
|
+
if (rewritesDescription) {
|
|
6912
|
+
const newDescription = strOf(decision["new_description"]).trim();
|
|
6913
|
+
if (!newDescription)
|
|
6914
|
+
stateCurationBlocked("State description rewrite requires a non-empty description.", [`${kind}:${assetId}/${stateId}`]);
|
|
6915
|
+
if (action !== "keep" && action !== "rename") {
|
|
6916
|
+
stateCurationBlocked("State description rewrite is only valid on KEEP_STATE or RENAME_STATE.", [`${kind}:${assetId}/${stateId}:${action}`]);
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
if (action === "merge") {
|
|
6920
|
+
const targetStateId = strOf(decision["target_state_id"]);
|
|
6921
|
+
if (!targetStateId || targetStateId === stateId) {
|
|
6922
|
+
stateCurationBlocked("MERGE_STATE requires a different target_state_id.", [`${kind}:${assetId}/${stateId}`]);
|
|
6923
|
+
}
|
|
6924
|
+
assertStateExistsForCuration(script, kind, assetId, targetStateId);
|
|
6925
|
+
}
|
|
6926
|
+
else if (action === "rename") {
|
|
6927
|
+
const newName = strOf(decision["new_name"]).trim();
|
|
6928
|
+
if (!newName)
|
|
6929
|
+
stateCurationBlocked("RENAME_STATE requires a non-empty new_name.", [`${kind}:${assetId}/${stateId}`]);
|
|
6930
|
+
}
|
|
6931
|
+
else if (action !== "keep" && action !== "drop" && action !== "action_desc") {
|
|
6932
|
+
stateCurationBlocked("Unknown state curation decision.", [`decision: ${action || "<empty>"}`]);
|
|
6933
|
+
}
|
|
6934
|
+
}
|
|
6935
|
+
function normalizeStateCurationMergeTargets(decisions) {
|
|
6936
|
+
const bySource = stateCurationDecisionMap(decisions);
|
|
6937
|
+
for (const decision of decisions) {
|
|
6938
|
+
if (strOf(decision["decision"]) !== "merge")
|
|
6939
|
+
continue;
|
|
6940
|
+
const kind = strOf(decision["kind"]);
|
|
6941
|
+
const assetId = strOf(decision["asset_id"]);
|
|
6942
|
+
let targetStateId = strOf(decision["target_state_id"]);
|
|
6943
|
+
const seen = new Set([stateCurationDecisionSourceKey(decision)]);
|
|
6944
|
+
while (targetStateId) {
|
|
6945
|
+
const targetKey = stateCurationSourceKey(kind, assetId, targetStateId);
|
|
6946
|
+
if (seen.has(targetKey))
|
|
6947
|
+
stateCurationBlocked("State curation merge plan contains a cycle.", [`cycle at ${targetKey}`]);
|
|
6948
|
+
seen.add(targetKey);
|
|
6949
|
+
const targetDecision = bySource.get(targetKey);
|
|
6950
|
+
if (!targetDecision)
|
|
6951
|
+
break;
|
|
6952
|
+
const targetAction = strOf(targetDecision["decision"]);
|
|
6953
|
+
if (targetAction === "merge") {
|
|
6954
|
+
targetStateId = strOf(targetDecision["target_state_id"]);
|
|
6955
|
+
decision["target_state_id"] = targetStateId;
|
|
6956
|
+
continue;
|
|
6957
|
+
}
|
|
6958
|
+
if (targetAction === "drop" || targetAction === "action_desc") {
|
|
6959
|
+
decision["decision"] = targetAction;
|
|
6960
|
+
delete decision["target_state_id"];
|
|
6961
|
+
}
|
|
6962
|
+
break;
|
|
6963
|
+
}
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
export function assertStateCurationPlan(script, plan, requiredKeys) {
|
|
6967
|
+
const rawDecisions = asList(plan["decisions"]).filter(isDict);
|
|
6968
|
+
const decisions = [...stateCurationDecisionMap(rawDecisions).values()];
|
|
6969
|
+
const defaultSelections = asList(plan["default_selections"]).filter(isDict);
|
|
6970
|
+
for (const decision of decisions)
|
|
6971
|
+
validateStateCurationDecision(script, decision);
|
|
6972
|
+
for (const selection of defaultSelections)
|
|
6973
|
+
validateStateCurationDecision(script, selection);
|
|
6974
|
+
normalizeStateCurationMergeTargets(decisions);
|
|
6975
|
+
const missing = missingStateCurationRequiredSourceKeys(script, { decisions }, requiredKeys);
|
|
6976
|
+
if (missing.length > 0) {
|
|
6977
|
+
stateCurationBlocked("State curation plan is missing required state decisions.", missing.slice(0, 80));
|
|
6978
|
+
}
|
|
6979
|
+
const defaultMap = stateCurationDefaultSelectionMap(defaultSelections);
|
|
6980
|
+
const missingDefaults = stateCurationRequiredDefaultAssetKeys(script, requiredKeys).filter((key) => !defaultMap.has(key));
|
|
6981
|
+
if (missingDefaults.length > 0) {
|
|
6982
|
+
stateCurationBlocked("State curation plan is missing required DEFAULT_STATE selections.", missingDefaults.slice(0, 80));
|
|
6983
|
+
}
|
|
6984
|
+
return {
|
|
6985
|
+
...plan,
|
|
6986
|
+
decisions,
|
|
6987
|
+
default_selections: [...defaultMap.values()],
|
|
6988
|
+
};
|
|
6989
|
+
}
|
|
6990
|
+
function mapStateForCuration(kind, assetId, stateId, replacements, removed) {
|
|
6991
|
+
const id = strOf(stateId);
|
|
6992
|
+
if (!id)
|
|
6993
|
+
return null;
|
|
6994
|
+
const key = stateCurationSourceKey(kind, assetId, id);
|
|
6995
|
+
const replacement = replacements.get(key);
|
|
6996
|
+
if (replacement)
|
|
6997
|
+
return replacement;
|
|
6998
|
+
if (removed.has(key))
|
|
6999
|
+
return null;
|
|
7000
|
+
return id;
|
|
7001
|
+
}
|
|
7002
|
+
function rewriteRefsAfterStateCuration(script, replacements, removed) {
|
|
7003
|
+
let sceneRefsCleared = 0;
|
|
7004
|
+
let sceneRefsMerged = 0;
|
|
7005
|
+
let stateChangesRemoved = 0;
|
|
7006
|
+
let stateChangesRewritten = 0;
|
|
7007
|
+
for (const ep of asList(script["episodes"])) {
|
|
7008
|
+
for (const scene of asList(ep["scenes"])) {
|
|
7009
|
+
const ctx = sceneContext(scene);
|
|
7010
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
7011
|
+
const idKey = assetIdKey(kind);
|
|
7012
|
+
const refKey = assetListKey(kind);
|
|
7013
|
+
for (const ref of asList(ctx[refKey])) {
|
|
7014
|
+
if (!isDict(ref))
|
|
7015
|
+
continue;
|
|
7016
|
+
const assetId = strOf(ref[idKey]);
|
|
7017
|
+
const prior = strOf(ref["state_id"]);
|
|
7018
|
+
const next = mapStateForCuration(kind, assetId, prior, replacements, removed);
|
|
7019
|
+
if (!next && prior) {
|
|
7020
|
+
ref["state_id"] = null;
|
|
7021
|
+
sceneRefsCleared += 1;
|
|
7022
|
+
}
|
|
7023
|
+
else if (next && next !== prior) {
|
|
7024
|
+
ref["state_id"] = next;
|
|
7025
|
+
sceneRefsMerged += 1;
|
|
7026
|
+
}
|
|
7027
|
+
}
|
|
7028
|
+
}
|
|
7029
|
+
setSceneContext(scene, ctx);
|
|
7030
|
+
for (const action of asList(scene["actions"])) {
|
|
7031
|
+
if (!isDict(action) || action["state_changes"] === undefined || action["state_changes"] === null)
|
|
7032
|
+
continue;
|
|
7033
|
+
const nextChanges = [];
|
|
7034
|
+
for (const change of asList(action["state_changes"])) {
|
|
7035
|
+
if (!isDict(change))
|
|
7036
|
+
continue;
|
|
7037
|
+
const kind = strOf(change["target_kind"]);
|
|
7038
|
+
if (!isAssetKindValue(kind))
|
|
7039
|
+
continue;
|
|
7040
|
+
const assetId = strOf(change["target_id"]);
|
|
7041
|
+
const toState = mapStateForCuration(kind, assetId, change["to_state_id"], replacements, removed);
|
|
7042
|
+
if (!toState) {
|
|
7043
|
+
stateChangesRemoved += 1;
|
|
7044
|
+
continue;
|
|
7045
|
+
}
|
|
7046
|
+
const fromState = mapStateForCuration(kind, assetId, change["from_state_id"], replacements, removed);
|
|
7047
|
+
if (fromState && fromState === toState) {
|
|
7048
|
+
stateChangesRemoved += 1;
|
|
7049
|
+
continue;
|
|
7050
|
+
}
|
|
7051
|
+
const next = { ...change, to_state_id: toState };
|
|
7052
|
+
if (fromState)
|
|
7053
|
+
next["from_state_id"] = fromState;
|
|
7054
|
+
else
|
|
7055
|
+
delete next["from_state_id"];
|
|
7056
|
+
if (toState !== strOf(change["to_state_id"]) || fromState !== strOf(change["from_state_id"]))
|
|
7057
|
+
stateChangesRewritten += 1;
|
|
7058
|
+
nextChanges.push(next);
|
|
7059
|
+
}
|
|
7060
|
+
if (nextChanges.length > 0)
|
|
7061
|
+
action["state_changes"] = nextChanges;
|
|
7062
|
+
else
|
|
7063
|
+
delete action["state_changes"];
|
|
7064
|
+
}
|
|
7065
|
+
}
|
|
7066
|
+
}
|
|
7067
|
+
return {
|
|
7068
|
+
scene_refs_cleared: sceneRefsCleared,
|
|
7069
|
+
scene_refs_merged: sceneRefsMerged,
|
|
7070
|
+
state_changes_removed: stateChangesRemoved,
|
|
7071
|
+
state_changes_rewritten: stateChangesRewritten,
|
|
7072
|
+
};
|
|
7073
|
+
}
|
|
7074
|
+
function appendStateCurationMergedFrom(script, kind, assetId, sourceStateId, targetStateId, reason) {
|
|
7075
|
+
const source = assertStateExistsForCuration(script, kind, assetId, sourceStateId);
|
|
7076
|
+
const target = assertStateExistsForCuration(script, kind, assetId, targetStateId);
|
|
7077
|
+
const sourceName = strOf(source["state_name"]).trim();
|
|
7078
|
+
if (!sourceName)
|
|
7079
|
+
return;
|
|
7080
|
+
const existing = asList(target["merged_from"]).filter(isDict);
|
|
7081
|
+
if (existing.some((item) => item["state_name"] === sourceName))
|
|
7082
|
+
return;
|
|
7083
|
+
const next = [
|
|
7084
|
+
...existing,
|
|
7085
|
+
{
|
|
7086
|
+
state_name: sourceName,
|
|
7087
|
+
reason: cleanStateReason(reason),
|
|
7088
|
+
},
|
|
7089
|
+
].slice(0, MERGED_FROM_MAX_ITEMS);
|
|
7090
|
+
target["merged_from"] = next;
|
|
7091
|
+
}
|
|
7092
|
+
function synthesizedDefaultState(asset) {
|
|
7093
|
+
const state = {
|
|
7094
|
+
state_id: "default",
|
|
7095
|
+
state_name: "默认",
|
|
7096
|
+
};
|
|
7097
|
+
const description = strOf(asset["description"]).trim();
|
|
7098
|
+
if (description)
|
|
7099
|
+
state["description"] = description;
|
|
7100
|
+
return state;
|
|
7101
|
+
}
|
|
7102
|
+
function canonicalizeDefaultStatesAfterCuration(script, defaultSelections, replacements, removed) {
|
|
7103
|
+
const selectionByAsset = stateCurationDefaultSelectionMap([...defaultSelections]);
|
|
7104
|
+
const defaultReplacements = new Map();
|
|
7105
|
+
const selectedDefaults = [];
|
|
7106
|
+
let synthesized = 0;
|
|
7107
|
+
let rewritten = 0;
|
|
7108
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
7109
|
+
for (const asset of asList(script[assetListKey(kind)])) {
|
|
7110
|
+
if (!isDict(asset))
|
|
7111
|
+
continue;
|
|
7112
|
+
const assetId = strOf(asset[assetIdKey(kind)]);
|
|
7113
|
+
if (!assetId)
|
|
7114
|
+
continue;
|
|
7115
|
+
const assetKey = stateCurationAssetSourceKey(kind, assetId);
|
|
7116
|
+
const states = asList(asset["states"]).filter(isDict);
|
|
7117
|
+
if (states.length === 0) {
|
|
7118
|
+
asset["states"] = [synthesizedDefaultState(asset)];
|
|
7119
|
+
synthesized += 1;
|
|
7120
|
+
selectedDefaults.push({
|
|
7121
|
+
kind,
|
|
7122
|
+
asset_id: assetId,
|
|
7123
|
+
input_state_id: null,
|
|
7124
|
+
final_state_id: "default",
|
|
7125
|
+
state_name: "默认",
|
|
7126
|
+
synthesized: true,
|
|
7127
|
+
reason: "asset has no surviving explicit states",
|
|
7128
|
+
});
|
|
7129
|
+
continue;
|
|
7130
|
+
}
|
|
7131
|
+
const selection = selectionByAsset.get(assetKey);
|
|
7132
|
+
if (!selection) {
|
|
7133
|
+
stateCurationBlocked("State curation default canonicalization is missing a DEFAULT_STATE selection.", [assetKey]);
|
|
7134
|
+
}
|
|
7135
|
+
const inputStateId = strOf(selection["state_id"]);
|
|
7136
|
+
const selectedStateId = mapStateForCuration(kind, assetId, inputStateId, replacements, removed);
|
|
7137
|
+
if (!selectedStateId) {
|
|
7138
|
+
stateCurationBlocked("DEFAULT_STATE selection was removed while other states survived.", [
|
|
7139
|
+
`${assetKey}/${inputStateId}`,
|
|
7140
|
+
]);
|
|
7141
|
+
}
|
|
7142
|
+
const selectedState = states.find((state) => strOf(state["state_id"]) === selectedStateId);
|
|
7143
|
+
if (!selectedState) {
|
|
7144
|
+
stateCurationBlocked("DEFAULT_STATE selection does not resolve to a surviving state.", [
|
|
7145
|
+
`${assetKey}/${inputStateId} -> ${selectedStateId}`,
|
|
7146
|
+
]);
|
|
7147
|
+
}
|
|
7148
|
+
const existingDefault = states.find((state) => strOf(state["state_id"]) === "default");
|
|
7149
|
+
if (existingDefault && existingDefault !== selectedState) {
|
|
7150
|
+
stateCurationBlocked("State curation would create two `default` states for one asset.", [
|
|
7151
|
+
`${assetKey}: selected=${selectedStateId}`,
|
|
7152
|
+
]);
|
|
7153
|
+
}
|
|
7154
|
+
if (selectedStateId !== "default") {
|
|
7155
|
+
selectedState["state_id"] = "default";
|
|
7156
|
+
defaultReplacements.set(stateCurationSourceKey(kind, assetId, selectedStateId), "default");
|
|
7157
|
+
rewritten += 1;
|
|
7158
|
+
}
|
|
7159
|
+
selectedDefaults.push({
|
|
7160
|
+
kind,
|
|
7161
|
+
asset_id: assetId,
|
|
7162
|
+
input_state_id: inputStateId,
|
|
7163
|
+
final_state_id: "default",
|
|
7164
|
+
original_final_state_id: selectedStateId,
|
|
7165
|
+
state_name: selectedState["state_name"],
|
|
7166
|
+
synthesized: false,
|
|
7167
|
+
reason: selection["reason"],
|
|
7168
|
+
});
|
|
7169
|
+
}
|
|
7170
|
+
}
|
|
7171
|
+
const rewriteSummary = rewriteRefsAfterStateCuration(script, defaultReplacements, new Set());
|
|
7172
|
+
return {
|
|
7173
|
+
default_states_synthesized: synthesized,
|
|
7174
|
+
default_state_ids_rewritten: rewritten,
|
|
7175
|
+
default_ref_rewrite: rewriteSummary,
|
|
7176
|
+
selected_default_states: selectedDefaults,
|
|
7177
|
+
};
|
|
7178
|
+
}
|
|
7179
|
+
export function applyStateCurationPlan(script, rawPlan) {
|
|
7180
|
+
const plan = assertStateCurationPlan(script, rawPlan);
|
|
7181
|
+
const decisions = asList(plan["decisions"]).filter(isDict);
|
|
7182
|
+
const defaultSelections = asList(plan["default_selections"]).filter(isDict);
|
|
7183
|
+
const beforeByKind = { actor: 0, location: 0, prop: 0 };
|
|
7184
|
+
const afterByKind = { actor: 0, location: 0, prop: 0 };
|
|
7185
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
7186
|
+
for (const asset of asList(script[assetListKey(kind)]))
|
|
7187
|
+
beforeByKind[kind] += asList(asset["states"]).filter(isDict).length;
|
|
7188
|
+
}
|
|
7189
|
+
const replacements = new Map();
|
|
7190
|
+
const removed = new Set();
|
|
7191
|
+
const decisionSummary = {
|
|
7192
|
+
keep: 0,
|
|
7193
|
+
merge: 0,
|
|
7194
|
+
drop: 0,
|
|
7195
|
+
action_desc: 0,
|
|
7196
|
+
rename: 0,
|
|
7197
|
+
description_rewrite: 0,
|
|
7198
|
+
};
|
|
7199
|
+
for (const decision of decisions) {
|
|
7200
|
+
const kind = strOf(decision["kind"]);
|
|
7201
|
+
const assetId = strOf(decision["asset_id"]);
|
|
7202
|
+
const stateId = strOf(decision["state_id"]);
|
|
7203
|
+
const key = stateCurationSourceKey(kind, assetId, stateId);
|
|
7204
|
+
const action = strOf(decision["decision"]);
|
|
7205
|
+
const inputState = assertStateExistsForCuration(script, kind, assetId, stateId);
|
|
7206
|
+
const inputStateName = strOf(inputState["state_name"]).trim();
|
|
7207
|
+
if (inputStateName)
|
|
7208
|
+
decision["input_state_name"] = inputStateName;
|
|
7209
|
+
const inputStateReason = cleanStateReason(inputState["state_reason"]);
|
|
7210
|
+
if (inputStateReason)
|
|
7211
|
+
decision["input_state_reason"] = inputStateReason;
|
|
7212
|
+
decisionSummary[action] = Number(decisionSummary[action] ?? 0) + 1;
|
|
7213
|
+
if (action === "keep" || action === "rename") {
|
|
7214
|
+
const state = inputState;
|
|
7215
|
+
if (action === "rename")
|
|
7216
|
+
state["state_name"] = strOf(decision["new_name"]).trim();
|
|
7217
|
+
if (Object.prototype.hasOwnProperty.call(decision, "new_description")) {
|
|
7218
|
+
const newDescription = strOf(decision["new_description"]).trim();
|
|
7219
|
+
if (strOf(state["description"]) !== newDescription)
|
|
7220
|
+
decisionSummary["description_rewrite"] = Number(decisionSummary["description_rewrite"] ?? 0) + 1;
|
|
7221
|
+
state["description"] = newDescription;
|
|
7222
|
+
}
|
|
7223
|
+
}
|
|
7224
|
+
else if (action === "merge") {
|
|
7225
|
+
const targetStateId = strOf(decision["target_state_id"]);
|
|
7226
|
+
appendStateCurationMergedFrom(script, kind, assetId, stateId, targetStateId, strOf(decision["reason"]));
|
|
7227
|
+
replacements.set(key, targetStateId);
|
|
7228
|
+
removed.add(key);
|
|
7229
|
+
}
|
|
7230
|
+
else if (action === "drop" || action === "action_desc") {
|
|
7231
|
+
removed.add(key);
|
|
7232
|
+
}
|
|
7233
|
+
}
|
|
7234
|
+
const rewriteSummary = rewriteRefsAfterStateCuration(script, replacements, removed);
|
|
7235
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
7236
|
+
for (const asset of asList(script[assetListKey(kind)])) {
|
|
7237
|
+
if (!isDict(asset))
|
|
7238
|
+
continue;
|
|
7239
|
+
const assetId = strOf(asset[assetIdKey(kind)]);
|
|
7240
|
+
asset["states"] = asList(asset["states"]).filter((state) => {
|
|
7241
|
+
if (!isDict(state))
|
|
7242
|
+
return false;
|
|
7243
|
+
const key = stateCurationSourceKey(kind, assetId, strOf(state["state_id"]));
|
|
7244
|
+
return !removed.has(key);
|
|
7245
|
+
});
|
|
7246
|
+
}
|
|
7247
|
+
}
|
|
7248
|
+
const defaultSummary = canonicalizeDefaultStatesAfterCuration(script, defaultSelections, replacements, removed);
|
|
7249
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
7250
|
+
for (const asset of asList(script[assetListKey(kind)]))
|
|
7251
|
+
afterByKind[kind] += asList(asset["states"]).filter(isDict).length;
|
|
7252
|
+
}
|
|
7253
|
+
const statesBefore = beforeByKind.actor + beforeByKind.location + beforeByKind.prop;
|
|
7254
|
+
const statesAfter = afterByKind.actor + afterByKind.location + afterByKind.prop;
|
|
7255
|
+
const synthesizedDefaults = Number(defaultSummary["default_states_synthesized"] ?? 0);
|
|
7256
|
+
return {
|
|
7257
|
+
version: 1,
|
|
7258
|
+
format: "state-curation-exp-v1",
|
|
7259
|
+
summary: {
|
|
7260
|
+
states_before: statesBefore,
|
|
7261
|
+
states_after: statesAfter,
|
|
7262
|
+
states_removed: statesBefore - statesAfter + synthesizedDefaults,
|
|
7263
|
+
actor_states_before: beforeByKind.actor,
|
|
7264
|
+
actor_states_after: afterByKind.actor,
|
|
7265
|
+
location_states_before: beforeByKind.location,
|
|
7266
|
+
location_states_after: afterByKind.location,
|
|
7267
|
+
prop_states_before: beforeByKind.prop,
|
|
7268
|
+
prop_states_after: afterByKind.prop,
|
|
7269
|
+
decisions: decisionSummary,
|
|
7270
|
+
...rewriteSummary,
|
|
7271
|
+
default_states_synthesized: synthesizedDefaults,
|
|
7272
|
+
default_state_ids_rewritten: defaultSummary["default_state_ids_rewritten"],
|
|
7273
|
+
default_ref_rewrite: defaultSummary["default_ref_rewrite"],
|
|
7274
|
+
},
|
|
7275
|
+
decisions,
|
|
7276
|
+
default_selections: defaultSelections,
|
|
7277
|
+
selected_default_states: defaultSummary["selected_default_states"],
|
|
7278
|
+
};
|
|
7279
|
+
}
|
|
7280
|
+
// ---------------------------------------------------------------------------
|
|
6174
7281
|
// State binding
|
|
6175
7282
|
// ---------------------------------------------------------------------------
|
|
6176
7283
|
function stateBindingBlocked(message, received) {
|
|
@@ -6223,29 +7330,35 @@ function stateNameById(asset, stateId) {
|
|
|
6223
7330
|
}
|
|
6224
7331
|
function bindableDefaultStateId(asset) {
|
|
6225
7332
|
const states = asList(asset["states"]);
|
|
6226
|
-
|
|
6227
|
-
return "";
|
|
6228
|
-
const defaultState = states.find((state) => strOf(state["state_name"]).trim() === "默认");
|
|
7333
|
+
const defaultState = states.find((state) => strOf(state["state_id"]).trim() === "default");
|
|
6229
7334
|
if (defaultState)
|
|
6230
7335
|
return strOf(defaultState["state_id"]);
|
|
6231
|
-
if (states.length === 1)
|
|
6232
|
-
return strOf(states[0]["state_id"]);
|
|
6233
7336
|
return "";
|
|
6234
7337
|
}
|
|
6235
7338
|
export function ensureDefaultStates(script) {
|
|
6236
|
-
let
|
|
7339
|
+
let synthesized = 0;
|
|
7340
|
+
let missing = 0;
|
|
6237
7341
|
for (const kind of ["actor", "location", "prop"]) {
|
|
6238
7342
|
for (const asset of asList(script[assetListKey(kind)])) {
|
|
6239
|
-
|
|
6240
|
-
if (states.length <= 1)
|
|
6241
|
-
continue;
|
|
6242
|
-
if (states.some((state) => strOf(state["state_name"]).trim() === "默认"))
|
|
7343
|
+
if (!isDict(asset))
|
|
6243
7344
|
continue;
|
|
6244
|
-
|
|
6245
|
-
|
|
7345
|
+
const states = asList(asset["states"]).filter(isDict);
|
|
7346
|
+
const defaults = states.filter((state) => strOf(state["state_id"]).trim() === "default");
|
|
7347
|
+
if (states.length === 0) {
|
|
7348
|
+
asset["states"] = [synthesizedDefaultState(asset)];
|
|
7349
|
+
synthesized += 1;
|
|
7350
|
+
}
|
|
7351
|
+
else if (defaults.length === 0) {
|
|
7352
|
+
missing += 1;
|
|
7353
|
+
}
|
|
7354
|
+
else if (defaults.length > 1) {
|
|
7355
|
+
stateBindingBlocked("Asset has more than one `default` state.", [
|
|
7356
|
+
`${kind}:${strOf(asset[assetIdKey(kind)])}`,
|
|
7357
|
+
]);
|
|
7358
|
+
}
|
|
6246
7359
|
}
|
|
6247
7360
|
}
|
|
6248
|
-
return {
|
|
7361
|
+
return { default_states_synthesized: synthesized, assets_missing_default_state: missing };
|
|
6249
7362
|
}
|
|
6250
7363
|
export function bindDefaultStateRefsInScript(script) {
|
|
6251
7364
|
const assetMaps = {
|
|
@@ -6280,7 +7393,7 @@ export function bindDefaultStateRefsInScript(script) {
|
|
|
6280
7393
|
setSceneContext(scene, ctx);
|
|
6281
7394
|
}
|
|
6282
7395
|
}
|
|
6283
|
-
return {
|
|
7396
|
+
return { default_refs_bound: bound, unresolved_stateful_refs: unresolved };
|
|
6284
7397
|
}
|
|
6285
7398
|
function compactStateBindingAsset(kind, asset) {
|
|
6286
7399
|
return {
|
|
@@ -6380,7 +7493,8 @@ export function buildStateBindingContexts(script, chunkSize = 10) {
|
|
|
6380
7493
|
title: script["title"],
|
|
6381
7494
|
policy: [
|
|
6382
7495
|
"Bind each scene asset ref to the best existing state_id when the actions/context show a durable visual state.",
|
|
6383
|
-
"
|
|
7496
|
+
"Each asset should already have an asset-scoped state_id `default`; keep current_state_id unless actions prove a non-default state.",
|
|
7497
|
+
"Use null only when no existing state fits and current_state_id is already null.",
|
|
6384
7498
|
"Emit state_changes only for explicit transformation, outfit, damage, breakage, repair, or form-change actions.",
|
|
6385
7499
|
"Do not invent new states or assets.",
|
|
6386
7500
|
],
|
|
@@ -6599,18 +7713,27 @@ function splitLocationStatePhrase(rawName) {
|
|
|
6599
7713
|
return null;
|
|
6600
7714
|
return { name: locationName, state };
|
|
6601
7715
|
}
|
|
6602
|
-
function addStateDefinition(stateDefinitions, seen, kind, assetName, stateName, description = null) {
|
|
7716
|
+
function addStateDefinition(stateDefinitions, seen, kind, assetName, stateName, description = null, stateReason = "") {
|
|
6603
7717
|
const state = strOf(stateName).trim();
|
|
6604
7718
|
if (!assetName || !state || stateRejectionReason(kind, state))
|
|
6605
7719
|
return;
|
|
6606
7720
|
const key = `${kind}::${assetName}::${state}`;
|
|
6607
|
-
if (seen.has(key))
|
|
7721
|
+
if (seen.has(key)) {
|
|
7722
|
+
const existing = stateDefinitions.find((item) => item["kind"] === kind && item["asset_name"] === assetName && item["state_name"] === state);
|
|
7723
|
+
if (existing) {
|
|
7724
|
+
const desc = strOf(description).trim();
|
|
7725
|
+
if (desc && !existing["description"])
|
|
7726
|
+
existing["description"] = desc;
|
|
7727
|
+
setStateReason(existing, stateReason);
|
|
7728
|
+
}
|
|
6608
7729
|
return;
|
|
7730
|
+
}
|
|
6609
7731
|
seen.add(key);
|
|
6610
7732
|
const item = { kind, asset_name: assetName, state_name: state };
|
|
6611
7733
|
const desc = strOf(description).trim();
|
|
6612
7734
|
if (desc)
|
|
6613
7735
|
item["description"] = desc;
|
|
7736
|
+
setStateReason(item, stateReason);
|
|
6614
7737
|
stateDefinitions.push(item);
|
|
6615
7738
|
}
|
|
6616
7739
|
function rawStringMap(value) {
|
|
@@ -6624,31 +7747,78 @@ function rawStringMap(value) {
|
|
|
6624
7747
|
}
|
|
6625
7748
|
return out;
|
|
6626
7749
|
}
|
|
7750
|
+
const ACTOR_VOICE_SUFFIX_RE = /\s*(?:VO|V\.O\.|OS|O\.S\.|旁白|画外音|内心独白|内心)\s*$/i;
|
|
7751
|
+
const LEADING_ACTION_ACTOR_PREFIX_RE = /^\s*([^::]{1,24})[::]\s*(\S[\s\S]*)$/;
|
|
7752
|
+
function actorVoiceDisplayName(speaker, actorRegistry, report) {
|
|
7753
|
+
const raw = cleanName(speaker);
|
|
7754
|
+
if (!raw || !ACTOR_VOICE_SUFFIX_RE.test(raw))
|
|
7755
|
+
return null;
|
|
7756
|
+
const base = cleanName(raw.replace(ACTOR_VOICE_SUFFIX_RE, ""));
|
|
7757
|
+
if (!base || base === raw)
|
|
7758
|
+
return null;
|
|
7759
|
+
if (inferNonActorSpeakerKind(base) !== "actor")
|
|
7760
|
+
return null;
|
|
7761
|
+
return canonicalAssetDisplay(actorRegistry, base, report);
|
|
7762
|
+
}
|
|
7763
|
+
function actionPrefixActorDisplayName(prefix, actorRegistry) {
|
|
7764
|
+
const raw = cleanName(prefix);
|
|
7765
|
+
if (!raw)
|
|
7766
|
+
return "";
|
|
7767
|
+
const direct = actorRegistry.displayByKey.get(canonicalAssetNameKey("actor", raw));
|
|
7768
|
+
if (direct)
|
|
7769
|
+
return direct;
|
|
7770
|
+
if (!hasEnumeratedActorName(raw))
|
|
7771
|
+
return "";
|
|
7772
|
+
const base = cleanName(raw.replace(/(?:[甲乙丙丁戊己庚辛壬癸]|[A-Z]|[0-9]+)$/u, ""));
|
|
7773
|
+
if (!base || base === raw)
|
|
7774
|
+
return "";
|
|
7775
|
+
return actorRegistry.displayByKey.get(canonicalAssetNameKey("actor", base)) || "";
|
|
7776
|
+
}
|
|
6627
7777
|
function normalizeSpeakerNameInAction(action, actorRegistry, locationRegistry, propRegistry, report) {
|
|
6628
7778
|
const item = { ...action };
|
|
6629
7779
|
const normalizeByKind = (speaker, speakerKind) => {
|
|
7780
|
+
const actorVoice = actorVoiceDisplayName(speaker, actorRegistry, report);
|
|
7781
|
+
if (actorVoice)
|
|
7782
|
+
return { speaker: actorVoice, speaker_kind: "actor" };
|
|
6630
7783
|
const kind = strOf(speakerKind || "actor").trim() || "actor";
|
|
6631
7784
|
if (kind === "actor")
|
|
6632
|
-
return canonicalAssetDisplay(actorRegistry, speaker, report);
|
|
7785
|
+
return { speaker: canonicalAssetDisplay(actorRegistry, speaker, report), speaker_kind: "actor" };
|
|
6633
7786
|
if (kind === "location")
|
|
6634
|
-
return canonicalAssetDisplay(locationRegistry, speaker, report);
|
|
7787
|
+
return { speaker: canonicalAssetDisplay(locationRegistry, speaker, report), speaker_kind: "location" };
|
|
6635
7788
|
if (kind === "prop")
|
|
6636
|
-
return canonicalAssetDisplay(propRegistry, speaker, report);
|
|
6637
|
-
return cleanName(speaker);
|
|
7789
|
+
return { speaker: canonicalAssetDisplay(propRegistry, speaker, report), speaker_kind: "prop" };
|
|
7790
|
+
return { speaker: cleanName(speaker), speaker_kind: kind };
|
|
6638
7791
|
};
|
|
6639
|
-
if (item["speaker"])
|
|
6640
|
-
|
|
7792
|
+
if (item["speaker"]) {
|
|
7793
|
+
const normalized = normalizeByKind(item["speaker"], item["speaker_kind"]);
|
|
7794
|
+
item["speaker"] = normalized.speaker;
|
|
7795
|
+
if (normalized.speaker_kind === "actor")
|
|
7796
|
+
delete item["speaker_kind"];
|
|
7797
|
+
else
|
|
7798
|
+
item["speaker_kind"] = normalized.speaker_kind;
|
|
7799
|
+
}
|
|
6641
7800
|
if (isList(item["speakers"])) {
|
|
6642
|
-
item["speakers"] = asList(item["speakers"]).map((speaker) =>
|
|
6643
|
-
|
|
6644
|
-
speaker:
|
|
6645
|
-
})
|
|
7801
|
+
item["speakers"] = asList(item["speakers"]).map((speaker) => {
|
|
7802
|
+
const normalized = normalizeByKind(speaker["speaker"], speaker["speaker_kind"]);
|
|
7803
|
+
return { ...speaker, speaker: normalized.speaker, speaker_kind: normalized.speaker_kind };
|
|
7804
|
+
});
|
|
6646
7805
|
}
|
|
6647
7806
|
if (isList(item["lines"])) {
|
|
6648
|
-
item["lines"] = asList(item["lines"]).map((line) =>
|
|
6649
|
-
|
|
6650
|
-
speaker:
|
|
6651
|
-
})
|
|
7807
|
+
item["lines"] = asList(item["lines"]).map((line) => {
|
|
7808
|
+
const normalized = normalizeByKind(line["speaker"], line["speaker_kind"]);
|
|
7809
|
+
return { ...line, speaker: normalized.speaker, speaker_kind: normalized.speaker_kind };
|
|
7810
|
+
});
|
|
7811
|
+
}
|
|
7812
|
+
if (strOf(item["type"]) === "action") {
|
|
7813
|
+
const content = strOf(item["content"]).trim();
|
|
7814
|
+
const prefixed = LEADING_ACTION_ACTOR_PREFIX_RE.exec(content);
|
|
7815
|
+
if (prefixed) {
|
|
7816
|
+
const rawPrefix = cleanName(prefixed[1]);
|
|
7817
|
+
const rest = strOf(prefixed[2]).trim();
|
|
7818
|
+
const actorName = actionPrefixActorDisplayName(rawPrefix, actorRegistry);
|
|
7819
|
+
if (actorName && rest)
|
|
7820
|
+
item["content"] = `${actorName}${rest}`;
|
|
7821
|
+
}
|
|
6652
7822
|
}
|
|
6653
7823
|
return item;
|
|
6654
7824
|
}
|
|
@@ -6684,10 +7854,13 @@ function normalizeEpisodeResultsForAssets(results, opts) {
|
|
|
6684
7854
|
for (const asset of asList(result["actors"])) {
|
|
6685
7855
|
if (!isDict(asset))
|
|
6686
7856
|
continue;
|
|
6687
|
-
const rawName =
|
|
7857
|
+
const [rawName, stateFromName] = splitEntityState(asset["name"]);
|
|
6688
7858
|
const name = canonicalAssetDisplay(actorRegistry, rawName, report);
|
|
6689
7859
|
if (name)
|
|
6690
7860
|
normalized["actors"].push({ ...asset, name });
|
|
7861
|
+
if (name && stateFromName) {
|
|
7862
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "actor", name, stateFromName, asset["description"]);
|
|
7863
|
+
}
|
|
6691
7864
|
}
|
|
6692
7865
|
for (const asset of asList(result["locations"])) {
|
|
6693
7866
|
if (!isDict(asset))
|
|
@@ -6719,7 +7892,11 @@ function normalizeEpisodeResultsForAssets(results, opts) {
|
|
|
6719
7892
|
if (!isDict(speaker))
|
|
6720
7893
|
continue;
|
|
6721
7894
|
const sourceKind = strOf(speaker["source_kind"] || "other").trim();
|
|
6722
|
-
|
|
7895
|
+
const actorVoice = actorVoiceDisplayName(speaker["name"], actorRegistry, report);
|
|
7896
|
+
if (actorVoice) {
|
|
7897
|
+
normalized["actors"].push({ name: actorVoice });
|
|
7898
|
+
}
|
|
7899
|
+
else if (sourceKind === "prop") {
|
|
6723
7900
|
normalized["speakers"].push({
|
|
6724
7901
|
...speaker,
|
|
6725
7902
|
name: canonicalAssetDisplay(propRegistry, speaker["name"], report),
|
|
@@ -6732,9 +7909,10 @@ function normalizeEpisodeResultsForAssets(results, opts) {
|
|
|
6732
7909
|
});
|
|
6733
7910
|
}
|
|
6734
7911
|
else if (sourceKind === "actor") {
|
|
7912
|
+
const [speakerName] = splitEntityState(speaker["name"]);
|
|
6735
7913
|
normalized["speakers"].push({
|
|
6736
7914
|
...speaker,
|
|
6737
|
-
name: canonicalAssetDisplay(actorRegistry,
|
|
7915
|
+
name: canonicalAssetDisplay(actorRegistry, speakerName, report),
|
|
6738
7916
|
});
|
|
6739
7917
|
}
|
|
6740
7918
|
else {
|
|
@@ -6748,14 +7926,18 @@ function normalizeEpisodeResultsForAssets(results, opts) {
|
|
|
6748
7926
|
if (kind !== "actor" && kind !== "location" && kind !== "prop")
|
|
6749
7927
|
continue;
|
|
6750
7928
|
if (kind === "actor") {
|
|
6751
|
-
const
|
|
6752
|
-
|
|
7929
|
+
const [assetBaseName, stateFromAssetName] = splitEntityState(stateDef["asset_name"]);
|
|
7930
|
+
const assetName = canonicalAssetDisplay(actorRegistry, assetBaseName, report);
|
|
7931
|
+
if (stateFromAssetName) {
|
|
7932
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "actor", assetName, stateFromAssetName, stateDef["description"], stateDef["state_reason"]);
|
|
7933
|
+
}
|
|
7934
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "actor", assetName, stateDef["state_name"], stateDef["description"], stateDef["state_reason"]);
|
|
6753
7935
|
}
|
|
6754
7936
|
else if (kind === "location") {
|
|
6755
7937
|
const split = splitLocationStatePhrase(stateDef["asset_name"]);
|
|
6756
7938
|
const assetName = canonicalAssetDisplay(locationRegistry, split ? split.name : stateDef["asset_name"], report);
|
|
6757
7939
|
if (split) {
|
|
6758
|
-
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "location", assetName, split.state, stateDef["description"]);
|
|
7940
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "location", assetName, split.state, stateDef["description"], stateDef["state_reason"]);
|
|
6759
7941
|
addNormalizationDecision(report, {
|
|
6760
7942
|
action: "location_state_from_name",
|
|
6761
7943
|
kind: "location",
|
|
@@ -6765,11 +7947,11 @@ function normalizeEpisodeResultsForAssets(results, opts) {
|
|
|
6765
7947
|
reason: "地点状态定义的资产名含时间状态,拆成稳定地点 + 状态。",
|
|
6766
7948
|
});
|
|
6767
7949
|
}
|
|
6768
|
-
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "location", assetName, stateDef["state_name"], stateDef["description"]);
|
|
7950
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "location", assetName, stateDef["state_name"], stateDef["description"], stateDef["state_reason"]);
|
|
6769
7951
|
}
|
|
6770
7952
|
else {
|
|
6771
7953
|
const assetName = canonicalAssetDisplay(propRegistry, stateDef["asset_name"], report);
|
|
6772
|
-
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "prop", assetName, stateDef["state_name"], stateDef["description"]);
|
|
7954
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "prop", assetName, stateDef["state_name"], stateDef["description"], stateDef["state_reason"]);
|
|
6773
7955
|
}
|
|
6774
7956
|
}
|
|
6775
7957
|
const scenes = [];
|
|
@@ -6813,12 +7995,12 @@ function normalizeEpisodeResultsForAssets(results, opts) {
|
|
|
6813
7995
|
const propNames = [];
|
|
6814
7996
|
const normalizedPropStates = {};
|
|
6815
7997
|
for (const rawActor of asList(scene["actor_names"])) {
|
|
6816
|
-
const cleanActor =
|
|
7998
|
+
const [cleanActor, stateFromName] = splitEntityState(rawActor);
|
|
6817
7999
|
const actorName = canonicalAssetDisplay(actorRegistry, cleanActor, report);
|
|
6818
8000
|
if (!actorName)
|
|
6819
8001
|
continue;
|
|
6820
8002
|
uniqueAdd(actorNames, actorName);
|
|
6821
|
-
const state = actorStates[cleanActor] || actorStates[actorName] || "";
|
|
8003
|
+
const state = actorStates[cleanActor] || actorStates[actorName] || stateFromName || "";
|
|
6822
8004
|
if (!state)
|
|
6823
8005
|
continue;
|
|
6824
8006
|
normalizedActorStates[actorName] = state;
|
|
@@ -7033,6 +8215,12 @@ function mergeEpisodeResultsInternal(results, title, opts = {}) {
|
|
|
7033
8215
|
for (const name of speakerOrder) {
|
|
7034
8216
|
const record = speakerRecords.get(name) ?? { source_kind: "other" };
|
|
7035
8217
|
let sourceKind = record.source_kind || "other";
|
|
8218
|
+
if ((sourceKind === "system" || sourceKind === "broadcast" || sourceKind === "other") && actorIds.has(name)) {
|
|
8219
|
+
const speakerId = `spk_${actorIds.get(name)}`;
|
|
8220
|
+
speakerIdsByName.set(`${sourceKind}::${name}`, speakerId);
|
|
8221
|
+
speakerIdsByName.set(`${record.source_kind}::${name}`, speakerId);
|
|
8222
|
+
continue;
|
|
8223
|
+
}
|
|
7036
8224
|
let sourceId = null;
|
|
7037
8225
|
if (sourceKind === "prop")
|
|
7038
8226
|
sourceId = propIds.get(name) ?? null;
|
|
@@ -7082,6 +8270,9 @@ function mergeEpisodeResultsInternal(results, title, opts = {}) {
|
|
|
7082
8270
|
const description = strOf(stateDef["description"]).trim();
|
|
7083
8271
|
if (description)
|
|
7084
8272
|
stateEntry["description"] = description;
|
|
8273
|
+
const stateReason = cleanStateReason(stateDef["state_reason"]);
|
|
8274
|
+
if (stateReason)
|
|
8275
|
+
stateEntry["state_reason"] = stateReason;
|
|
7085
8276
|
states.push(stateEntry);
|
|
7086
8277
|
}
|
|
7087
8278
|
if (states.length > 0)
|
|
@@ -7107,6 +8298,9 @@ function mergeEpisodeResultsInternal(results, title, opts = {}) {
|
|
|
7107
8298
|
const description = strOf(stateDef["description"]).trim();
|
|
7108
8299
|
if (description)
|
|
7109
8300
|
stateEntry["description"] = description;
|
|
8301
|
+
const stateReason = cleanStateReason(stateDef["state_reason"]);
|
|
8302
|
+
if (stateReason)
|
|
8303
|
+
stateEntry["state_reason"] = stateReason;
|
|
7110
8304
|
states.push(stateEntry);
|
|
7111
8305
|
}
|
|
7112
8306
|
if (states.length > 0)
|
|
@@ -7132,6 +8326,9 @@ function mergeEpisodeResultsInternal(results, title, opts = {}) {
|
|
|
7132
8326
|
const description = strOf(stateDef["description"]).trim();
|
|
7133
8327
|
if (description)
|
|
7134
8328
|
stateEntry["description"] = description;
|
|
8329
|
+
const stateReason = cleanStateReason(stateDef["state_reason"]);
|
|
8330
|
+
if (stateReason)
|
|
8331
|
+
stateEntry["state_reason"] = stateReason;
|
|
7135
8332
|
states.push(stateEntry);
|
|
7136
8333
|
}
|
|
7137
8334
|
if (states.length > 0)
|
|
@@ -7146,6 +8343,8 @@ function mergeEpisodeResultsInternal(results, title, opts = {}) {
|
|
|
7146
8343
|
if (speakerKind === "actor" && actorIds.has(speaker))
|
|
7147
8344
|
return `spk_${actorIds.get(speaker)}`;
|
|
7148
8345
|
const normKind = _MD_SPEAKER_KIND_NORM[speakerKind.toLowerCase()] ?? _MD_SPEAKER_KIND_NORM[speakerKind] ?? "other";
|
|
8346
|
+
if ((normKind === "system" || normKind === "broadcast" || normKind === "other") && actorIds.has(speaker))
|
|
8347
|
+
return `spk_${actorIds.get(speaker)}`;
|
|
7149
8348
|
return speakerIdsByName.get(`${normKind}::${speaker}`) ?? speakerIdsByName.get(`other::${speaker}`) ?? null;
|
|
7150
8349
|
};
|
|
7151
8350
|
const stateIdForRef = (kind, assetName, explicitState, unresolved) => {
|