@lingjingai/scriptctl 0.33.0 → 0.35.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/README.md +9 -0
- package/changes/0.35.0.md +22 -0
- package/dist/bin.js +0 -0
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/common.js +29 -6
- package/dist/common.js.map +1 -1
- package/dist/domain/direct-core.d.ts +4 -1
- package/dist/domain/direct-core.js +718 -177
- 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 +31 -7
- package/dist/help-text.js.map +1 -1
- package/dist/infra/providers.js +76 -24
- package/dist/infra/providers.js.map +1 -1
- package/dist/usecases/direct.js +25 -9
- package/dist/usecases/direct.js.map +1 -1
- package/dist/usecases/script/actions.js +1 -1
- package/dist/usecases/script/actions.js.map +1 -1
- package/dist/usecases/script/actors.js +1 -0
- package/dist/usecases/script/actors.js.map +1 -1
- package/dist/usecases/script/assets.js +2 -1
- package/dist/usecases/script/assets.js.map +1 -1
- package/dist/usecases/script/lib.d.ts +11 -0
- package/dist/usecases/script/lib.js +93 -3
- package/dist/usecases/script/lib.js.map +1 -1
- package/dist/usecases/script/locations.js +1 -0
- package/dist/usecases/script/locations.js.map +1 -1
- package/dist/usecases/script/props.js +1 -0
- package/dist/usecases/script/props.js.map +1 -1
- package/dist/usecases/script/states.js +58 -13
- package/dist/usecases/script/states.js.map +1 -1
- package/dist/usecases/script/summary.js +8 -0
- package/dist/usecases/script/summary.js.map +1 -1
- package/package.json +17 -14
- package/scripts/install-skill.mjs +120 -0
- package/skills/scriptctl/SKILL.md +273 -0
- package/skills/scriptctl/references/atomic-write-workflow.md +114 -0
- package/skills/scriptctl/references/direct-workflow.md +72 -0
- package/skills/scriptctl/references/state-reference-repair.md +72 -0
|
@@ -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) {
|
|
@@ -4299,8 +4410,9 @@ function auditCandidateReason(script, decision, evidence, relatedById) {
|
|
|
4299
4410
|
return "speaking_actor_removed_or_speaker";
|
|
4300
4411
|
}
|
|
4301
4412
|
if (action === "keep" && asset) {
|
|
4302
|
-
if (hasEnumeratedActorName(strOf(asset["actor_name"])))
|
|
4413
|
+
if (hasEnumeratedActorName(strOf(asset["actor_name"])) && item.dialogueCount === 0 && item.speakingScenes.size === 0) {
|
|
4303
4414
|
return "kept_enumerated_actor_review";
|
|
4415
|
+
}
|
|
4304
4416
|
const identity = actorIdentityCandidates(script, sourceId, strOf(asset["actor_name"]), item);
|
|
4305
4417
|
if (identity.length > 0)
|
|
4306
4418
|
return "kept_actor_identity_or_form_review";
|
|
@@ -4379,7 +4491,8 @@ export function buildAssetCurationAuditContextText(script, primaryPlan, selected
|
|
|
4379
4491
|
lines.push("- Output expressions only for listed candidates when the primary decision should be corrected or when the candidate is marked primary=MISSING.");
|
|
4380
4492
|
lines.push("- Otherwise output exactly <exp>NOOP # no asset curation changes</exp>.");
|
|
4381
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.");
|
|
4382
|
-
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 众人/宾客们/围观者.");
|
|
4383
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.");
|
|
4384
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()}.`);
|
|
4385
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.");
|
|
@@ -4567,7 +4680,7 @@ export function assertLocationCanonicalizationPlan(rawPlan) {
|
|
|
4567
4680
|
const invalid = [];
|
|
4568
4681
|
for (const decision of normalizedCurationDecisions(rawPlan)) {
|
|
4569
4682
|
const kind = strOf(decision["kind"]);
|
|
4570
|
-
|
|
4683
|
+
let action = strOf(decision["decision"]);
|
|
4571
4684
|
const sourceId = strOf(decision["source_id"]);
|
|
4572
4685
|
if (kind !== "location" || (action !== "rename" && action !== "merge")) {
|
|
4573
4686
|
invalid.push(`${kind || "<empty>"}:${sourceId || "<empty>"} ${action || "<empty>"}`);
|
|
@@ -5278,34 +5391,44 @@ export function combinePrimaryAssetCurationPlans(plans) {
|
|
|
5278
5391
|
primary_part_expression_text: expressionTextByPlan.join("\n"),
|
|
5279
5392
|
};
|
|
5280
5393
|
}
|
|
5281
|
-
export function combineAssetCurationPlans(primaryPlan, auditPlan, allowedNewAuditSources = []) {
|
|
5394
|
+
export function combineAssetCurationPlans(primaryPlan, auditPlan, allowedNewAuditSources = [], options = {}) {
|
|
5282
5395
|
const primaryDecisions = asList(primaryPlan["decisions"]).filter(isDict);
|
|
5283
|
-
const
|
|
5396
|
+
const rawAuditDecisions = asList(auditPlan["decisions"]).filter(isDict);
|
|
5284
5397
|
const primaryKeys = curationDecisionSourceSet(primaryDecisions);
|
|
5285
5398
|
const allowedNewKeys = new Set(allowedNewAuditSources);
|
|
5286
5399
|
const invalidAuditSources = [];
|
|
5287
5400
|
let addedAuditDecisions = 0;
|
|
5288
|
-
|
|
5401
|
+
const auditDecisions = [];
|
|
5402
|
+
for (const decision of rawAuditDecisions) {
|
|
5289
5403
|
const key = curationSourceKey(strOf(decision["kind"]), strOf(decision["source_id"]));
|
|
5290
|
-
if (primaryKeys.has(key))
|
|
5404
|
+
if (primaryKeys.has(key)) {
|
|
5405
|
+
auditDecisions.push(decision);
|
|
5291
5406
|
continue;
|
|
5407
|
+
}
|
|
5292
5408
|
if (allowedNewKeys.has(key)) {
|
|
5293
5409
|
addedAuditDecisions += 1;
|
|
5410
|
+
auditDecisions.push(decision);
|
|
5294
5411
|
continue;
|
|
5295
5412
|
}
|
|
5296
5413
|
invalidAuditSources.push(key);
|
|
5297
5414
|
}
|
|
5298
|
-
if (invalidAuditSources.length > 0) {
|
|
5415
|
+
if (invalidAuditSources.length > 0 && !options.salvageInvalidAuditSources) {
|
|
5299
5416
|
curationBlocked("Audit curation expressions may only override primary decision sources or listed missing required sources.", invalidAuditSources);
|
|
5300
5417
|
}
|
|
5301
5418
|
const primaryExpressions = asList(primaryPlan["expressions"]);
|
|
5302
|
-
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"]);
|
|
5303
5425
|
return {
|
|
5304
5426
|
version: 3,
|
|
5305
5427
|
format: "asset-curation-exp-v1",
|
|
5306
5428
|
primary_expression_text: strOf(primaryPlan["expression_text"]),
|
|
5307
|
-
audit_expression_text:
|
|
5308
|
-
|
|
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"),
|
|
5309
5432
|
primary_expressions: primaryExpressions,
|
|
5310
5433
|
audit_expressions: auditExpressions,
|
|
5311
5434
|
expressions: [...primaryExpressions, ...auditExpressions],
|
|
@@ -5313,7 +5436,10 @@ export function combineAssetCurationPlans(primaryPlan, auditPlan, allowedNewAudi
|
|
|
5313
5436
|
audit_summary: {
|
|
5314
5437
|
primary_decisions: primaryDecisions.length,
|
|
5315
5438
|
audit_decisions: auditDecisions.length,
|
|
5439
|
+
audit_raw_decisions: rawAuditDecisions.length,
|
|
5316
5440
|
audit_added_decisions: addedAuditDecisions,
|
|
5441
|
+
audit_rejected_decisions: invalidAuditSources.length,
|
|
5442
|
+
audit_rejected_sources: invalidAuditSources,
|
|
5317
5443
|
audit_noop: auditDecisions.length === 0,
|
|
5318
5444
|
},
|
|
5319
5445
|
};
|
|
@@ -5679,7 +5805,7 @@ function normalizeCurationPlanTargets(script, decisions) {
|
|
|
5679
5805
|
normalizeMergeCycles(script, decisions);
|
|
5680
5806
|
const decisionsBySource = buildCurationDecisionIndex(decisions);
|
|
5681
5807
|
for (const decision of decisions) {
|
|
5682
|
-
|
|
5808
|
+
let action = strOf(decision["decision"]);
|
|
5683
5809
|
if (action !== "merge" && action !== "move_to_state" && action !== "move_to_prop")
|
|
5684
5810
|
continue;
|
|
5685
5811
|
const targetId = strOf(decision["target_id"]);
|
|
@@ -5829,6 +5955,71 @@ function rewriteCurationRefs(script, replacementsByKind, droppedByKind, stateRep
|
|
|
5829
5955
|
}
|
|
5830
5956
|
}
|
|
5831
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
|
+
}
|
|
5832
6023
|
function copyStatesForMerge(script, kind, source, target, stateReplacements) {
|
|
5833
6024
|
for (const state of asList(source["states"])) {
|
|
5834
6025
|
const sourceStateId = strOf(state["state_id"]);
|
|
@@ -6019,6 +6210,7 @@ function applyAssetCurationDecisions(script, decisions, coverageSummary, default
|
|
|
6019
6210
|
}
|
|
6020
6211
|
}
|
|
6021
6212
|
}
|
|
6213
|
+
const speakerNormalizationSummary = normalizeNonActorSpeakersToActorSpeakers(script);
|
|
6022
6214
|
const actorsAfter = asList(script["actors"]).length;
|
|
6023
6215
|
const locationsAfter = asList(script["locations"]).length;
|
|
6024
6216
|
const propsAfter = asList(script["props"]).length;
|
|
@@ -6042,6 +6234,8 @@ function applyAssetCurationDecisions(script, decisions, coverageSummary, default
|
|
|
6042
6234
|
default_policy: defaultPolicy,
|
|
6043
6235
|
coverage: coverageSummary,
|
|
6044
6236
|
actions: summarizeActionableCuration(decisions),
|
|
6237
|
+
speaker_refs_normalized: speakerNormalizationSummary["speaker_refs_normalized"],
|
|
6238
|
+
speakers_normalized_removed: speakerNormalizationSummary["speakers_removed"],
|
|
6045
6239
|
},
|
|
6046
6240
|
decisions,
|
|
6047
6241
|
};
|
|
@@ -6201,6 +6395,12 @@ function stateCurationSourceKey(kind, assetId, stateId) {
|
|
|
6201
6395
|
function stateCurationDecisionSourceKey(decision) {
|
|
6202
6396
|
return stateCurationSourceKey(strOf(decision["kind"]), strOf(decision["asset_id"]), strOf(decision["state_id"]));
|
|
6203
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
|
+
}
|
|
6204
6404
|
function stateCurationUsageFor(script) {
|
|
6205
6405
|
const usage = new Map();
|
|
6206
6406
|
const ensure = (kind, assetId, stateId) => {
|
|
@@ -6279,6 +6479,36 @@ function formatStateCurationUsage(usage) {
|
|
|
6279
6479
|
return "refs=0 changes=0";
|
|
6280
6480
|
return `refs=${usage.sceneRefs.size} changes=${usage.stateChanges.size}`;
|
|
6281
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
|
+
}
|
|
6282
6512
|
function stateCurationPrefix(kind) {
|
|
6283
6513
|
if (kind === "actor")
|
|
6284
6514
|
return "A";
|
|
@@ -6302,8 +6532,10 @@ function formatStateCurationAssetBlock(script, ref, usageByKey) {
|
|
|
6302
6532
|
if (!stateId || !stateName)
|
|
6303
6533
|
continue;
|
|
6304
6534
|
const usage = usageByKey.get(stateCurationSourceKey(ref.kind, ref.asset_id, stateId));
|
|
6305
|
-
|
|
6535
|
+
const stateReason = compactStateCurationText(state["state_reason"], 220);
|
|
6536
|
+
lines.push(` S | ${stateId} | ${stateName} | ${formatStateCurationUsage(usage)} | desc=${compactStateCurationText(state["description"], 180) || "-"} | reason=${stateReason || "-"}`);
|
|
6306
6537
|
}
|
|
6538
|
+
lines.push(...formatStateCurationClusterHints(states));
|
|
6307
6539
|
return lines;
|
|
6308
6540
|
}
|
|
6309
6541
|
export function buildStateCurationContextText(script, refs) {
|
|
@@ -6317,20 +6549,29 @@ export function buildStateCurationContextText(script, refs) {
|
|
|
6317
6549
|
lines.push("");
|
|
6318
6550
|
lines.push("Policy:");
|
|
6319
6551
|
lines.push("- A state is a large reusable visual asset form, not a plot/action/emotion/pose label.");
|
|
6320
|
-
lines.push("- KEEP only 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,
|
|
6321
|
-
lines.push("-
|
|
6322
|
-
lines.push("-
|
|
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.");
|
|
6323
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.");
|
|
6324
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.");
|
|
6325
6565
|
lines.push("- Use only state_id values listed under the same asset block. Do not invent ids or cross-asset merge targets.");
|
|
6326
6566
|
lines.push("- If a broad canonical state name is needed, RENAME_STATE one existing representative state and MERGE_STATE the variants into it.");
|
|
6327
6567
|
lines.push("");
|
|
6328
6568
|
lines.push("Expression grammar:");
|
|
6329
|
-
lines.push("- <exp>KEEP_STATE <kind> <asset_id> <state_id> # reason</exp>");
|
|
6569
|
+
lines.push("- <exp>KEEP_STATE <kind> <asset_id> <state_id> [DESC=\"<clean visual description>\"] # reason</exp>");
|
|
6330
6570
|
lines.push("- <exp>MERGE_STATE <kind> <asset_id> <state_id> -> <target_state_id> # reason</exp>");
|
|
6331
6571
|
lines.push("- <exp>DROP_STATE <kind> <asset_id> <state_id> # reason</exp>");
|
|
6332
6572
|
lines.push("- <exp>ACTION_STATE <kind> <asset_id> <state_id> # reason</exp>");
|
|
6333
|
-
lines.push("- <exp>RENAME_STATE <kind> <asset_id> <state_id> \"<new_name>\" # 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>");
|
|
6334
6575
|
lines.push("- <kind> is one of actor, location, prop. Use double quotes for names; escape only \\\" and \\\\.");
|
|
6335
6576
|
lines.push("");
|
|
6336
6577
|
lines.push("Assets:");
|
|
@@ -6351,6 +6592,14 @@ export function buildStateCurationContextText(script, refs) {
|
|
|
6351
6592
|
lines.push(`- ${stateCurationSourceKey(ref.kind, ref.asset_id, stateId)}`);
|
|
6352
6593
|
}
|
|
6353
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
|
+
}
|
|
6354
6603
|
return `${lines.join("\n")}\n`;
|
|
6355
6604
|
}
|
|
6356
6605
|
export function buildStateCurationChunks(script, options = {}) {
|
|
@@ -6444,9 +6693,22 @@ function parseStateCurationExpression(expression) {
|
|
|
6444
6693
|
const { command, reason } = splitStateCurationExpressionReason(expression.trim());
|
|
6445
6694
|
if (/^NOOP$/i.test(command))
|
|
6446
6695
|
return null;
|
|
6447
|
-
let match = /^
|
|
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);
|
|
6448
6707
|
if (match) {
|
|
6449
|
-
|
|
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;
|
|
6450
6712
|
}
|
|
6451
6713
|
match = /^MERGE_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)\s+->\s+(\S+)$/i.exec(command);
|
|
6452
6714
|
if (match) {
|
|
@@ -6459,17 +6721,17 @@ function parseStateCurationExpression(expression) {
|
|
|
6459
6721
|
reason,
|
|
6460
6722
|
};
|
|
6461
6723
|
}
|
|
6462
|
-
match = /^DROP_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)
|
|
6724
|
+
match = /^DROP_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)(?:\s+DESC=".*")?$/i.exec(command);
|
|
6463
6725
|
if (match) {
|
|
6464
6726
|
return { kind: match[1].toLowerCase(), asset_id: match[2], state_id: match[3], decision: "drop", reason };
|
|
6465
6727
|
}
|
|
6466
|
-
match = /^ACTION_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)
|
|
6728
|
+
match = /^ACTION_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)(?:\s+DESC=".*")?$/i.exec(command);
|
|
6467
6729
|
if (match) {
|
|
6468
6730
|
return { kind: match[1].toLowerCase(), asset_id: match[2], state_id: match[3], decision: "action_desc", reason };
|
|
6469
6731
|
}
|
|
6470
|
-
match = /^RENAME_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)\s+"((?:\\.|[^"\\])*)"
|
|
6732
|
+
match = /^RENAME_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)\s+"((?:\\.|[^"\\])*)"(?:\s+DESC="(.*)")?$/i.exec(command);
|
|
6471
6733
|
if (match) {
|
|
6472
|
-
|
|
6734
|
+
const decision = {
|
|
6473
6735
|
kind: match[1].toLowerCase(),
|
|
6474
6736
|
asset_id: match[2],
|
|
6475
6737
|
state_id: match[3],
|
|
@@ -6477,23 +6739,18 @@ function parseStateCurationExpression(expression) {
|
|
|
6477
6739
|
new_name: unquoteStateCurationExpressionString(match[4]),
|
|
6478
6740
|
reason,
|
|
6479
6741
|
};
|
|
6742
|
+
if (match[5] !== undefined)
|
|
6743
|
+
decision["new_description"] = unquoteStateCurationExpressionString(match[5]);
|
|
6744
|
+
return decision;
|
|
6480
6745
|
}
|
|
6481
6746
|
stateCurationBlocked("Unknown or malformed state curation expression.", [expression]);
|
|
6482
6747
|
}
|
|
6483
6748
|
function extractStateCurationExpressions(rawText) {
|
|
6484
6749
|
const expressions = [];
|
|
6485
6750
|
const re = /<exp>([\s\S]*?)<\/exp>/g;
|
|
6486
|
-
let outside = "";
|
|
6487
|
-
let lastIndex = 0;
|
|
6488
6751
|
let match;
|
|
6489
6752
|
while ((match = re.exec(rawText)) !== null) {
|
|
6490
|
-
outside += rawText.slice(lastIndex, match.index);
|
|
6491
6753
|
expressions.push(strOf(match[1]).trim());
|
|
6492
|
-
lastIndex = match.index + match[0].length;
|
|
6493
|
-
}
|
|
6494
|
-
outside += rawText.slice(lastIndex);
|
|
6495
|
-
if (outside.trim()) {
|
|
6496
|
-
stateCurationBlocked("State curation response must contain only <exp>...</exp> lines.", [outside.trim().slice(0, 200)]);
|
|
6497
6754
|
}
|
|
6498
6755
|
if (expressions.length === 0) {
|
|
6499
6756
|
stateCurationBlocked("State curation response did not contain any <exp> expressions.", [rawText.slice(0, 200)]);
|
|
@@ -6502,16 +6759,19 @@ function extractStateCurationExpressions(rawText) {
|
|
|
6502
6759
|
}
|
|
6503
6760
|
export function parseStateCurationExpressions(rawText) {
|
|
6504
6761
|
const decisions = [];
|
|
6762
|
+
const defaultSelections = [];
|
|
6505
6763
|
const expressions = extractStateCurationExpressions(rawText);
|
|
6506
6764
|
let noop = false;
|
|
6507
6765
|
for (const expression of expressions) {
|
|
6508
6766
|
const decision = parseStateCurationExpression(expression);
|
|
6509
6767
|
if (decision === null)
|
|
6510
6768
|
noop = true;
|
|
6769
|
+
else if (strOf(decision["decision"]) === "default")
|
|
6770
|
+
defaultSelections.push(decision);
|
|
6511
6771
|
else
|
|
6512
6772
|
decisions.push(decision);
|
|
6513
6773
|
}
|
|
6514
|
-
if (noop && decisions.length > 0) {
|
|
6774
|
+
if (noop && (decisions.length > 0 || defaultSelections.length > 0)) {
|
|
6515
6775
|
stateCurationBlocked("NOOP cannot be combined with actionable state curation expressions.", [rawText.slice(0, 200)]);
|
|
6516
6776
|
}
|
|
6517
6777
|
return {
|
|
@@ -6520,6 +6780,7 @@ export function parseStateCurationExpressions(rawText) {
|
|
|
6520
6780
|
expression_text: rawText,
|
|
6521
6781
|
expressions,
|
|
6522
6782
|
decisions,
|
|
6783
|
+
default_selections: defaultSelections,
|
|
6523
6784
|
};
|
|
6524
6785
|
}
|
|
6525
6786
|
function renderStateCurationDecisionExpression(decision) {
|
|
@@ -6528,23 +6789,35 @@ function renderStateCurationDecisionExpression(decision) {
|
|
|
6528
6789
|
const stateId = strOf(decision["state_id"]);
|
|
6529
6790
|
const reason = strOf(decision["reason"]).trim() || "state curation decision";
|
|
6530
6791
|
const action = strOf(decision["decision"]);
|
|
6531
|
-
if (action === "
|
|
6532
|
-
return `
|
|
6792
|
+
if (action === "default")
|
|
6793
|
+
return `DEFAULT_STATE ${kind} ${assetId} ${stateId} # ${reason}`;
|
|
6533
6794
|
if (action === "merge")
|
|
6534
6795
|
return `MERGE_STATE ${kind} ${assetId} ${stateId} -> ${strOf(decision["target_state_id"])} # ${reason}`;
|
|
6535
6796
|
if (action === "drop")
|
|
6536
6797
|
return `DROP_STATE ${kind} ${assetId} ${stateId} # ${reason}`;
|
|
6537
6798
|
if (action === "action_desc")
|
|
6538
6799
|
return `ACTION_STATE ${kind} ${assetId} ${stateId} # ${reason}`;
|
|
6539
|
-
if (action === "
|
|
6540
|
-
|
|
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
|
+
}
|
|
6541
6812
|
stateCurationBlocked("Cannot render unknown state curation decision.", [`decision: ${action || "<empty>"}`]);
|
|
6542
6813
|
}
|
|
6543
6814
|
export function formatStateCurationDecisionExpressions(plan) {
|
|
6544
6815
|
const decisions = asList(plan["decisions"]).filter(isDict);
|
|
6545
|
-
|
|
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)
|
|
6546
6819
|
return "<exp>NOOP # no state curation changes</exp>";
|
|
6547
|
-
return `${
|
|
6820
|
+
return `${expressions.join("\n")}\n`;
|
|
6548
6821
|
}
|
|
6549
6822
|
function stateCurationAllSourceKeys(script) {
|
|
6550
6823
|
const keys = [];
|
|
@@ -6586,6 +6859,28 @@ export function missingStateCurationRequiredSourceKeys(script, plan, requiredKey
|
|
|
6586
6859
|
const decisions = stateCurationDecisionMap(asList(plan["decisions"]).filter(isDict));
|
|
6587
6860
|
return keys.filter((key) => !decisions.has(key));
|
|
6588
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
|
+
}
|
|
6589
6884
|
function assertStateExistsForCuration(script, kind, assetId, stateId) {
|
|
6590
6885
|
const asset = assertAssetExists(script, kind, assetId);
|
|
6591
6886
|
for (const state of asList(asset["states"])) {
|
|
@@ -6602,11 +6897,25 @@ function validateStateCurationDecision(script, decision) {
|
|
|
6602
6897
|
const stateId = strOf(decision["state_id"]);
|
|
6603
6898
|
const action = strOf(decision["decision"]);
|
|
6604
6899
|
const reason = strOf(decision["reason"]);
|
|
6900
|
+
const rewritesDescription = Object.prototype.hasOwnProperty.call(decision, "new_description");
|
|
6605
6901
|
if (!assetId || !stateId)
|
|
6606
6902
|
stateCurationBlocked("State curation decision is missing asset_id/state_id.", [`${kind}:${assetId}/${stateId}`]);
|
|
6607
6903
|
if (!reason)
|
|
6608
6904
|
stateCurationBlocked("State curation decision is missing reason.", [`${kind}:${assetId}/${stateId}`]);
|
|
6609
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
|
+
}
|
|
6610
6919
|
if (action === "merge") {
|
|
6611
6920
|
const targetStateId = strOf(decision["target_state_id"]);
|
|
6612
6921
|
if (!targetStateId || targetStateId === stateId) {
|
|
@@ -6657,16 +6966,25 @@ function normalizeStateCurationMergeTargets(decisions) {
|
|
|
6657
6966
|
export function assertStateCurationPlan(script, plan, requiredKeys) {
|
|
6658
6967
|
const rawDecisions = asList(plan["decisions"]).filter(isDict);
|
|
6659
6968
|
const decisions = [...stateCurationDecisionMap(rawDecisions).values()];
|
|
6969
|
+
const defaultSelections = asList(plan["default_selections"]).filter(isDict);
|
|
6660
6970
|
for (const decision of decisions)
|
|
6661
6971
|
validateStateCurationDecision(script, decision);
|
|
6972
|
+
for (const selection of defaultSelections)
|
|
6973
|
+
validateStateCurationDecision(script, selection);
|
|
6662
6974
|
normalizeStateCurationMergeTargets(decisions);
|
|
6663
6975
|
const missing = missingStateCurationRequiredSourceKeys(script, { decisions }, requiredKeys);
|
|
6664
6976
|
if (missing.length > 0) {
|
|
6665
6977
|
stateCurationBlocked("State curation plan is missing required state decisions.", missing.slice(0, 80));
|
|
6666
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
|
+
}
|
|
6667
6984
|
return {
|
|
6668
6985
|
...plan,
|
|
6669
6986
|
decisions,
|
|
6987
|
+
default_selections: [...defaultMap.values()],
|
|
6670
6988
|
};
|
|
6671
6989
|
}
|
|
6672
6990
|
function mapStateForCuration(kind, assetId, stateId, replacements, removed) {
|
|
@@ -6753,9 +7071,115 @@ function rewriteRefsAfterStateCuration(script, replacements, removed) {
|
|
|
6753
7071
|
state_changes_rewritten: stateChangesRewritten,
|
|
6754
7072
|
};
|
|
6755
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
|
+
}
|
|
6756
7179
|
export function applyStateCurationPlan(script, rawPlan) {
|
|
6757
7180
|
const plan = assertStateCurationPlan(script, rawPlan);
|
|
6758
7181
|
const decisions = asList(plan["decisions"]).filter(isDict);
|
|
7182
|
+
const defaultSelections = asList(plan["default_selections"]).filter(isDict);
|
|
6759
7183
|
const beforeByKind = { actor: 0, location: 0, prop: 0 };
|
|
6760
7184
|
const afterByKind = { actor: 0, location: 0, prop: 0 };
|
|
6761
7185
|
for (const kind of ["actor", "location", "prop"]) {
|
|
@@ -6770,6 +7194,7 @@ export function applyStateCurationPlan(script, rawPlan) {
|
|
|
6770
7194
|
drop: 0,
|
|
6771
7195
|
action_desc: 0,
|
|
6772
7196
|
rename: 0,
|
|
7197
|
+
description_rewrite: 0,
|
|
6773
7198
|
};
|
|
6774
7199
|
for (const decision of decisions) {
|
|
6775
7200
|
const kind = strOf(decision["kind"]);
|
|
@@ -6777,13 +7202,29 @@ export function applyStateCurationPlan(script, rawPlan) {
|
|
|
6777
7202
|
const stateId = strOf(decision["state_id"]);
|
|
6778
7203
|
const key = stateCurationSourceKey(kind, assetId, stateId);
|
|
6779
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;
|
|
6780
7212
|
decisionSummary[action] = Number(decisionSummary[action] ?? 0) + 1;
|
|
6781
|
-
if (action === "rename") {
|
|
6782
|
-
const state =
|
|
6783
|
-
|
|
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
|
+
}
|
|
6784
7223
|
}
|
|
6785
7224
|
else if (action === "merge") {
|
|
6786
|
-
|
|
7225
|
+
const targetStateId = strOf(decision["target_state_id"]);
|
|
7226
|
+
appendStateCurationMergedFrom(script, kind, assetId, stateId, targetStateId, strOf(decision["reason"]));
|
|
7227
|
+
replacements.set(key, targetStateId);
|
|
6787
7228
|
removed.add(key);
|
|
6788
7229
|
}
|
|
6789
7230
|
else if (action === "drop" || action === "action_desc") {
|
|
@@ -6802,18 +7243,23 @@ export function applyStateCurationPlan(script, rawPlan) {
|
|
|
6802
7243
|
const key = stateCurationSourceKey(kind, assetId, strOf(state["state_id"]));
|
|
6803
7244
|
return !removed.has(key);
|
|
6804
7245
|
});
|
|
6805
|
-
afterByKind[kind] += asList(asset["states"]).filter(isDict).length;
|
|
6806
7246
|
}
|
|
6807
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
|
+
}
|
|
6808
7253
|
const statesBefore = beforeByKind.actor + beforeByKind.location + beforeByKind.prop;
|
|
6809
7254
|
const statesAfter = afterByKind.actor + afterByKind.location + afterByKind.prop;
|
|
7255
|
+
const synthesizedDefaults = Number(defaultSummary["default_states_synthesized"] ?? 0);
|
|
6810
7256
|
return {
|
|
6811
7257
|
version: 1,
|
|
6812
7258
|
format: "state-curation-exp-v1",
|
|
6813
7259
|
summary: {
|
|
6814
7260
|
states_before: statesBefore,
|
|
6815
7261
|
states_after: statesAfter,
|
|
6816
|
-
states_removed: statesBefore - statesAfter,
|
|
7262
|
+
states_removed: statesBefore - statesAfter + synthesizedDefaults,
|
|
6817
7263
|
actor_states_before: beforeByKind.actor,
|
|
6818
7264
|
actor_states_after: afterByKind.actor,
|
|
6819
7265
|
location_states_before: beforeByKind.location,
|
|
@@ -6822,8 +7268,13 @@ export function applyStateCurationPlan(script, rawPlan) {
|
|
|
6822
7268
|
prop_states_after: afterByKind.prop,
|
|
6823
7269
|
decisions: decisionSummary,
|
|
6824
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"],
|
|
6825
7274
|
},
|
|
6826
7275
|
decisions,
|
|
7276
|
+
default_selections: defaultSelections,
|
|
7277
|
+
selected_default_states: defaultSummary["selected_default_states"],
|
|
6827
7278
|
};
|
|
6828
7279
|
}
|
|
6829
7280
|
// ---------------------------------------------------------------------------
|
|
@@ -6879,29 +7330,35 @@ function stateNameById(asset, stateId) {
|
|
|
6879
7330
|
}
|
|
6880
7331
|
function bindableDefaultStateId(asset) {
|
|
6881
7332
|
const states = asList(asset["states"]);
|
|
6882
|
-
|
|
6883
|
-
return "";
|
|
6884
|
-
const defaultState = states.find((state) => strOf(state["state_name"]).trim() === "默认");
|
|
7333
|
+
const defaultState = states.find((state) => strOf(state["state_id"]).trim() === "default");
|
|
6885
7334
|
if (defaultState)
|
|
6886
7335
|
return strOf(defaultState["state_id"]);
|
|
6887
|
-
if (states.length === 1)
|
|
6888
|
-
return strOf(states[0]["state_id"]);
|
|
6889
7336
|
return "";
|
|
6890
7337
|
}
|
|
6891
7338
|
export function ensureDefaultStates(script) {
|
|
6892
|
-
let
|
|
7339
|
+
let synthesized = 0;
|
|
7340
|
+
let missing = 0;
|
|
6893
7341
|
for (const kind of ["actor", "location", "prop"]) {
|
|
6894
7342
|
for (const asset of asList(script[assetListKey(kind)])) {
|
|
6895
|
-
|
|
6896
|
-
if (states.length <= 1)
|
|
6897
|
-
continue;
|
|
6898
|
-
if (states.some((state) => strOf(state["state_name"]).trim() === "默认"))
|
|
7343
|
+
if (!isDict(asset))
|
|
6899
7344
|
continue;
|
|
6900
|
-
|
|
6901
|
-
|
|
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
|
+
}
|
|
6902
7359
|
}
|
|
6903
7360
|
}
|
|
6904
|
-
return {
|
|
7361
|
+
return { default_states_synthesized: synthesized, assets_missing_default_state: missing };
|
|
6905
7362
|
}
|
|
6906
7363
|
export function bindDefaultStateRefsInScript(script) {
|
|
6907
7364
|
const assetMaps = {
|
|
@@ -6936,7 +7393,7 @@ export function bindDefaultStateRefsInScript(script) {
|
|
|
6936
7393
|
setSceneContext(scene, ctx);
|
|
6937
7394
|
}
|
|
6938
7395
|
}
|
|
6939
|
-
return {
|
|
7396
|
+
return { default_refs_bound: bound, unresolved_stateful_refs: unresolved };
|
|
6940
7397
|
}
|
|
6941
7398
|
function compactStateBindingAsset(kind, asset) {
|
|
6942
7399
|
return {
|
|
@@ -7036,7 +7493,8 @@ export function buildStateBindingContexts(script, chunkSize = 10) {
|
|
|
7036
7493
|
title: script["title"],
|
|
7037
7494
|
policy: [
|
|
7038
7495
|
"Bind each scene asset ref to the best existing state_id when the actions/context show a durable visual state.",
|
|
7039
|
-
"
|
|
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.",
|
|
7040
7498
|
"Emit state_changes only for explicit transformation, outfit, damage, breakage, repair, or form-change actions.",
|
|
7041
7499
|
"Do not invent new states or assets.",
|
|
7042
7500
|
],
|
|
@@ -7255,18 +7713,27 @@ function splitLocationStatePhrase(rawName) {
|
|
|
7255
7713
|
return null;
|
|
7256
7714
|
return { name: locationName, state };
|
|
7257
7715
|
}
|
|
7258
|
-
function addStateDefinition(stateDefinitions, seen, kind, assetName, stateName, description = null) {
|
|
7716
|
+
function addStateDefinition(stateDefinitions, seen, kind, assetName, stateName, description = null, stateReason = "") {
|
|
7259
7717
|
const state = strOf(stateName).trim();
|
|
7260
7718
|
if (!assetName || !state || stateRejectionReason(kind, state))
|
|
7261
7719
|
return;
|
|
7262
7720
|
const key = `${kind}::${assetName}::${state}`;
|
|
7263
|
-
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
|
+
}
|
|
7264
7729
|
return;
|
|
7730
|
+
}
|
|
7265
7731
|
seen.add(key);
|
|
7266
7732
|
const item = { kind, asset_name: assetName, state_name: state };
|
|
7267
7733
|
const desc = strOf(description).trim();
|
|
7268
7734
|
if (desc)
|
|
7269
7735
|
item["description"] = desc;
|
|
7736
|
+
setStateReason(item, stateReason);
|
|
7270
7737
|
stateDefinitions.push(item);
|
|
7271
7738
|
}
|
|
7272
7739
|
function rawStringMap(value) {
|
|
@@ -7280,31 +7747,78 @@ function rawStringMap(value) {
|
|
|
7280
7747
|
}
|
|
7281
7748
|
return out;
|
|
7282
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
|
+
}
|
|
7283
7777
|
function normalizeSpeakerNameInAction(action, actorRegistry, locationRegistry, propRegistry, report) {
|
|
7284
7778
|
const item = { ...action };
|
|
7285
7779
|
const normalizeByKind = (speaker, speakerKind) => {
|
|
7780
|
+
const actorVoice = actorVoiceDisplayName(speaker, actorRegistry, report);
|
|
7781
|
+
if (actorVoice)
|
|
7782
|
+
return { speaker: actorVoice, speaker_kind: "actor" };
|
|
7286
7783
|
const kind = strOf(speakerKind || "actor").trim() || "actor";
|
|
7287
7784
|
if (kind === "actor")
|
|
7288
|
-
return canonicalAssetDisplay(actorRegistry, speaker, report);
|
|
7785
|
+
return { speaker: canonicalAssetDisplay(actorRegistry, speaker, report), speaker_kind: "actor" };
|
|
7289
7786
|
if (kind === "location")
|
|
7290
|
-
return canonicalAssetDisplay(locationRegistry, speaker, report);
|
|
7787
|
+
return { speaker: canonicalAssetDisplay(locationRegistry, speaker, report), speaker_kind: "location" };
|
|
7291
7788
|
if (kind === "prop")
|
|
7292
|
-
return canonicalAssetDisplay(propRegistry, speaker, report);
|
|
7293
|
-
return cleanName(speaker);
|
|
7789
|
+
return { speaker: canonicalAssetDisplay(propRegistry, speaker, report), speaker_kind: "prop" };
|
|
7790
|
+
return { speaker: cleanName(speaker), speaker_kind: kind };
|
|
7294
7791
|
};
|
|
7295
|
-
if (item["speaker"])
|
|
7296
|
-
|
|
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
|
+
}
|
|
7297
7800
|
if (isList(item["speakers"])) {
|
|
7298
|
-
item["speakers"] = asList(item["speakers"]).map((speaker) =>
|
|
7299
|
-
|
|
7300
|
-
speaker:
|
|
7301
|
-
})
|
|
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
|
+
});
|
|
7302
7805
|
}
|
|
7303
7806
|
if (isList(item["lines"])) {
|
|
7304
|
-
item["lines"] = asList(item["lines"]).map((line) =>
|
|
7305
|
-
|
|
7306
|
-
speaker:
|
|
7307
|
-
})
|
|
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
|
+
}
|
|
7308
7822
|
}
|
|
7309
7823
|
return item;
|
|
7310
7824
|
}
|
|
@@ -7340,10 +7854,13 @@ function normalizeEpisodeResultsForAssets(results, opts) {
|
|
|
7340
7854
|
for (const asset of asList(result["actors"])) {
|
|
7341
7855
|
if (!isDict(asset))
|
|
7342
7856
|
continue;
|
|
7343
|
-
const rawName =
|
|
7857
|
+
const [rawName, stateFromName] = splitEntityState(asset["name"]);
|
|
7344
7858
|
const name = canonicalAssetDisplay(actorRegistry, rawName, report);
|
|
7345
7859
|
if (name)
|
|
7346
7860
|
normalized["actors"].push({ ...asset, name });
|
|
7861
|
+
if (name && stateFromName) {
|
|
7862
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "actor", name, stateFromName, asset["description"]);
|
|
7863
|
+
}
|
|
7347
7864
|
}
|
|
7348
7865
|
for (const asset of asList(result["locations"])) {
|
|
7349
7866
|
if (!isDict(asset))
|
|
@@ -7375,7 +7892,11 @@ function normalizeEpisodeResultsForAssets(results, opts) {
|
|
|
7375
7892
|
if (!isDict(speaker))
|
|
7376
7893
|
continue;
|
|
7377
7894
|
const sourceKind = strOf(speaker["source_kind"] || "other").trim();
|
|
7378
|
-
|
|
7895
|
+
const actorVoice = actorVoiceDisplayName(speaker["name"], actorRegistry, report);
|
|
7896
|
+
if (actorVoice) {
|
|
7897
|
+
normalized["actors"].push({ name: actorVoice });
|
|
7898
|
+
}
|
|
7899
|
+
else if (sourceKind === "prop") {
|
|
7379
7900
|
normalized["speakers"].push({
|
|
7380
7901
|
...speaker,
|
|
7381
7902
|
name: canonicalAssetDisplay(propRegistry, speaker["name"], report),
|
|
@@ -7388,9 +7909,10 @@ function normalizeEpisodeResultsForAssets(results, opts) {
|
|
|
7388
7909
|
});
|
|
7389
7910
|
}
|
|
7390
7911
|
else if (sourceKind === "actor") {
|
|
7912
|
+
const [speakerName] = splitEntityState(speaker["name"]);
|
|
7391
7913
|
normalized["speakers"].push({
|
|
7392
7914
|
...speaker,
|
|
7393
|
-
name: canonicalAssetDisplay(actorRegistry,
|
|
7915
|
+
name: canonicalAssetDisplay(actorRegistry, speakerName, report),
|
|
7394
7916
|
});
|
|
7395
7917
|
}
|
|
7396
7918
|
else {
|
|
@@ -7404,14 +7926,18 @@ function normalizeEpisodeResultsForAssets(results, opts) {
|
|
|
7404
7926
|
if (kind !== "actor" && kind !== "location" && kind !== "prop")
|
|
7405
7927
|
continue;
|
|
7406
7928
|
if (kind === "actor") {
|
|
7407
|
-
const
|
|
7408
|
-
|
|
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"]);
|
|
7409
7935
|
}
|
|
7410
7936
|
else if (kind === "location") {
|
|
7411
7937
|
const split = splitLocationStatePhrase(stateDef["asset_name"]);
|
|
7412
7938
|
const assetName = canonicalAssetDisplay(locationRegistry, split ? split.name : stateDef["asset_name"], report);
|
|
7413
7939
|
if (split) {
|
|
7414
|
-
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "location", assetName, split.state, stateDef["description"]);
|
|
7940
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "location", assetName, split.state, stateDef["description"], stateDef["state_reason"]);
|
|
7415
7941
|
addNormalizationDecision(report, {
|
|
7416
7942
|
action: "location_state_from_name",
|
|
7417
7943
|
kind: "location",
|
|
@@ -7421,11 +7947,11 @@ function normalizeEpisodeResultsForAssets(results, opts) {
|
|
|
7421
7947
|
reason: "地点状态定义的资产名含时间状态,拆成稳定地点 + 状态。",
|
|
7422
7948
|
});
|
|
7423
7949
|
}
|
|
7424
|
-
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "location", assetName, stateDef["state_name"], stateDef["description"]);
|
|
7950
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "location", assetName, stateDef["state_name"], stateDef["description"], stateDef["state_reason"]);
|
|
7425
7951
|
}
|
|
7426
7952
|
else {
|
|
7427
7953
|
const assetName = canonicalAssetDisplay(propRegistry, stateDef["asset_name"], report);
|
|
7428
|
-
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "prop", assetName, stateDef["state_name"], stateDef["description"]);
|
|
7954
|
+
addStateDefinition(globalStateDefinitions, seenStateDefinitions, "prop", assetName, stateDef["state_name"], stateDef["description"], stateDef["state_reason"]);
|
|
7429
7955
|
}
|
|
7430
7956
|
}
|
|
7431
7957
|
const scenes = [];
|
|
@@ -7469,12 +7995,12 @@ function normalizeEpisodeResultsForAssets(results, opts) {
|
|
|
7469
7995
|
const propNames = [];
|
|
7470
7996
|
const normalizedPropStates = {};
|
|
7471
7997
|
for (const rawActor of asList(scene["actor_names"])) {
|
|
7472
|
-
const cleanActor =
|
|
7998
|
+
const [cleanActor, stateFromName] = splitEntityState(rawActor);
|
|
7473
7999
|
const actorName = canonicalAssetDisplay(actorRegistry, cleanActor, report);
|
|
7474
8000
|
if (!actorName)
|
|
7475
8001
|
continue;
|
|
7476
8002
|
uniqueAdd(actorNames, actorName);
|
|
7477
|
-
const state = actorStates[cleanActor] || actorStates[actorName] || "";
|
|
8003
|
+
const state = actorStates[cleanActor] || actorStates[actorName] || stateFromName || "";
|
|
7478
8004
|
if (!state)
|
|
7479
8005
|
continue;
|
|
7480
8006
|
normalizedActorStates[actorName] = state;
|
|
@@ -7689,6 +8215,12 @@ function mergeEpisodeResultsInternal(results, title, opts = {}) {
|
|
|
7689
8215
|
for (const name of speakerOrder) {
|
|
7690
8216
|
const record = speakerRecords.get(name) ?? { source_kind: "other" };
|
|
7691
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
|
+
}
|
|
7692
8224
|
let sourceId = null;
|
|
7693
8225
|
if (sourceKind === "prop")
|
|
7694
8226
|
sourceId = propIds.get(name) ?? null;
|
|
@@ -7719,27 +8251,58 @@ function mergeEpisodeResultsInternal(results, title, opts = {}) {
|
|
|
7719
8251
|
if (!names.includes(stateName))
|
|
7720
8252
|
names.push(stateName);
|
|
7721
8253
|
};
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
8254
|
+
// The asset's canonical default state name — the one named 默认, else the
|
|
8255
|
+
// first. Empty when the asset has no named states.
|
|
8256
|
+
const canonicalDefaultStateName = (defs) => {
|
|
8257
|
+
const names = defs.map((d) => strOf(d["state_name"]).trim()).filter((n) => n);
|
|
8258
|
+
if (names.length === 0)
|
|
8259
|
+
return "";
|
|
8260
|
+
return names.includes("默认") ? "默认" : names[0];
|
|
8261
|
+
};
|
|
8262
|
+
// Assemble an asset's states[] with sequential st_NNN ids, recording each
|
|
8263
|
+
// name→id in stateIdsByTarget so context / state_change refs resolve through
|
|
8264
|
+
// it. In the deterministic terminal-assembly path (bindDefaultStateRefs), the
|
|
8265
|
+
// canonical default state takes the asset-scoped id `default` (its readable
|
|
8266
|
+
// state_name preserved) so the assembled script satisfies the
|
|
8267
|
+
// DEFAULT_STATE_MISSING invariant without a curation pass. Ref resolution is
|
|
8268
|
+
// unchanged: it still keys off the state name, now mapped to the canonical id.
|
|
8269
|
+
const buildAssetStates = (kind, assetName, defs) => {
|
|
8270
|
+
const defaultName = opts.bindDefaultStateRefs ? canonicalDefaultStateName(defs) : "";
|
|
8271
|
+
let defaultAssigned = false;
|
|
7730
8272
|
const states = [];
|
|
7731
|
-
for (const stateDef of
|
|
7732
|
-
stateCounter += 1;
|
|
7733
|
-
const sid = fmtId("st", stateCounter);
|
|
8273
|
+
for (const stateDef of defs) {
|
|
7734
8274
|
const stateName = strOf(stateDef["state_name"]).trim();
|
|
7735
|
-
|
|
7736
|
-
|
|
8275
|
+
let sid;
|
|
8276
|
+
if (!defaultAssigned && defaultName && stateName === defaultName) {
|
|
8277
|
+
sid = "default";
|
|
8278
|
+
defaultAssigned = true;
|
|
8279
|
+
}
|
|
8280
|
+
else {
|
|
8281
|
+
stateCounter += 1;
|
|
8282
|
+
sid = fmtId("st", stateCounter);
|
|
8283
|
+
}
|
|
8284
|
+
stateIdsByTarget.set(`${kind}::${assetName}::${stateName}`, sid);
|
|
8285
|
+
rememberStateName(kind, assetName, stateName);
|
|
7737
8286
|
const stateEntry = { state_id: sid, state_name: stateName };
|
|
7738
8287
|
const description = strOf(stateDef["description"]).trim();
|
|
7739
8288
|
if (description)
|
|
7740
8289
|
stateEntry["description"] = description;
|
|
8290
|
+
const stateReason = cleanStateReason(stateDef["state_reason"]);
|
|
8291
|
+
if (stateReason)
|
|
8292
|
+
stateEntry["state_reason"] = stateReason;
|
|
7741
8293
|
states.push(stateEntry);
|
|
7742
8294
|
}
|
|
8295
|
+
return states;
|
|
8296
|
+
};
|
|
8297
|
+
const actors = [];
|
|
8298
|
+
for (const name of actorNames) {
|
|
8299
|
+
const entry = { actor_id: actorIds.get(name), actor_name: name };
|
|
8300
|
+
if (actorDescriptions.has(name))
|
|
8301
|
+
entry["description"] = actorDescriptions.get(name);
|
|
8302
|
+
const aliases = actorAliases.get(name) ?? [];
|
|
8303
|
+
if (aliases.length > 0)
|
|
8304
|
+
entry["aliases"] = aliases;
|
|
8305
|
+
const states = buildAssetStates("actor", name, stateDefsByTarget.get(`actor::${name}`) ?? []);
|
|
7743
8306
|
if (states.length > 0)
|
|
7744
8307
|
entry["states"] = states;
|
|
7745
8308
|
actors.push(entry);
|
|
@@ -7752,19 +8315,7 @@ function mergeEpisodeResultsInternal(results, title, opts = {}) {
|
|
|
7752
8315
|
const aliases = locationAliases.get(name) ?? [];
|
|
7753
8316
|
if (aliases.length > 0)
|
|
7754
8317
|
entry["aliases"] = aliases;
|
|
7755
|
-
const states = [];
|
|
7756
|
-
for (const stateDef of stateDefsByTarget.get(`location::${name}`) ?? []) {
|
|
7757
|
-
stateCounter += 1;
|
|
7758
|
-
const sid = fmtId("st", stateCounter);
|
|
7759
|
-
const stateName = strOf(stateDef["state_name"]).trim();
|
|
7760
|
-
stateIdsByTarget.set(`location::${name}::${stateName}`, sid);
|
|
7761
|
-
rememberStateName("location", name, stateName);
|
|
7762
|
-
const stateEntry = { state_id: sid, state_name: stateName };
|
|
7763
|
-
const description = strOf(stateDef["description"]).trim();
|
|
7764
|
-
if (description)
|
|
7765
|
-
stateEntry["description"] = description;
|
|
7766
|
-
states.push(stateEntry);
|
|
7767
|
-
}
|
|
8318
|
+
const states = buildAssetStates("location", name, stateDefsByTarget.get(`location::${name}`) ?? []);
|
|
7768
8319
|
if (states.length > 0)
|
|
7769
8320
|
entry["states"] = states;
|
|
7770
8321
|
locations.push(entry);
|
|
@@ -7777,19 +8328,7 @@ function mergeEpisodeResultsInternal(results, title, opts = {}) {
|
|
|
7777
8328
|
const aliases = propAliases.get(name) ?? [];
|
|
7778
8329
|
if (aliases.length > 0)
|
|
7779
8330
|
entry["aliases"] = aliases;
|
|
7780
|
-
const states = [];
|
|
7781
|
-
for (const stateDef of stateDefsByTarget.get(`prop::${name}`) ?? []) {
|
|
7782
|
-
stateCounter += 1;
|
|
7783
|
-
const sid = fmtId("st", stateCounter);
|
|
7784
|
-
const stateName = strOf(stateDef["state_name"]).trim();
|
|
7785
|
-
stateIdsByTarget.set(`prop::${name}::${stateName}`, sid);
|
|
7786
|
-
rememberStateName("prop", name, stateName);
|
|
7787
|
-
const stateEntry = { state_id: sid, state_name: stateName };
|
|
7788
|
-
const description = strOf(stateDef["description"]).trim();
|
|
7789
|
-
if (description)
|
|
7790
|
-
stateEntry["description"] = description;
|
|
7791
|
-
states.push(stateEntry);
|
|
7792
|
-
}
|
|
8331
|
+
const states = buildAssetStates("prop", name, stateDefsByTarget.get(`prop::${name}`) ?? []);
|
|
7793
8332
|
if (states.length > 0)
|
|
7794
8333
|
entry["states"] = states;
|
|
7795
8334
|
props.push(entry);
|
|
@@ -7802,6 +8341,8 @@ function mergeEpisodeResultsInternal(results, title, opts = {}) {
|
|
|
7802
8341
|
if (speakerKind === "actor" && actorIds.has(speaker))
|
|
7803
8342
|
return `spk_${actorIds.get(speaker)}`;
|
|
7804
8343
|
const normKind = _MD_SPEAKER_KIND_NORM[speakerKind.toLowerCase()] ?? _MD_SPEAKER_KIND_NORM[speakerKind] ?? "other";
|
|
8344
|
+
if ((normKind === "system" || normKind === "broadcast" || normKind === "other") && actorIds.has(speaker))
|
|
8345
|
+
return `spk_${actorIds.get(speaker)}`;
|
|
7805
8346
|
return speakerIdsByName.get(`${normKind}::${speaker}`) ?? speakerIdsByName.get(`other::${speaker}`) ?? null;
|
|
7806
8347
|
};
|
|
7807
8348
|
const stateIdForRef = (kind, assetName, explicitState, unresolved) => {
|