@lingjingai/scriptctl 0.60.13 → 0.61.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changes/0.61.0.md +19 -0
- package/changes/0.61.1.md +15 -0
- package/dist/bin.js +16127 -11
- package/dist/bin.js.map +7 -1
- package/dist/cli/{register-batch.d.ts → register-review.d.ts} +1 -1
- package/dist/cli/register-review.js +19 -0
- package/dist/cli/register-review.js.map +1 -0
- package/dist/cli/register-script.d.ts +3 -0
- package/dist/cli/register-script.js +256 -0
- package/dist/cli/register-script.js.map +1 -0
- package/dist/cli/register-workflows.js +37 -42
- package/dist/cli/register-workflows.js.map +1 -1
- package/dist/cli.js +6 -16
- package/dist/cli.js.map +1 -1
- package/dist/domain/ingest/environment.d.ts +2 -3
- package/dist/domain/ingest/environment.js +1 -4
- package/dist/domain/ingest/environment.js.map +1 -1
- package/dist/domain/script/names.d.ts +1 -1
- package/dist/domain/script/names.js +1 -3
- package/dist/domain/script/names.js.map +1 -1
- package/dist/domain/script/shared/types.d.ts +1 -18
- package/dist/domain/script/shared/types.js +4 -8
- package/dist/domain/script/shared/types.js.map +1 -1
- package/dist/domain/script/shared/validation.d.ts +1 -12
- package/dist/domain/script/shared/validation.js +2 -22
- package/dist/domain/script/shared/validation.js.map +1 -1
- package/dist/domain/script/timestamp.d.ts +1 -23
- package/dist/domain/script/timestamp.js +2 -126
- package/dist/domain/script/timestamp.js.map +1 -1
- package/dist/domain/script/v3/validation.d.ts +2 -3
- package/dist/domain/script/v3/validation.js +1 -232
- package/dist/domain/script/v3/validation.js.map +1 -1
- package/dist/domain/script/v4/ids.d.ts +2 -24
- package/dist/domain/script/v4/ids.js +19 -40
- package/dist/domain/script/v4/ids.js.map +1 -1
- package/dist/domain/script/v4/validation.d.ts +2 -2
- package/dist/domain/script/v4/validation.js +1 -410
- package/dist/domain/script/v4/validation.js.map +1 -1
- package/dist/llm/config.js +4 -0
- package/dist/llm/config.js.map +1 -1
- package/dist/output.js +5 -0
- package/dist/output.js.map +1 -1
- package/dist/package-root.d.ts +2 -0
- package/dist/package-root.js +38 -0
- package/dist/package-root.js.map +1 -0
- package/dist/review-agent.js +2956 -0
- package/dist/review-agent.js.map +7 -0
- package/dist/usecases/changelog.js +3 -8
- package/dist/usecases/changelog.js.map +1 -1
- package/dist/usecases/script/action-span.d.ts +3 -19
- package/dist/usecases/script/action-span.js +19 -146
- package/dist/usecases/script/action-span.js.map +1 -1
- package/dist/usecases/script/review.d.ts +3 -0
- package/dist/usecases/script/review.js +185 -0
- package/dist/usecases/script/review.js.map +1 -0
- package/dist/usecases/update.js +2 -14
- package/dist/usecases/update.js.map +1 -1
- package/package.json +11 -3
- package/skills/scriptctl/SKILL.md +157 -214
- package/skills/scriptctl/references/ingest-workflow.md +1 -1
- package/skills/scriptctl/references/state-reference-repair.md +23 -19
- package/skills/scriptctl/references/video-transcript-repair.md +27 -29
- package/dist/cli/register-batch.js +0 -28
- package/dist/cli/register-batch.js.map +0 -1
- package/dist/cli/register-edits.d.ts +0 -4
- package/dist/cli/register-edits.js +0 -234
- package/dist/cli/register-edits.js.map +0 -1
- package/dist/cli/register-query.d.ts +0 -4
- package/dist/cli/register-query.js +0 -95
- package/dist/cli/register-query.js.map +0 -1
- package/skills/scriptctl/references/atomic-edit-workflow.md +0 -47
- package/skills/scriptctl/references/bulk-authoring.md +0 -50
|
@@ -0,0 +1,2956 @@
|
|
|
1
|
+
// packages/screenplay/dist/model.js
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
// packages/screenplay/dist/trace.js
|
|
5
|
+
var sink = null;
|
|
6
|
+
var t0 = 0;
|
|
7
|
+
function step(fn, note) {
|
|
8
|
+
if (sink)
|
|
9
|
+
sink.push({ fn, note, ms: Math.round((performance.now() - t0) * 100) / 100 });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// packages/screenplay/dist/schema/types.js
|
|
13
|
+
var ACTION_TYPES = ["action", "dialogue", "inner_thought", "transition"];
|
|
14
|
+
var ACTION_TYPE_SET = new Set(ACTION_TYPES);
|
|
15
|
+
var NON_ACTOR_SPEAKER_KINDS = ["system", "broadcast", "offscreen", "group"];
|
|
16
|
+
var NON_ACTOR_SPEAKER_KIND_SET = new Set(NON_ACTOR_SPEAKER_KINDS);
|
|
17
|
+
var IMPORTANCE_TIERS = ["featured", "background"];
|
|
18
|
+
var IMPORTANCE_TIER_SET = new Set(IMPORTANCE_TIERS);
|
|
19
|
+
var ROLE_TYPES = ["\u4E3B\u89D2", "\u914D\u89D2"];
|
|
20
|
+
var ROLE_TYPE_SET = new Set(ROLE_TYPES);
|
|
21
|
+
var SCENE_TIME_VALUES = ["day", "night", "dawn", "dusk"];
|
|
22
|
+
var SCENE_TIME_SET = new Set(SCENE_TIME_VALUES);
|
|
23
|
+
function isSceneTime(value) {
|
|
24
|
+
return typeof value === "string" && SCENE_TIME_SET.has(value);
|
|
25
|
+
}
|
|
26
|
+
var SCENE_SPACE_VALUES = ["interior", "exterior"];
|
|
27
|
+
var SCENE_SPACE_SET = new Set(SCENE_SPACE_VALUES);
|
|
28
|
+
|
|
29
|
+
// packages/screenplay/dist/schema/names.js
|
|
30
|
+
function canonicalNameKey(name) {
|
|
31
|
+
return name.normalize("NFKC").toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// packages/screenplay/dist/schema/issues.js
|
|
35
|
+
function issueKey(issue) {
|
|
36
|
+
return JSON.stringify([issue.code, issue.path, issue.message]);
|
|
37
|
+
}
|
|
38
|
+
function issueCounts(issues2) {
|
|
39
|
+
const counts = /* @__PURE__ */ new Map();
|
|
40
|
+
for (const issue of issues2) {
|
|
41
|
+
const key = issueKey(issue);
|
|
42
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
43
|
+
}
|
|
44
|
+
return counts;
|
|
45
|
+
}
|
|
46
|
+
function issuesIntroduced(baseline2, current) {
|
|
47
|
+
const budget = issueCounts(baseline2);
|
|
48
|
+
const fresh = [];
|
|
49
|
+
for (const issue of current) {
|
|
50
|
+
const key = issueKey(issue);
|
|
51
|
+
const left = budget.get(key) ?? 0;
|
|
52
|
+
if (left > 0)
|
|
53
|
+
budget.set(key, left - 1);
|
|
54
|
+
else
|
|
55
|
+
fresh.push(issue);
|
|
56
|
+
}
|
|
57
|
+
return fresh;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// packages/screenplay/dist/schema/ids.js
|
|
61
|
+
import { customAlphabet } from "nanoid";
|
|
62
|
+
var V4_ID_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
63
|
+
var V4_ID_RANDOM_LENGTH = 6;
|
|
64
|
+
var V4_ID_PREFIXES = {
|
|
65
|
+
episode: "ep",
|
|
66
|
+
scene: "scn",
|
|
67
|
+
action: "ac",
|
|
68
|
+
actor: "act",
|
|
69
|
+
location: "loc",
|
|
70
|
+
prop: "prp",
|
|
71
|
+
state: "st"
|
|
72
|
+
};
|
|
73
|
+
var randomSuffix = customAlphabet(V4_ID_ALPHABET, V4_ID_RANDOM_LENGTH);
|
|
74
|
+
var alphabetClass = `[${V4_ID_ALPHABET}]`;
|
|
75
|
+
var V4_ID_PATTERNS = Object.fromEntries(Object.entries(V4_ID_PREFIXES).map(([kind, prefix]) => [
|
|
76
|
+
kind,
|
|
77
|
+
new RegExp(`^${prefix}${alphabetClass}{${V4_ID_RANDOM_LENGTH}}$`)
|
|
78
|
+
]));
|
|
79
|
+
function isV4Id(kind, value) {
|
|
80
|
+
return typeof value === "string" && V4_ID_PATTERNS[kind].test(value);
|
|
81
|
+
}
|
|
82
|
+
var ID_SPACE_EXHAUSTED = "ID_SPACE_EXHAUSTED";
|
|
83
|
+
function generateV4Id(kind, existing = /* @__PURE__ */ new Set(), generateSuffix = randomSuffix) {
|
|
84
|
+
const prefix = V4_ID_PREFIXES[kind];
|
|
85
|
+
for (let attempt = 0; attempt < 128; attempt++) {
|
|
86
|
+
const candidate = `${prefix}${generateSuffix()}`;
|
|
87
|
+
if (!existing.has(candidate))
|
|
88
|
+
return candidate;
|
|
89
|
+
}
|
|
90
|
+
throw Object.assign(new Error(`Could not generate an unused ${kind} ID after 128 attempts.`), { code: ID_SPACE_EXHAUSTED });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// packages/screenplay/dist/schema/timestamp.js
|
|
94
|
+
var TimestampParseError = class extends Error {
|
|
95
|
+
input;
|
|
96
|
+
constructor(input, message) {
|
|
97
|
+
super(message);
|
|
98
|
+
this.input = input;
|
|
99
|
+
this.name = "TimestampParseError";
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var MINUTE_SECOND_RE = /^(\d+):(\d{2})(?:\.(\d{1,3}))?$/;
|
|
103
|
+
var HOUR_MINUTE_SECOND_RE = /^(\d+):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/;
|
|
104
|
+
function fractionMilliseconds(raw) {
|
|
105
|
+
return raw ? Number(raw.padEnd(3, "0")) : 0;
|
|
106
|
+
}
|
|
107
|
+
function requireSafeMilliseconds(raw, milliseconds) {
|
|
108
|
+
if (!Number.isSafeInteger(milliseconds)) {
|
|
109
|
+
throw new TimestampParseError(raw, "Timestamp exceeds the supported numeric range.");
|
|
110
|
+
}
|
|
111
|
+
return milliseconds;
|
|
112
|
+
}
|
|
113
|
+
function canonicalTimestamp(milliseconds, fraction) {
|
|
114
|
+
const totalSeconds = Math.floor(milliseconds / 1e3);
|
|
115
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
116
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
117
|
+
const seconds2 = totalSeconds % 60;
|
|
118
|
+
const fractionPart = fraction ? `.${fraction}` : "";
|
|
119
|
+
if (hours > 0) {
|
|
120
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds2).padStart(2, "0")}${fractionPart}`;
|
|
121
|
+
}
|
|
122
|
+
return `${String(minutes).padStart(2, "0")}:${String(seconds2).padStart(2, "0")}${fractionPart}`;
|
|
123
|
+
}
|
|
124
|
+
function parseTimestamp(raw) {
|
|
125
|
+
const input = raw.trim();
|
|
126
|
+
if (!input) {
|
|
127
|
+
throw new TimestampParseError(raw, "Timestamp must not be empty.");
|
|
128
|
+
}
|
|
129
|
+
const hourMatch = input.match(HOUR_MINUTE_SECOND_RE);
|
|
130
|
+
if (hourMatch) {
|
|
131
|
+
const hours = Number(hourMatch[1]);
|
|
132
|
+
const minutes2 = Number(hourMatch[2]);
|
|
133
|
+
const seconds3 = Number(hourMatch[3]);
|
|
134
|
+
const fraction2 = hourMatch[4];
|
|
135
|
+
if (minutes2 > 59) {
|
|
136
|
+
throw new TimestampParseError(raw, "Hour-form timestamps require minutes between 00 and 59.");
|
|
137
|
+
}
|
|
138
|
+
if (seconds3 > 59) {
|
|
139
|
+
throw new TimestampParseError(raw, "Timestamp seconds must be between 00 and 59.");
|
|
140
|
+
}
|
|
141
|
+
const milliseconds2 = requireSafeMilliseconds(raw, ((hours * 60 + minutes2) * 60 + seconds3) * 1e3 + fractionMilliseconds(fraction2));
|
|
142
|
+
return {
|
|
143
|
+
canonical: canonicalTimestamp(milliseconds2, fraction2),
|
|
144
|
+
milliseconds: milliseconds2,
|
|
145
|
+
fractionDigits: fraction2?.length ?? 0
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const minuteMatch = input.match(MINUTE_SECOND_RE);
|
|
149
|
+
if (!minuteMatch) {
|
|
150
|
+
throw new TimestampParseError(raw, "Use M:SS, MM:SS, or H:MM:SS with an optional 1-3 digit fraction.");
|
|
151
|
+
}
|
|
152
|
+
const minutes = Number(minuteMatch[1]);
|
|
153
|
+
const seconds2 = Number(minuteMatch[2]);
|
|
154
|
+
const fraction = minuteMatch[3];
|
|
155
|
+
if (seconds2 > 59) {
|
|
156
|
+
throw new TimestampParseError(raw, "Timestamp seconds must be between 00 and 59.");
|
|
157
|
+
}
|
|
158
|
+
const milliseconds = requireSafeMilliseconds(raw, (minutes * 60 + seconds2) * 1e3 + fractionMilliseconds(fraction));
|
|
159
|
+
return {
|
|
160
|
+
canonical: canonicalTimestamp(milliseconds, fraction),
|
|
161
|
+
milliseconds,
|
|
162
|
+
fractionDigits: fraction?.length ?? 0
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function timestampOfAction(action) {
|
|
166
|
+
if (typeof action !== "object" || action === null || Array.isArray(action))
|
|
167
|
+
return void 0;
|
|
168
|
+
const extend = action.extend;
|
|
169
|
+
if (typeof extend !== "object" || extend === null || Array.isArray(extend))
|
|
170
|
+
return void 0;
|
|
171
|
+
return extend.timestamp;
|
|
172
|
+
}
|
|
173
|
+
function validateActionTimestampOrder(actions, actionsPath, issue) {
|
|
174
|
+
let previous = null;
|
|
175
|
+
actions.forEach((action, actionIndex) => {
|
|
176
|
+
const rawTimestamp = timestampOfAction(action);
|
|
177
|
+
if (rawTimestamp === void 0)
|
|
178
|
+
return;
|
|
179
|
+
const timestampPath = `${actionsPath}[${actionIndex}].extend.timestamp`;
|
|
180
|
+
if (typeof rawTimestamp !== "string") {
|
|
181
|
+
issue("timestamp_invalid", timestampPath, "timestamp must be a string in M:SS, MM:SS, or H:MM:SS form");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
let timestamp;
|
|
185
|
+
try {
|
|
186
|
+
timestamp = parseTimestamp(rawTimestamp);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (!(error instanceof TimestampParseError))
|
|
189
|
+
throw error;
|
|
190
|
+
issue("timestamp_invalid", timestampPath, error.message);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (previous !== null && timestamp.milliseconds < previous.timestamp.milliseconds) {
|
|
194
|
+
issue("timestamp_out_of_order", timestampPath, `timestamp ${timestamp.canonical} is earlier than Action ${previous.actionIndex + 1} timestamp ${previous.timestamp.canonical}; equal timestamps are allowed`);
|
|
195
|
+
}
|
|
196
|
+
previous = { actionIndex, timestamp };
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// packages/screenplay/dist/schema/v3.js
|
|
201
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
202
|
+
var isArr = (v) => Array.isArray(v);
|
|
203
|
+
var isStr = (v) => typeof v === "string";
|
|
204
|
+
var ACTION_LEGACY = ["state_changes", "actor_id", "speakers", "lines"];
|
|
205
|
+
function validateScriptV3(doc) {
|
|
206
|
+
const errors = [];
|
|
207
|
+
const warnings = [];
|
|
208
|
+
const err = (code, path4, message, repairHint) => errors.push({ code, path: path4, message, ...repairHint ? { repair_hint: repairHint } : {} });
|
|
209
|
+
const warn = (code, path4, message) => warnings.push({ code, path: path4, message });
|
|
210
|
+
if (!isObj(doc))
|
|
211
|
+
return { ok: false, errors: [{ code: "not_object", path: "$", message: "script must be an object" }], warnings };
|
|
212
|
+
if (doc.version !== 3)
|
|
213
|
+
err("version", "$.version", "version must be 3");
|
|
214
|
+
for (const key of ["title", "worldview", "style"])
|
|
215
|
+
if (!isStr(doc[key]))
|
|
216
|
+
err("field", `$.${key}`, `${key} must be a string`);
|
|
217
|
+
const stateIds = /* @__PURE__ */ new Map();
|
|
218
|
+
const idKind = /* @__PURE__ */ new Map();
|
|
219
|
+
const actorNames = /* @__PURE__ */ new Map();
|
|
220
|
+
const collect = (list, kind, idField, nameField, path4) => {
|
|
221
|
+
if (!isArr(list)) {
|
|
222
|
+
err("field", path4, `${path4} must be an array`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
list.forEach((raw, i) => {
|
|
226
|
+
const p = `${path4}[${i}]`;
|
|
227
|
+
if (!isObj(raw)) {
|
|
228
|
+
err("asset", p, "asset must be an object");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const id = raw[idField];
|
|
232
|
+
if (!isStr(id)) {
|
|
233
|
+
err("asset_id", `${p}.${idField}`, `${idField} required`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (idKind.has(id))
|
|
237
|
+
err("dup_id", `${p}.${idField}`, `duplicate id ${id}`);
|
|
238
|
+
idKind.set(id, kind);
|
|
239
|
+
if (!isStr(raw[nameField]))
|
|
240
|
+
err("asset_name", `${p}.${nameField}`, `${nameField} required`);
|
|
241
|
+
else if (kind === "actor") {
|
|
242
|
+
const nameKey = canonicalNameKey(raw[nameField]);
|
|
243
|
+
const existingId = actorNames.get(nameKey);
|
|
244
|
+
if (existingId !== void 0) {
|
|
245
|
+
err("dup_actor_name", `${p}.${nameField}`, `actor name ${raw[nameField]} already belongs to ${existingId}`);
|
|
246
|
+
} else {
|
|
247
|
+
actorNames.set(nameKey, id);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (!isStr(raw.importance) || !IMPORTANCE_TIERS.includes(raw.importance))
|
|
251
|
+
err("importance", `${p}.importance`, `importance must be one of ${IMPORTANCE_TIERS.join("|")}`);
|
|
252
|
+
if (!isStr(raw.description))
|
|
253
|
+
err("asset_desc", `${p}.description`, "description must be a string");
|
|
254
|
+
const ids = /* @__PURE__ */ new Set();
|
|
255
|
+
const names = /* @__PURE__ */ new Set();
|
|
256
|
+
let defaults = 0;
|
|
257
|
+
if (!isArr(raw.states) || raw.states.length === 0)
|
|
258
|
+
err("states", `${p}.states`, "states must be a non-empty array");
|
|
259
|
+
else
|
|
260
|
+
raw.states.forEach((st, j) => {
|
|
261
|
+
const sp = `${p}.states[${j}]`;
|
|
262
|
+
if (!isObj(st) || !isStr(st.state_id)) {
|
|
263
|
+
err("state_id", sp, "state_id required");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (ids.has(st.state_id))
|
|
267
|
+
err("dup_state", sp, `duplicate state_id ${st.state_id}`);
|
|
268
|
+
ids.add(st.state_id);
|
|
269
|
+
if (st.state_id === "default")
|
|
270
|
+
defaults++;
|
|
271
|
+
if (!isStr(st.state_name))
|
|
272
|
+
err("state_name", `${sp}.state_name`, "state_name required");
|
|
273
|
+
else {
|
|
274
|
+
if (names.has(st.state_name))
|
|
275
|
+
err("dup_state_name", `${sp}.state_name`, `duplicate state_name ${st.state_name}`);
|
|
276
|
+
names.add(st.state_name);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
if (isArr(raw.states) && defaults !== 1)
|
|
280
|
+
err("default_state", `${p}.states`, `exactly one 'default' state required, found ${defaults}`);
|
|
281
|
+
stateIds.set(id, ids);
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
collect(doc.actors, "actor", "actor_id", "actor_name", "$.actors");
|
|
285
|
+
collect(doc.locations, "location", "location_id", "location_name", "$.locations");
|
|
286
|
+
collect(doc.props, "prop", "prop_id", "prop_name", "$.props");
|
|
287
|
+
const refState = (assetId, stateId, kind, path4) => {
|
|
288
|
+
if (idKind.get(assetId) !== kind) {
|
|
289
|
+
err("ref", path4, `${kind} ${assetId} not found`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (stateId !== void 0 && (!isStr(stateId) || !stateIds.get(assetId)?.has(stateId)))
|
|
293
|
+
err("state_ref", path4, `state ${String(stateId)} not materialized on ${assetId}`);
|
|
294
|
+
};
|
|
295
|
+
const checkEntityList = (list, kind, idField, path4) => {
|
|
296
|
+
if (list === void 0) {
|
|
297
|
+
err("field", path4, `${path4} required (may be empty array)`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!isArr(list)) {
|
|
301
|
+
err("field", path4, `${path4} must be an array`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
list.forEach((ref2, i) => {
|
|
305
|
+
if (!isObj(ref2) || !isStr(ref2[idField])) {
|
|
306
|
+
err("ref", `${path4}[${i}]`, `${idField} required`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
refState(ref2[idField], ref2.state_id, kind, `${path4}[${i}]`);
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
const checkAction = (action, path4, sceneActorIds, sceneAddress) => {
|
|
313
|
+
if (!isObj(action)) {
|
|
314
|
+
err("action", path4, "action must be an object");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
for (const bad of ACTION_LEGACY)
|
|
318
|
+
if (bad in action)
|
|
319
|
+
err("legacy_field", `${path4}.${bad}`, `v3 forbids action.${bad}`);
|
|
320
|
+
const type = action.type;
|
|
321
|
+
if (!isStr(type) || !ACTION_TYPES.includes(type))
|
|
322
|
+
err("action_type", `${path4}.type`, `invalid action type ${String(type)}`);
|
|
323
|
+
if (!isStr(action.content))
|
|
324
|
+
err("content", `${path4}.content`, "content must be a plain-text string");
|
|
325
|
+
if (type === "dialogue" || type === "inner_thought") {
|
|
326
|
+
const sp = action.speaker;
|
|
327
|
+
if (!isObj(sp))
|
|
328
|
+
err("speaker", `${path4}.speaker`, "dialogue/inner_thought needs a speaker");
|
|
329
|
+
else if ("kind" in sp) {
|
|
330
|
+
if (!isStr(sp.kind) || !NON_ACTOR_SPEAKER_KINDS.includes(sp.kind))
|
|
331
|
+
err("speaker_kind", `${path4}.speaker.kind`, "invalid non-actor speaker kind");
|
|
332
|
+
} else if (isStr(sp.actor_id)) {
|
|
333
|
+
refState(sp.actor_id, sp.state_id, "actor", `${path4}.speaker`);
|
|
334
|
+
if (!sceneActorIds.has(sp.actor_id)) {
|
|
335
|
+
const ownedStates = stateIds.get(sp.actor_id);
|
|
336
|
+
const requestedState = isStr(sp.state_id) && ownedStates?.has(sp.state_id) ? sp.state_id : ownedStates?.has("default") ? "default" : ownedStates?.values().next().value;
|
|
337
|
+
const repairHint = sceneAddress && requestedState ? `scriptctl scene-ref ${sceneAddress} actor:${sp.actor_id} --state ${requestedState} -p "$SCRIPT"` : void 0;
|
|
338
|
+
err("actor_not_in_scene", `${path4}.speaker.actor_id`, `actor ${sp.actor_id} is not referenced by this scene`, repairHint);
|
|
339
|
+
}
|
|
340
|
+
} else
|
|
341
|
+
err("speaker", `${path4}.speaker`, "speaker needs actor_id or kind");
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const episodes = doc.episodes;
|
|
345
|
+
if (!isArr(episodes))
|
|
346
|
+
err("episodes", "$.episodes", "episodes must be an array");
|
|
347
|
+
else
|
|
348
|
+
episodes.forEach((ep, ei) => {
|
|
349
|
+
const ep_p = `$.episodes[${ei}]`;
|
|
350
|
+
if (!isObj(ep) || !isStr(ep.episode_id)) {
|
|
351
|
+
err("episode_id", ep_p, "episode_id required");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!isArr(ep.scenes)) {
|
|
355
|
+
err("scenes", `${ep_p}.scenes`, "scenes must be an array");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
ep.scenes.forEach((sc, si) => {
|
|
359
|
+
const sc_p = `${ep_p}.scenes[${si}]`;
|
|
360
|
+
if (!isObj(sc)) {
|
|
361
|
+
err("scene", sc_p, "scene must be an object");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if ("context" in sc)
|
|
365
|
+
err("legacy_field", `${sc_p}.context`, "v3 forbids scene.context");
|
|
366
|
+
if (!isStr(sc.scene_id))
|
|
367
|
+
err("scene_id", sc_p, "scene_id required");
|
|
368
|
+
const environment = sc.environment;
|
|
369
|
+
if (environment === void 0 || isObj(environment) && (!isStr(environment.time) || environment.time.trim() === "")) {
|
|
370
|
+
warn("scene_time_missing", `${sc_p}.environment.time`, "scene time is required for production review");
|
|
371
|
+
} else if (!isObj(environment) || environment.space !== void 0 && !isStr(environment.space) || !isStr(environment.time)) {
|
|
372
|
+
err("environment", `${sc_p}.environment`, "environment must contain optional string space and a time string");
|
|
373
|
+
} else if (!isSceneTime(environment.time)) {
|
|
374
|
+
err("scene_time", `${sc_p}.environment.time`, "scene time must be day|night|dawn|dusk");
|
|
375
|
+
}
|
|
376
|
+
const loc = sc.location;
|
|
377
|
+
if (loc !== null) {
|
|
378
|
+
if (!isObj(loc) || !isStr(loc.location_id))
|
|
379
|
+
err("location", `${sc_p}.location`, "location must be null or {location_id}");
|
|
380
|
+
else
|
|
381
|
+
refState(loc.location_id, loc.state_id, "location", `${sc_p}.location`);
|
|
382
|
+
}
|
|
383
|
+
checkEntityList(sc.actors, "actor", "actor_id", `${sc_p}.actors`);
|
|
384
|
+
checkEntityList(sc.props, "prop", "prop_id", `${sc_p}.props`);
|
|
385
|
+
if (isArr(sc.props)) {
|
|
386
|
+
const propIds = /* @__PURE__ */ new Set();
|
|
387
|
+
sc.props.forEach((ref2, i) => {
|
|
388
|
+
if (!isObj(ref2) || !isStr(ref2.prop_id))
|
|
389
|
+
return;
|
|
390
|
+
if (propIds.has(ref2.prop_id))
|
|
391
|
+
err("dup_prop_ref", `${sc_p}.props[${i}]`, `duplicate prop ref ${ref2.prop_id}`);
|
|
392
|
+
propIds.add(ref2.prop_id);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const sceneActorIds = new Set(isArr(sc.actors) ? sc.actors.filter((ref2) => isObj(ref2)).map((ref2) => ref2.actor_id).filter((id) => isStr(id)) : []);
|
|
396
|
+
if (!isArr(sc.actions))
|
|
397
|
+
err("actions", `${sc_p}.actions`, "actions must be an array");
|
|
398
|
+
else {
|
|
399
|
+
const sceneAddress = isStr(sc.scene_id) ? `${ep.episode_id}/${sc.scene_id}` : void 0;
|
|
400
|
+
sc.actions.forEach((a, ai) => checkAction(a, `${sc_p}.actions[${ai}]`, sceneActorIds, sceneAddress));
|
|
401
|
+
validateActionTimestampOrder(sc.actions, `${sc_p}.actions`, err);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// packages/screenplay/dist/schema/v4.js
|
|
409
|
+
var isObj2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
410
|
+
var isStr2 = (value) => typeof value === "string";
|
|
411
|
+
var isArr2 = (value) => Array.isArray(value);
|
|
412
|
+
function isJsonValue(value) {
|
|
413
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
414
|
+
return true;
|
|
415
|
+
if (Array.isArray(value))
|
|
416
|
+
return value.every(isJsonValue);
|
|
417
|
+
return isObj2(value) && Object.values(value).every(isJsonValue);
|
|
418
|
+
}
|
|
419
|
+
function validateExtend(value, path4, err) {
|
|
420
|
+
if (value !== void 0 && (!isObj2(value) || !Object.values(value).every(isJsonValue))) {
|
|
421
|
+
err("extend", path4, "extend must be an object containing JSON values");
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function isSorted(values) {
|
|
425
|
+
for (let index = 1; index < values.length; index++) {
|
|
426
|
+
if (values[index - 1] > values[index])
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
function validateScriptV4(doc) {
|
|
432
|
+
const errors = [];
|
|
433
|
+
const warnings = [];
|
|
434
|
+
const err = (code, path4, message, repairHint) => errors.push({ code, path: path4, message, ...repairHint ? { repair_hint: repairHint } : {} });
|
|
435
|
+
const warn = (code, path4, message, repairHint) => warnings.push({ code, path: path4, message, ...repairHint ? { repair_hint: repairHint } : {} });
|
|
436
|
+
if (!isObj2(doc)) {
|
|
437
|
+
return {
|
|
438
|
+
ok: false,
|
|
439
|
+
errors: [{ code: "not_object", path: "$", message: "script must be an object" }],
|
|
440
|
+
warnings
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (doc.version !== 4)
|
|
444
|
+
err("version", "$.version", "version must be 4");
|
|
445
|
+
for (const key of ["title", "worldview", "style"]) {
|
|
446
|
+
if (!isStr2(doc[key]))
|
|
447
|
+
err("field", `$.${key}`, `${key} must be a string`);
|
|
448
|
+
}
|
|
449
|
+
for (const key of ["worldview_raw", "synopsis", "logline", "theme"]) {
|
|
450
|
+
if (doc[key] !== void 0 && !isStr2(doc[key]))
|
|
451
|
+
err("field", `$.${key}`, `${key} must be a string`);
|
|
452
|
+
}
|
|
453
|
+
if (doc.main_characters !== void 0 && (!isArr2(doc.main_characters) || !doc.main_characters.every((item) => isObj2(item) && isStr2(item.name) && isStr2(item.role)))) {
|
|
454
|
+
err("main_characters", "$.main_characters", "main_characters must contain {name, role} string records");
|
|
455
|
+
}
|
|
456
|
+
validateExtend(doc.extend, "$.extend", err);
|
|
457
|
+
const assetKinds = /* @__PURE__ */ new Map();
|
|
458
|
+
const stateIds = /* @__PURE__ */ new Map();
|
|
459
|
+
const actorNames = /* @__PURE__ */ new Set();
|
|
460
|
+
const collectAssets = (list, kind, idField, nameField, path4) => {
|
|
461
|
+
if (!isArr2(list)) {
|
|
462
|
+
err("field", path4, `${path4} must be an array`);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const ids = /* @__PURE__ */ new Set();
|
|
466
|
+
list.forEach((raw, index) => {
|
|
467
|
+
const itemPath = `${path4}[${index}]`;
|
|
468
|
+
if (!isObj2(raw)) {
|
|
469
|
+
err("asset", itemPath, "asset must be an object");
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const id = raw[idField];
|
|
473
|
+
if (!isV4Id(kind, id)) {
|
|
474
|
+
err("asset_id", `${itemPath}.${idField}`, `${idField} must be ${kind} ID`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (ids.has(id))
|
|
478
|
+
err("dup_id", `${itemPath}.${idField}`, `duplicate ${idField} ${id}`);
|
|
479
|
+
ids.add(id);
|
|
480
|
+
assetKinds.set(id, kind);
|
|
481
|
+
if (!isStr2(raw[nameField]) || raw[nameField].trim() === "") {
|
|
482
|
+
err("asset_name", `${itemPath}.${nameField}`, `${nameField} must be a non-empty string`);
|
|
483
|
+
} else if (kind === "actor") {
|
|
484
|
+
const nameKey = raw[nameField].trim().toLocaleLowerCase();
|
|
485
|
+
if (actorNames.has(nameKey)) {
|
|
486
|
+
err("dup_actor_name", `${itemPath}.${nameField}`, `duplicate actor name ${raw[nameField]}`);
|
|
487
|
+
}
|
|
488
|
+
actorNames.add(nameKey);
|
|
489
|
+
}
|
|
490
|
+
if (!isStr2(raw.importance) || !IMPORTANCE_TIERS.includes(raw.importance)) {
|
|
491
|
+
err("importance", `${itemPath}.importance`, `importance must be ${IMPORTANCE_TIERS.join("|")}`);
|
|
492
|
+
}
|
|
493
|
+
if (kind === "actor" && raw.role_type !== void 0 && (!isStr2(raw.role_type) || !ROLE_TYPES.includes(raw.role_type))) {
|
|
494
|
+
err("role_type", `${itemPath}.role_type`, `role_type must be ${ROLE_TYPES.join("|")}`);
|
|
495
|
+
}
|
|
496
|
+
if (!isStr2(raw.description))
|
|
497
|
+
err("asset_desc", `${itemPath}.description`, "description must be a string");
|
|
498
|
+
if (raw.aliases !== void 0 && (!isArr2(raw.aliases) || !raw.aliases.every(isStr2))) {
|
|
499
|
+
err("aliases", `${itemPath}.aliases`, "aliases must be an array of strings");
|
|
500
|
+
} else if (isArr2(raw.aliases) && raw.aliases.every(isStr2)) {
|
|
501
|
+
const seenAliases = /* @__PURE__ */ new Set();
|
|
502
|
+
const assetName2 = isStr2(raw[nameField]) ? raw[nameField] : "";
|
|
503
|
+
raw.aliases.forEach((alias, aliasIndex) => {
|
|
504
|
+
if (alias === "") {
|
|
505
|
+
err("alias_empty", `${itemPath}.aliases[${aliasIndex}]`, "alias must not be empty");
|
|
506
|
+
}
|
|
507
|
+
if (alias === assetName2) {
|
|
508
|
+
err("alias_equals_name", `${itemPath}.aliases[${aliasIndex}]`, "alias must differ from the asset name");
|
|
509
|
+
}
|
|
510
|
+
if (seenAliases.has(alias)) {
|
|
511
|
+
err("dup_alias", `${itemPath}.aliases[${aliasIndex}]`, `duplicate alias ${alias}`);
|
|
512
|
+
}
|
|
513
|
+
seenAliases.add(alias);
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
validateExtend(raw.extend, `${itemPath}.extend`, err);
|
|
517
|
+
const ownerStates = /* @__PURE__ */ new Set();
|
|
518
|
+
const ownerStateNames = /* @__PURE__ */ new Set();
|
|
519
|
+
let defaults = 0;
|
|
520
|
+
if (!isArr2(raw.states) || raw.states.length === 0) {
|
|
521
|
+
err("states", `${itemPath}.states`, "states must be a non-empty array");
|
|
522
|
+
} else {
|
|
523
|
+
raw.states.forEach((state2, stateIndex) => {
|
|
524
|
+
const statePath = `${itemPath}.states[${stateIndex}]`;
|
|
525
|
+
if (!isObj2(state2)) {
|
|
526
|
+
err("state", statePath, "state must be an object");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const stateId = state2.state_id;
|
|
530
|
+
if (stateId !== "default" && !isV4Id("state", stateId)) {
|
|
531
|
+
err("state_id", `${statePath}.state_id`, "state_id must be default or st plus six Base58 characters");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (ownerStates.has(stateId))
|
|
535
|
+
err("dup_state", `${statePath}.state_id`, `duplicate state_id ${stateId}`);
|
|
536
|
+
ownerStates.add(stateId);
|
|
537
|
+
if (stateId === "default")
|
|
538
|
+
defaults++;
|
|
539
|
+
if (!isStr2(state2.state_name) || state2.state_name.trim() === "") {
|
|
540
|
+
err("state_name", `${statePath}.state_name`, "state_name must be a non-empty string");
|
|
541
|
+
} else {
|
|
542
|
+
if (ownerStateNames.has(state2.state_name)) {
|
|
543
|
+
err("dup_state_name", `${statePath}.state_name`, `duplicate state_name ${state2.state_name}`);
|
|
544
|
+
}
|
|
545
|
+
ownerStateNames.add(state2.state_name);
|
|
546
|
+
}
|
|
547
|
+
if (!isStr2(state2.description))
|
|
548
|
+
err("state_description", `${statePath}.description`, "description must be a string");
|
|
549
|
+
validateExtend(state2.extend, `${statePath}.extend`, err);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
if (defaults !== 1) {
|
|
553
|
+
err("default_state", `${itemPath}.states`, `exactly one default state required, found ${defaults}`);
|
|
554
|
+
}
|
|
555
|
+
stateIds.set(id, ownerStates);
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
collectAssets(doc.actors, "actor", "actor_id", "actor_name", "$.actors");
|
|
559
|
+
collectAssets(doc.locations, "location", "location_id", "location_name", "$.locations");
|
|
560
|
+
collectAssets(doc.props, "prop", "prop_id", "prop_name", "$.props");
|
|
561
|
+
const validateStateSet = (assetId, rawStateIds, path4) => {
|
|
562
|
+
if (!isArr2(rawStateIds) || rawStateIds.length === 0 || !rawStateIds.every(isStr2)) {
|
|
563
|
+
err("state_ids", path4, "state_ids must be a non-empty array of strings");
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
566
|
+
const ids = rawStateIds;
|
|
567
|
+
const seen = /* @__PURE__ */ new Set();
|
|
568
|
+
ids.forEach((stateId, index) => {
|
|
569
|
+
if (seen.has(stateId))
|
|
570
|
+
err("dup_state_ref", `${path4}[${index}]`, `duplicate state_id ${stateId}`);
|
|
571
|
+
seen.add(stateId);
|
|
572
|
+
if (!stateIds.get(assetId)?.has(stateId)) {
|
|
573
|
+
err("state_ref", `${path4}[${index}]`, `state ${stateId} not found on ${assetId}`);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
if (!isSorted(ids))
|
|
577
|
+
err("state_ids_order", path4, "state_ids must be sorted lexically");
|
|
578
|
+
return ids;
|
|
579
|
+
};
|
|
580
|
+
const episodeIds = /* @__PURE__ */ new Set();
|
|
581
|
+
const sceneIds = /* @__PURE__ */ new Set();
|
|
582
|
+
const actionIds = /* @__PURE__ */ new Set();
|
|
583
|
+
if (!isArr2(doc.episodes)) {
|
|
584
|
+
err("episodes", "$.episodes", "episodes must be an array");
|
|
585
|
+
} else {
|
|
586
|
+
doc.episodes.forEach((episode, episodeIndex) => {
|
|
587
|
+
const episodePath = `$.episodes[${episodeIndex}]`;
|
|
588
|
+
if (!isObj2(episode)) {
|
|
589
|
+
err("episode", episodePath, "episode must be an object");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (!isV4Id("episode", episode.episode_id)) {
|
|
593
|
+
err("episode_id", `${episodePath}.episode_id`, "episode_id must be ep plus six Base58 characters");
|
|
594
|
+
} else {
|
|
595
|
+
if (episodeIds.has(episode.episode_id)) {
|
|
596
|
+
err("dup_episode_id", `${episodePath}.episode_id`, `duplicate episode_id ${episode.episode_id}`);
|
|
597
|
+
}
|
|
598
|
+
episodeIds.add(episode.episode_id);
|
|
599
|
+
}
|
|
600
|
+
for (const key of ["title", "synopsis"]) {
|
|
601
|
+
if (episode[key] !== void 0 && !isStr2(episode[key])) {
|
|
602
|
+
err("field", `${episodePath}.${key}`, `${key} must be a string`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
validateExtend(episode.extend, `${episodePath}.extend`, err);
|
|
606
|
+
if (!isArr2(episode.scenes)) {
|
|
607
|
+
err("scenes", `${episodePath}.scenes`, "scenes must be an array");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
episode.scenes.forEach((scene, sceneIndex) => {
|
|
611
|
+
const scenePath = `${episodePath}.scenes[${sceneIndex}]`;
|
|
612
|
+
if (!isObj2(scene)) {
|
|
613
|
+
err("scene", scenePath, "scene must be an object");
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (!isV4Id("scene", scene.scene_id)) {
|
|
617
|
+
err("scene_id", `${scenePath}.scene_id`, "scene_id must be scn plus six Base58 characters");
|
|
618
|
+
} else {
|
|
619
|
+
if (sceneIds.has(scene.scene_id)) {
|
|
620
|
+
err("dup_scene_id", `${scenePath}.scene_id`, `duplicate scene_id ${scene.scene_id}`);
|
|
621
|
+
}
|
|
622
|
+
sceneIds.add(scene.scene_id);
|
|
623
|
+
}
|
|
624
|
+
validateExtend(scene.extend, `${scenePath}.extend`, err);
|
|
625
|
+
if (scene.environment === void 0 || isObj2(scene.environment) && (!isStr2(scene.environment.time) || scene.environment.time.trim() === "")) {
|
|
626
|
+
warn("scene_time_missing", `${scenePath}.environment.time`, "scene time is required for production review");
|
|
627
|
+
} else if (!isObj2(scene.environment) || scene.environment.space !== void 0 && !isStr2(scene.environment.space) || !isStr2(scene.environment.time)) {
|
|
628
|
+
err("environment", `${scenePath}.environment`, "environment must contain optional string space and a time string");
|
|
629
|
+
} else if (!isSceneTime(scene.environment.time)) {
|
|
630
|
+
err("scene_time", `${scenePath}.environment.time`, "scene time must be day|night|dawn|dusk");
|
|
631
|
+
}
|
|
632
|
+
if (scene.location !== null) {
|
|
633
|
+
if (!isObj2(scene.location) || !isV4Id("location", scene.location.location_id)) {
|
|
634
|
+
err("location", `${scenePath}.location`, "location must be null or a typed v4 location reference");
|
|
635
|
+
} else {
|
|
636
|
+
if ("state_id" in scene.location) {
|
|
637
|
+
err("v3_field", `${scenePath}.location.state_id`, "Script v4 scene references use state_ids");
|
|
638
|
+
}
|
|
639
|
+
if (assetKinds.get(scene.location.location_id) !== "location") {
|
|
640
|
+
err("location_ref", `${scenePath}.location.location_id`, `location ${scene.location.location_id} not found`);
|
|
641
|
+
}
|
|
642
|
+
validateStateSet(scene.location.location_id, scene.location.state_ids, `${scenePath}.location.state_ids`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const sceneActorStates = /* @__PURE__ */ new Map();
|
|
646
|
+
const validateRefs = (refs, kind, idField, path4) => {
|
|
647
|
+
if (!isArr2(refs)) {
|
|
648
|
+
err("field", path4, `${path4} must be an array`);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const seen = /* @__PURE__ */ new Set();
|
|
652
|
+
refs.forEach((ref2, refIndex) => {
|
|
653
|
+
const refPath = `${path4}[${refIndex}]`;
|
|
654
|
+
if (!isObj2(ref2) || !isV4Id(kind, ref2[idField])) {
|
|
655
|
+
err("ref", refPath, `${idField} must be a typed v4 ID`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const assetId = ref2[idField];
|
|
659
|
+
if ("state_id" in ref2) {
|
|
660
|
+
err("v3_field", `${refPath}.state_id`, "Script v4 scene references use state_ids");
|
|
661
|
+
}
|
|
662
|
+
if (seen.has(assetId))
|
|
663
|
+
err("dup_asset_ref", `${refPath}.${idField}`, `duplicate ${kind} ref ${assetId}`);
|
|
664
|
+
seen.add(assetId);
|
|
665
|
+
if (assetKinds.get(assetId) !== kind) {
|
|
666
|
+
err("asset_ref", `${refPath}.${idField}`, `${kind} ${assetId} not found`);
|
|
667
|
+
}
|
|
668
|
+
const ids = validateStateSet(assetId, ref2.state_ids, `${refPath}.state_ids`);
|
|
669
|
+
if (kind === "actor")
|
|
670
|
+
sceneActorStates.set(assetId, new Set(ids));
|
|
671
|
+
});
|
|
672
|
+
};
|
|
673
|
+
validateRefs(scene.actors, "actor", "actor_id", `${scenePath}.actors`);
|
|
674
|
+
validateRefs(scene.props, "prop", "prop_id", `${scenePath}.props`);
|
|
675
|
+
if (!isArr2(scene.actions)) {
|
|
676
|
+
err("actions", `${scenePath}.actions`, "actions must be an array");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
scene.actions.forEach((action, actionIndex) => {
|
|
680
|
+
const actionPath = `${scenePath}.actions[${actionIndex}]`;
|
|
681
|
+
if (!isObj2(action)) {
|
|
682
|
+
err("action", actionPath, "action must be an object");
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (!isV4Id("action", action.action_id)) {
|
|
686
|
+
err("action_id", `${actionPath}.action_id`, "action_id must be ac plus six Base58 characters");
|
|
687
|
+
} else {
|
|
688
|
+
if (actionIds.has(action.action_id)) {
|
|
689
|
+
err("dup_action_id", `${actionPath}.action_id`, `duplicate action_id ${action.action_id}`);
|
|
690
|
+
}
|
|
691
|
+
actionIds.add(action.action_id);
|
|
692
|
+
}
|
|
693
|
+
if (!isStr2(action.type) || !ACTION_TYPES.includes(action.type)) {
|
|
694
|
+
err("action_type", `${actionPath}.type`, `invalid action type ${String(action.type)}`);
|
|
695
|
+
}
|
|
696
|
+
if (!isStr2(action.content))
|
|
697
|
+
err("content", `${actionPath}.content`, "content must be a string");
|
|
698
|
+
if ("emotion" in action)
|
|
699
|
+
err("v3_field", `${actionPath}.emotion`, "Script v4 does not allow emotion");
|
|
700
|
+
validateExtend(action.extend, `${actionPath}.extend`, err);
|
|
701
|
+
const speaking = action.type === "dialogue" || action.type === "inner_thought";
|
|
702
|
+
if (speaking) {
|
|
703
|
+
if (action.parenthetical !== void 0 && !isStr2(action.parenthetical)) {
|
|
704
|
+
err("parenthetical", `${actionPath}.parenthetical`, "parenthetical must be a string");
|
|
705
|
+
}
|
|
706
|
+
if (!isObj2(action.speaker)) {
|
|
707
|
+
err("speaker", `${actionPath}.speaker`, "dialogue/inner_thought requires a speaker");
|
|
708
|
+
} else if (isStr2(action.speaker.actor_id)) {
|
|
709
|
+
if ("kind" in action.speaker || "label" in action.speaker) {
|
|
710
|
+
err("speaker_shape", `${actionPath}.speaker`, "actor speaker cannot contain non-actor speaker fields");
|
|
711
|
+
}
|
|
712
|
+
const actorId = action.speaker.actor_id;
|
|
713
|
+
if (!isV4Id("actor", actorId) || assetKinds.get(actorId) !== "actor") {
|
|
714
|
+
err("speaker_actor", `${actionPath}.speaker.actor_id`, `actor ${actorId} not found`);
|
|
715
|
+
}
|
|
716
|
+
const sceneStates = sceneActorStates.get(actorId);
|
|
717
|
+
const ownedStates = stateIds.get(actorId);
|
|
718
|
+
const speakerState = isStr2(action.speaker.state_id) && ownedStates?.has(action.speaker.state_id) ? action.speaker.state_id : void 0;
|
|
719
|
+
const defaultState = ownedStates?.has("default") ? "default" : ownedStates?.values().next().value;
|
|
720
|
+
const repairStates = speakerState ? [speakerState] : defaultState ? [defaultState] : [];
|
|
721
|
+
const sceneRefRepair = isV4Id("scene", scene.scene_id) && isV4Id("actor", actorId) && repairStates.length > 0 ? `scriptctl scene-ref ${scene.scene_id} ${actorId} ${repairStates.map((id) => `--state ${id}`).join(" ")} -p "$SCRIPT"` : void 0;
|
|
722
|
+
if (!sceneStates) {
|
|
723
|
+
err("actor_not_in_scene", `${actionPath}.speaker.actor_id`, `actor ${actorId} is not referenced by this scene`, sceneRefRepair);
|
|
724
|
+
}
|
|
725
|
+
if (action.speaker.state_id !== void 0 && (!isStr2(action.speaker.state_id) || !sceneStates?.has(action.speaker.state_id))) {
|
|
726
|
+
const nextStates = speakerState ? [.../* @__PURE__ */ new Set([...sceneStates ?? [], speakerState])].sort() : [];
|
|
727
|
+
const stateRepair = isV4Id("scene", scene.scene_id) && isV4Id("actor", actorId) && nextStates.length > 0 ? `scriptctl scene-ref ${scene.scene_id} ${actorId} ${nextStates.map((id) => `--state ${id}`).join(" ")} -p "$SCRIPT"` : sceneRefRepair;
|
|
728
|
+
err("speaker_state", `${actionPath}.speaker.state_id`, `speaker state ${String(action.speaker.state_id)} is not in this scene's actor state_ids`, stateRepair);
|
|
729
|
+
}
|
|
730
|
+
} else if (isStr2(action.speaker.kind)) {
|
|
731
|
+
if ("actor_id" in action.speaker || "state_id" in action.speaker) {
|
|
732
|
+
err("speaker_shape", `${actionPath}.speaker`, "non-actor speaker cannot contain actor speaker fields");
|
|
733
|
+
}
|
|
734
|
+
if (!NON_ACTOR_SPEAKER_KINDS.includes(action.speaker.kind)) {
|
|
735
|
+
err("speaker_kind", `${actionPath}.speaker.kind`, "invalid non-actor speaker kind");
|
|
736
|
+
}
|
|
737
|
+
if (action.speaker.label !== void 0 && !isStr2(action.speaker.label)) {
|
|
738
|
+
err("speaker_label", `${actionPath}.speaker.label`, "speaker label must be a string");
|
|
739
|
+
}
|
|
740
|
+
} else {
|
|
741
|
+
err("speaker", `${actionPath}.speaker`, "speaker needs actor_id or kind");
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
if ("parenthetical" in action) {
|
|
745
|
+
err("parenthetical_type", `${actionPath}.parenthetical`, "parenthetical only belongs to speaking actions");
|
|
746
|
+
}
|
|
747
|
+
if ("speaker" in action) {
|
|
748
|
+
err("speaker_type", `${actionPath}.speaker`, "speaker only belongs to speaking actions");
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (action.transition_prompt !== void 0) {
|
|
752
|
+
const prompt = action.transition_prompt;
|
|
753
|
+
if (!isObj2(prompt) || !isStr2(prompt.target) || !isStr2(prompt.process) || prompt.process === "" || !isStr2(prompt.contrast) || prompt.contrast === "") {
|
|
754
|
+
err("transition_prompt", `${actionPath}.transition_prompt`, "transition_prompt requires non-empty target/process/contrast");
|
|
755
|
+
} else if (!assetKinds.has(prompt.target)) {
|
|
756
|
+
err("transition_target", `${actionPath}.transition_prompt.target`, `asset ${prompt.target} not found`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
validateActionTimestampOrder(scene.actions, `${scenePath}.actions`, err);
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// packages/screenplay/dist/model.js
|
|
768
|
+
var KINDS = [
|
|
769
|
+
["actor", "actors", "actor"],
|
|
770
|
+
["location", "locations", "location"],
|
|
771
|
+
["prop", "props", "prop"]
|
|
772
|
+
];
|
|
773
|
+
function arr(v) {
|
|
774
|
+
return Array.isArray(v) ? v : [];
|
|
775
|
+
}
|
|
776
|
+
function str(v) {
|
|
777
|
+
return typeof v === "string" ? v : "";
|
|
778
|
+
}
|
|
779
|
+
function put(map, key, value, dupes) {
|
|
780
|
+
if (!key)
|
|
781
|
+
return;
|
|
782
|
+
if (map.has(key) && map.get(key) !== value) {
|
|
783
|
+
dupes.add(key);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
map.set(key, value);
|
|
787
|
+
}
|
|
788
|
+
function parse(raw, path4 = "") {
|
|
789
|
+
const version = raw?.version;
|
|
790
|
+
if (version !== 3 && version !== 4) {
|
|
791
|
+
throw new Error(`Unsupported script.version: ${JSON.stringify(version)} (expected 3 or 4)`);
|
|
792
|
+
}
|
|
793
|
+
const doc = {
|
|
794
|
+
version,
|
|
795
|
+
path: path4,
|
|
796
|
+
raw,
|
|
797
|
+
episodes: [],
|
|
798
|
+
assets: [],
|
|
799
|
+
byId: /* @__PURE__ */ new Map(),
|
|
800
|
+
eps: /* @__PURE__ */ new Map(),
|
|
801
|
+
scenes: /* @__PURE__ */ new Map(),
|
|
802
|
+
actions: /* @__PURE__ */ new Map()
|
|
803
|
+
};
|
|
804
|
+
for (const [kind, field, idKey] of KINDS) {
|
|
805
|
+
for (const a of arr(raw[field])) {
|
|
806
|
+
const asset = {
|
|
807
|
+
id: str(a[`${idKey}_id`]),
|
|
808
|
+
kind,
|
|
809
|
+
name: str(a[`${idKey}_name`]),
|
|
810
|
+
aliases: arr(a.aliases).map(str).filter(Boolean),
|
|
811
|
+
importance: str(a.importance),
|
|
812
|
+
desc: str(a.description),
|
|
813
|
+
states: arr(a.states).map((s) => ({
|
|
814
|
+
id: str(s.state_id),
|
|
815
|
+
name: str(s.state_name),
|
|
816
|
+
desc: str(s.description)
|
|
817
|
+
})),
|
|
818
|
+
raw: a,
|
|
819
|
+
label: ""
|
|
820
|
+
};
|
|
821
|
+
doc.assets.push(asset);
|
|
822
|
+
doc.byId.set(asset.id, asset);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const seen = /* @__PURE__ */ new Map();
|
|
826
|
+
for (const a of doc.assets)
|
|
827
|
+
for (const n of [a.name, ...a.aliases])
|
|
828
|
+
seen.set(n, (seen.get(n) ?? 0) + 1);
|
|
829
|
+
for (const a of doc.assets) {
|
|
830
|
+
a.label = [a.name, ...a.aliases].filter((n) => seen.get(n) === 1).sort((x, y) => x.length - y.length)[0] ?? a.name;
|
|
831
|
+
}
|
|
832
|
+
const dupes = /* @__PURE__ */ new Set();
|
|
833
|
+
let gScene = 0, gAction = 0;
|
|
834
|
+
arr(raw.episodes).forEach((eRaw, ei) => {
|
|
835
|
+
const ep = {
|
|
836
|
+
anchor: String(ei + 1),
|
|
837
|
+
id: str(eRaw.episode_id),
|
|
838
|
+
no: ei + 1,
|
|
839
|
+
title: str(eRaw.title) || void 0,
|
|
840
|
+
synopsis: str(eRaw.synopsis) || void 0,
|
|
841
|
+
scenes: [],
|
|
842
|
+
raw: eRaw
|
|
843
|
+
};
|
|
844
|
+
arr(eRaw.scenes).forEach((sRaw, si) => {
|
|
845
|
+
gScene++;
|
|
846
|
+
const scene = {
|
|
847
|
+
anchor: version === 4 ? str(sRaw.scene_id) : `${ei + 1}-${si + 1}`,
|
|
848
|
+
id: str(sRaw.scene_id),
|
|
849
|
+
no: si + 1,
|
|
850
|
+
gno: gScene,
|
|
851
|
+
env: sRaw.environment ?? {},
|
|
852
|
+
location: refOf(sRaw.location, "location_id"),
|
|
853
|
+
actors: arr(sRaw.actors).map((r) => refOf(r, "actor_id")).filter(Boolean),
|
|
854
|
+
props: arr(sRaw.props).map((r) => refOf(r, "prop_id")).filter(Boolean),
|
|
855
|
+
actions: [],
|
|
856
|
+
raw: sRaw,
|
|
857
|
+
ep
|
|
858
|
+
};
|
|
859
|
+
arr(sRaw.actions).forEach((aRaw, ai) => {
|
|
860
|
+
gAction++;
|
|
861
|
+
const action = {
|
|
862
|
+
anchor: version === 4 ? str(aRaw.action_id) : `${ei + 1}-${si + 1}-${ai + 1}`,
|
|
863
|
+
no: ai + 1,
|
|
864
|
+
gno: gAction,
|
|
865
|
+
type: str(aRaw.type) || "action",
|
|
866
|
+
content: str(aRaw.content),
|
|
867
|
+
speaker: speakerOf(aRaw.speaker),
|
|
868
|
+
paren: typeof aRaw.parenthetical === "string" ? aRaw.parenthetical : typeof aRaw.emotion === "string" ? aRaw.emotion : void 0,
|
|
869
|
+
ts: str(aRaw.extend?.timestamp) || void 0,
|
|
870
|
+
raw: aRaw,
|
|
871
|
+
scene
|
|
872
|
+
};
|
|
873
|
+
scene.actions.push(action);
|
|
874
|
+
put(doc.actions, action.anchor, action, dupes);
|
|
875
|
+
put(doc.actions, `${ei + 1}-${si + 1}-${ai + 1}`, action, dupes);
|
|
876
|
+
if (version === 3)
|
|
877
|
+
put(doc.actions, `${ep.id}/${scene.id}#${ai}`, action, dupes);
|
|
878
|
+
});
|
|
879
|
+
ep.scenes.push(scene);
|
|
880
|
+
put(doc.scenes, scene.anchor, scene, dupes);
|
|
881
|
+
put(doc.scenes, `${ei + 1}-${si + 1}`, scene, dupes);
|
|
882
|
+
put(doc.scenes, `${ep.id}/${scene.id}`, scene, dupes);
|
|
883
|
+
put(doc.scenes, scene.id, scene, dupes);
|
|
884
|
+
});
|
|
885
|
+
doc.episodes.push(ep);
|
|
886
|
+
put(doc.eps, String(ei + 1), ep, dupes);
|
|
887
|
+
put(doc.eps, ep.id, ep, dupes);
|
|
888
|
+
});
|
|
889
|
+
for (const k of dupes) {
|
|
890
|
+
doc.actions.delete(k);
|
|
891
|
+
doc.scenes.delete(k);
|
|
892
|
+
doc.eps.delete(k);
|
|
893
|
+
}
|
|
894
|
+
step(path4 ? "model.load" : "model.parse", `v${version} \xB7 ${doc.episodes.length} ep \xB7 ${doc.scenes.size} scene keys \xB7 ${doc.actions.size} action keys \xB7 ${doc.assets.length} assets \xB7 ${dupes.size} ambiguous keys dropped`);
|
|
895
|
+
return doc;
|
|
896
|
+
}
|
|
897
|
+
function refOf(r, idKey) {
|
|
898
|
+
if (!r || typeof r !== "object")
|
|
899
|
+
return null;
|
|
900
|
+
const id = str(r[idKey]);
|
|
901
|
+
if (!id)
|
|
902
|
+
return null;
|
|
903
|
+
const states = Array.isArray(r.state_ids) ? r.state_ids.map(str) : r.state_id ? [str(r.state_id)] : [];
|
|
904
|
+
return { id, states, raw: r };
|
|
905
|
+
}
|
|
906
|
+
function speakerOf(s) {
|
|
907
|
+
if (!s || typeof s !== "object")
|
|
908
|
+
return void 0;
|
|
909
|
+
if (s.actor_id)
|
|
910
|
+
return { id: str(s.actor_id), state: str(s.state_id) || void 0 };
|
|
911
|
+
if (s.kind)
|
|
912
|
+
return { kind: str(s.kind), label: str(s.label) || void 0 };
|
|
913
|
+
return void 0;
|
|
914
|
+
}
|
|
915
|
+
function resolve(doc, addr) {
|
|
916
|
+
const q = addr.trim();
|
|
917
|
+
if (!q)
|
|
918
|
+
return { t: "script", doc };
|
|
919
|
+
const slash = q.indexOf("/");
|
|
920
|
+
if (slash > 0 && !doc.scenes.has(q)) {
|
|
921
|
+
const left = q.slice(0, slash), right = q.slice(slash + 1);
|
|
922
|
+
const owner = doc.byId.get(left) ?? assetByName(doc, left);
|
|
923
|
+
if (owner) {
|
|
924
|
+
const hit = owner.states.filter((st) => st.id === right || st.name === right);
|
|
925
|
+
if (hit.length === 1) {
|
|
926
|
+
step("resolve", `"${q}" -> state ${owner.id}/${hit[0].id} "${hit[0].name}"`);
|
|
927
|
+
return { t: "state", asset: owner, state: hit[0] };
|
|
928
|
+
}
|
|
929
|
+
if (hit.length > 1) {
|
|
930
|
+
throw new Error(`${owner.name} has ${hit.length} states named "${right}"; address it by state id`);
|
|
931
|
+
}
|
|
932
|
+
throw new Error(`${owner.name} has no state "${right}" (has: ${owner.states.map((st) => `${st.id}(${st.name})`).join(", ")})`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const a = doc.actions.get(q);
|
|
936
|
+
if (a) {
|
|
937
|
+
step("resolve", `"${q}" -> action map hit -> ${a.anchor}`);
|
|
938
|
+
return { t: "action", action: a };
|
|
939
|
+
}
|
|
940
|
+
const s = doc.scenes.get(q);
|
|
941
|
+
if (s) {
|
|
942
|
+
step("resolve", `"${q}" -> scene map hit -> ${s.anchor}`);
|
|
943
|
+
return { t: "scene", scene: s };
|
|
944
|
+
}
|
|
945
|
+
const e = doc.eps.get(q);
|
|
946
|
+
if (e) {
|
|
947
|
+
step("resolve", `"${q}" -> episode map hit -> ${e.id}`);
|
|
948
|
+
return { t: "ep", ep: e };
|
|
949
|
+
}
|
|
950
|
+
const byId = doc.byId.get(q);
|
|
951
|
+
if (byId) {
|
|
952
|
+
step("resolve", `"${q}" -> asset id hit -> ${byId.name}`);
|
|
953
|
+
return { t: "asset", asset: byId };
|
|
954
|
+
}
|
|
955
|
+
const hits = doc.assets.filter((x) => x.name === q || x.aliases.includes(q));
|
|
956
|
+
if (hits.length === 1) {
|
|
957
|
+
step("resolve", `"${q}" -> exact name/alias -> ${hits[0].id} ${hits[0].name}`);
|
|
958
|
+
return { t: "asset", asset: hits[0] };
|
|
959
|
+
}
|
|
960
|
+
if (hits.length > 1) {
|
|
961
|
+
throw new Error(`Ambiguous name "${q}": ${hits.map((h) => `${h.id}(${h.name})`).join(", ")}`);
|
|
962
|
+
}
|
|
963
|
+
const loose = doc.assets.filter((x) => x.name.includes(q) || x.aliases.some((al) => al.includes(q)));
|
|
964
|
+
if (loose.length === 1) {
|
|
965
|
+
step("resolve", `"${q}" -> substring name match -> ${loose[0].id} ${loose[0].name}`);
|
|
966
|
+
return { t: "asset", asset: loose[0] };
|
|
967
|
+
}
|
|
968
|
+
throw new Error(`Unknown address "${q}". Use an ordinal (3 / 3-2 / 3-2-5), a canonical ID, or an asset name.` + (loose.length ? ` Did you mean: ${loose.slice(0, 5).map((h) => h.name).join(", ")}?` : ""));
|
|
969
|
+
}
|
|
970
|
+
function assetByName(doc, q) {
|
|
971
|
+
const exact = doc.assets.filter((x) => x.name === q || x.aliases.includes(q));
|
|
972
|
+
if (exact.length === 1)
|
|
973
|
+
return exact[0];
|
|
974
|
+
if (exact.length > 1)
|
|
975
|
+
return void 0;
|
|
976
|
+
const loose = doc.assets.filter((x) => x.name.includes(q) || x.aliases.some((a) => a.includes(q)));
|
|
977
|
+
return loose.length === 1 ? loose[0] : void 0;
|
|
978
|
+
}
|
|
979
|
+
function assetName(doc, id) {
|
|
980
|
+
return doc.byId.get(id)?.label || id;
|
|
981
|
+
}
|
|
982
|
+
function stateName(doc, assetId, stateId) {
|
|
983
|
+
const st = doc.byId.get(assetId)?.states.find((s) => s.id === stateId);
|
|
984
|
+
return st?.name || stateId;
|
|
985
|
+
}
|
|
986
|
+
function allActions(doc) {
|
|
987
|
+
return doc.episodes.flatMap((e) => e.scenes.flatMap((s) => s.actions));
|
|
988
|
+
}
|
|
989
|
+
var KIND_OF = {
|
|
990
|
+
ep: "episode",
|
|
991
|
+
scn: "scene",
|
|
992
|
+
ac: "action",
|
|
993
|
+
act: "actor",
|
|
994
|
+
loc: "location",
|
|
995
|
+
prp: "prop",
|
|
996
|
+
st: "state"
|
|
997
|
+
};
|
|
998
|
+
function newId(prefix, taken) {
|
|
999
|
+
const id = generateV4Id(KIND_OF[prefix], taken);
|
|
1000
|
+
taken.add(id);
|
|
1001
|
+
step("newId", `${id} (collision-checked against ${taken.size - 1} live ids)`);
|
|
1002
|
+
return id;
|
|
1003
|
+
}
|
|
1004
|
+
function save(doc) {
|
|
1005
|
+
if (!doc.path) {
|
|
1006
|
+
step("save", "skipped: document is not bound to a file");
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
commit(doc, doc.path);
|
|
1010
|
+
}
|
|
1011
|
+
function commit(doc, path4 = doc.path) {
|
|
1012
|
+
if (!path4)
|
|
1013
|
+
throw new Error("commit needs a path: this document is not bound to a file");
|
|
1014
|
+
const tmp = `${path4}.tmp-${process.pid}`;
|
|
1015
|
+
fs.writeFileSync(tmp, JSON.stringify(doc.raw, null, 2) + "\n");
|
|
1016
|
+
fs.renameSync(tmp, path4);
|
|
1017
|
+
doc.path = path4;
|
|
1018
|
+
step("commit", `${fs.statSync(path4).size} bytes -> ${path4} (tmp + atomic rename)`);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// packages/screenplay/dist/text.js
|
|
1022
|
+
var NON_ACTOR = {
|
|
1023
|
+
system: "\u65C1\u767D",
|
|
1024
|
+
broadcast: "\u5E7F\u64AD",
|
|
1025
|
+
offscreen: "\u753B\u5916",
|
|
1026
|
+
group: "\u7FA4\u58F0"
|
|
1027
|
+
};
|
|
1028
|
+
var THOUGHT = "\u5FC3\u58F0";
|
|
1029
|
+
function speakerLabel(doc, sp) {
|
|
1030
|
+
if (sp.kind)
|
|
1031
|
+
return NON_ACTOR[sp.kind] ?? sp.kind;
|
|
1032
|
+
const name = assetName(doc, sp.id);
|
|
1033
|
+
return sp.state ? `${name}\xB7${stateName(doc, sp.id, sp.state)}` : name;
|
|
1034
|
+
}
|
|
1035
|
+
function formatAction(doc, a, gutter, opts = {}) {
|
|
1036
|
+
const pad = (opts.plain ? "" : a.anchor).padEnd(gutter);
|
|
1037
|
+
const ts = !opts.plain && a.ts ? `${a.ts} ` : "";
|
|
1038
|
+
const body = (() => {
|
|
1039
|
+
if (a.type === "transition")
|
|
1040
|
+
return `\u2192 ${a.content}`;
|
|
1041
|
+
if (a.type === "dialogue" || a.type === "inner_thought") {
|
|
1042
|
+
const who = a.speaker ? speakerLabel(doc, a.speaker) : "?";
|
|
1043
|
+
const notes = [a.type === "inner_thought" ? THOUGHT : "", a.paren ?? ""].filter(Boolean);
|
|
1044
|
+
const label = a.speaker?.kind && a.speaker.label ? `${who}\xB7${a.speaker.label}` : who;
|
|
1045
|
+
return `${label}${notes.length ? `(${notes.join("/")})` : ""}\uFF1A${a.content}`;
|
|
1046
|
+
}
|
|
1047
|
+
return a.content;
|
|
1048
|
+
})();
|
|
1049
|
+
return `${pad}${pad ? " " : ""}${ts}${body}`.replace(/\n/g, "\\n");
|
|
1050
|
+
}
|
|
1051
|
+
function refText(doc, r) {
|
|
1052
|
+
const name = assetName(doc, r.id);
|
|
1053
|
+
if (r.states.length === 1 && r.states[0] === "default")
|
|
1054
|
+
return name;
|
|
1055
|
+
return `${name}(${r.states.map((x) => stateName(doc, r.id, x)).join("+")})`;
|
|
1056
|
+
}
|
|
1057
|
+
function formatSceneHead(doc, s) {
|
|
1058
|
+
const env = [s.env?.space, s.env?.time].filter(Boolean).join("/").toUpperCase();
|
|
1059
|
+
const where = s.location ? refText(doc, s.location) : "\u2014";
|
|
1060
|
+
const head = `## ${s.ep.no}-${s.no} ${s.anchor === `${s.ep.no}-${s.no}` ? s.id : s.anchor} ${env || "\u2014"} ${where} ${s.actions.length} actions`;
|
|
1061
|
+
const cast = s.actors.map((r) => refText(doc, r)).join(", ");
|
|
1062
|
+
const props = s.props.map((r) => refText(doc, r)).join(", ");
|
|
1063
|
+
const meta = [cast && `cast: ${cast}`, props && `props: ${props}`].filter(Boolean).join(" ");
|
|
1064
|
+
return meta ? `${head}
|
|
1065
|
+
# ${meta}` : head;
|
|
1066
|
+
}
|
|
1067
|
+
function formatScene(doc, s, opts = {}) {
|
|
1068
|
+
const gutter = opts.plain ? 0 : Math.max(0, ...s.actions.map((a) => a.anchor.length));
|
|
1069
|
+
step("formatScene", `${s.anchor}: ${s.actions.length} actions, gutter=${gutter}`);
|
|
1070
|
+
return [formatSceneHead(doc, s), ...s.actions.map((a) => formatAction(doc, a, gutter, opts))].join("\n");
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// packages/screenplay/dist/span.js
|
|
1074
|
+
function fail(message, detail) {
|
|
1075
|
+
throw Object.assign(new Error(message), detail);
|
|
1076
|
+
}
|
|
1077
|
+
function spanPoints(doc) {
|
|
1078
|
+
return doc.episodes.flatMap((ep) => ep.scenes.flatMap((scene) => scene.actions.map((a, index) => ({
|
|
1079
|
+
address: doc.version === 4 ? a.anchor : `${ep.id}/${scene.id}#${index}`,
|
|
1080
|
+
episodeId: ep.id,
|
|
1081
|
+
episodeNo: ep.no,
|
|
1082
|
+
timestamp: a.ts
|
|
1083
|
+
}))));
|
|
1084
|
+
}
|
|
1085
|
+
function stamp(point) {
|
|
1086
|
+
try {
|
|
1087
|
+
return parseTimestamp(point.timestamp ?? "");
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
if (!(error instanceof TimestampParseError))
|
|
1090
|
+
throw error;
|
|
1091
|
+
return fail(`Action ${point.address} has an invalid stored timestamp.`, {
|
|
1092
|
+
code: "ACTION_SPAN_TIMESTAMP_INVALID",
|
|
1093
|
+
hint: `Fix it with \`scriptctl timestamp ${point.address} --set <time>\` or remove it.`,
|
|
1094
|
+
data: {
|
|
1095
|
+
expected: ["valid action.extend.timestamp"],
|
|
1096
|
+
actual: [`${point.address}: ${JSON.stringify(point.timestamp ?? "")}`]
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
function nonMonotonic(previous, current) {
|
|
1102
|
+
return fail("Action timestamps run backward within the selected episode.", {
|
|
1103
|
+
code: "ACTION_SPAN_TIMESTAMP_NON_MONOTONIC",
|
|
1104
|
+
hint: "Fix the stored timestamps so they do not move backward in document order, then rerun `actions --span`.",
|
|
1105
|
+
data: { actual: [`${previous.address}: ${previous.timestamp}`, `${current.address}: ${current.timestamp}`] }
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
function resolveSpan(timeline, selectedAddresses) {
|
|
1109
|
+
if (selectedAddresses.length === 0) {
|
|
1110
|
+
fail("The action selection matched nothing.", {
|
|
1111
|
+
code: "ACTION_SPAN_NO_MATCH",
|
|
1112
|
+
hint: "Check the selector with `scriptctl actions` and rerun with `--span`.",
|
|
1113
|
+
data: { actual: ["0 matched actions"] }
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
const wanted = new Set(selectedAddresses);
|
|
1117
|
+
const selected = timeline.filter((point) => wanted.has(point.address));
|
|
1118
|
+
if (selected.length !== wanted.size) {
|
|
1119
|
+
fail("The resolved action selection is inconsistent with the script timeline.", {
|
|
1120
|
+
code: "ACTION_SPAN_SELECTION_INCONSISTENT",
|
|
1121
|
+
data: {
|
|
1122
|
+
expected: [`${wanted.size} timeline actions`],
|
|
1123
|
+
actual: [`${selected.length} timeline actions`]
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
const episodeIds = new Set(selected.map((point) => point.episodeId));
|
|
1128
|
+
if (episodeIds.size !== 1) {
|
|
1129
|
+
fail("An action timestamp span must stay within one episode.", {
|
|
1130
|
+
code: "ACTION_SPAN_CROSSES_EPISODES",
|
|
1131
|
+
hint: "Split the selector into one `actions --span` call per episode.",
|
|
1132
|
+
data: { actual: [...episodeIds] }
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
const first = selected[0], last = selected[selected.length - 1];
|
|
1136
|
+
if (!first.timestamp) {
|
|
1137
|
+
fail(`The first selected Action ${first.address} has no timestamp.`, {
|
|
1138
|
+
code: "ACTION_SPAN_START_TIMESTAMP_MISSING",
|
|
1139
|
+
hint: `Set it with \`scriptctl timestamp ${first.address} --set <time>\`, then rerun.`,
|
|
1140
|
+
data: { actual: [first.address] }
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
const start = stamp(first);
|
|
1144
|
+
let prevPoint = first, prev = start, lastSelected = "";
|
|
1145
|
+
for (const point of selected) {
|
|
1146
|
+
if (!point.timestamp)
|
|
1147
|
+
continue;
|
|
1148
|
+
const ts = stamp(point);
|
|
1149
|
+
if (ts.milliseconds < prev.milliseconds)
|
|
1150
|
+
nonMonotonic(prevPoint, point);
|
|
1151
|
+
prevPoint = point;
|
|
1152
|
+
prev = ts;
|
|
1153
|
+
if (point.address === last.address)
|
|
1154
|
+
lastSelected = ts.canonical;
|
|
1155
|
+
}
|
|
1156
|
+
const lastIndex = timeline.findIndex((point) => point.address === last.address);
|
|
1157
|
+
let boundary;
|
|
1158
|
+
let boundaryTs;
|
|
1159
|
+
for (let i = lastIndex + 1; i < timeline.length; i += 1) {
|
|
1160
|
+
const candidate = timeline[i];
|
|
1161
|
+
if (candidate.episodeId !== first.episodeId)
|
|
1162
|
+
break;
|
|
1163
|
+
if (!candidate.timestamp)
|
|
1164
|
+
continue;
|
|
1165
|
+
const ts = stamp(candidate);
|
|
1166
|
+
if (ts.milliseconds < prev.milliseconds)
|
|
1167
|
+
nonMonotonic(prevPoint, candidate);
|
|
1168
|
+
if (ts.milliseconds === prev.milliseconds)
|
|
1169
|
+
continue;
|
|
1170
|
+
boundary = candidate;
|
|
1171
|
+
boundaryTs = ts;
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
step("span", `${first.episodeId} ${start.canonical}..${boundaryTs?.canonical ?? "(open)"} over ${selected.length} action(s)`);
|
|
1175
|
+
return {
|
|
1176
|
+
episode_id: first.episodeId,
|
|
1177
|
+
ep_no: first.episodeNo,
|
|
1178
|
+
start_timestamp: start.canonical,
|
|
1179
|
+
last_selected_timestamp: lastSelected,
|
|
1180
|
+
end_timestamp: boundaryTs?.canonical ?? "",
|
|
1181
|
+
end_open: boundaryTs === void 0,
|
|
1182
|
+
end_exclusive: true,
|
|
1183
|
+
start_action: first.address,
|
|
1184
|
+
last_action: last.address,
|
|
1185
|
+
end_boundary_action: boundary?.address ?? "",
|
|
1186
|
+
matched_actions: selected.length
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
var SPAN_COLUMNS = [
|
|
1190
|
+
"episode_id",
|
|
1191
|
+
"ep_no",
|
|
1192
|
+
"start_timestamp",
|
|
1193
|
+
"last_selected_timestamp",
|
|
1194
|
+
"end_timestamp",
|
|
1195
|
+
"end_open",
|
|
1196
|
+
"end_exclusive",
|
|
1197
|
+
"start_action",
|
|
1198
|
+
"last_action",
|
|
1199
|
+
"end_boundary_action",
|
|
1200
|
+
"matched_actions"
|
|
1201
|
+
];
|
|
1202
|
+
function spanAddresses(doc, actions) {
|
|
1203
|
+
return actions.map((a) => {
|
|
1204
|
+
if (doc.version === 4)
|
|
1205
|
+
return a.anchor;
|
|
1206
|
+
const index = a.scene.actions.indexOf(a);
|
|
1207
|
+
return `${a.scene.ep.id}/${a.scene.id}#${index}`;
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// packages/screenplay/dist/tools.js
|
|
1212
|
+
var NON_ACTOR_KIND = {
|
|
1213
|
+
\u65C1\u767D: "system",
|
|
1214
|
+
\u5E7F\u64AD: "broadcast",
|
|
1215
|
+
\u753B\u5916: "offscreen",
|
|
1216
|
+
\u7FA4\u58F0: "group"
|
|
1217
|
+
};
|
|
1218
|
+
function read(doc, addr = "", opts = {}) {
|
|
1219
|
+
const node = resolve(doc, addr);
|
|
1220
|
+
step("read", `dispatch on node kind "${node.t}"`);
|
|
1221
|
+
switch (node.t) {
|
|
1222
|
+
case "script":
|
|
1223
|
+
return scriptCard(doc);
|
|
1224
|
+
case "ep":
|
|
1225
|
+
return readEpisode(doc, node.ep, opts);
|
|
1226
|
+
case "scene":
|
|
1227
|
+
return formatScene(doc, node.scene, opts);
|
|
1228
|
+
case "asset":
|
|
1229
|
+
return assetCard(doc, node.asset);
|
|
1230
|
+
case "state":
|
|
1231
|
+
return stateCard(doc, node.asset, node.state);
|
|
1232
|
+
case "action": {
|
|
1233
|
+
const n = opts.context ?? 3;
|
|
1234
|
+
const all = node.action.scene.actions;
|
|
1235
|
+
const i = all.indexOf(node.action);
|
|
1236
|
+
const slice = all.slice(Math.max(0, i - n), i + n + 1);
|
|
1237
|
+
const gutter = opts.plain ? 0 : Math.max(...slice.map((a) => a.anchor.length));
|
|
1238
|
+
const tp = node.action.raw.transition_prompt;
|
|
1239
|
+
const extra = Object.entries(node.action.raw.extend ?? {}).filter(([k]) => k !== "timestamp");
|
|
1240
|
+
return [
|
|
1241
|
+
formatSceneHead(doc, node.action.scene),
|
|
1242
|
+
...slice.map((a) => (a === node.action ? "> " : " ") + formatAction(doc, a, gutter, opts)),
|
|
1243
|
+
// Fields that have no place on the prose line still have to be visible
|
|
1244
|
+
// somewhere, or "editable" is a lie.
|
|
1245
|
+
tp ? `transition: ${assetName(doc, tp.target)} | ${tp.process} | ${tp.contrast}` : "",
|
|
1246
|
+
extra.length ? `extend: ${extra.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ")}` : ""
|
|
1247
|
+
].filter(Boolean).join("\n");
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
function chars(s) {
|
|
1252
|
+
return s.actions.reduce((n, a) => n + a.content.length, 0);
|
|
1253
|
+
}
|
|
1254
|
+
function scriptCard(doc) {
|
|
1255
|
+
const r = doc.raw;
|
|
1256
|
+
const scenes = doc.episodes.reduce((n, e) => n + e.scenes.length, 0);
|
|
1257
|
+
const actions = doc.episodes.reduce((n, e) => n + e.scenes.reduce((m, s) => m + s.actions.length, 0), 0);
|
|
1258
|
+
const out = [
|
|
1259
|
+
`${r.title || "(untitled)"} v${doc.version} ${doc.episodes.length} ep \xB7 ${scenes} scenes \xB7 ${actions} actions`
|
|
1260
|
+
];
|
|
1261
|
+
for (const k of ["logline", "theme", "synopsis", "worldview", "style"]) {
|
|
1262
|
+
if (r[k])
|
|
1263
|
+
out.push(`${k}: ${clip(String(r[k]), 300)}`);
|
|
1264
|
+
}
|
|
1265
|
+
if (Array.isArray(r.main_characters) && r.main_characters.length) {
|
|
1266
|
+
out.push(`main: ${r.main_characters.map((m) => `${m.name}=${m.role}`).join(", ")}`);
|
|
1267
|
+
}
|
|
1268
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
1269
|
+
const list = doc.assets.filter((a) => a.kind === kind);
|
|
1270
|
+
if (!list.length)
|
|
1271
|
+
continue;
|
|
1272
|
+
out.push("", `${kind}s (${list.length})`);
|
|
1273
|
+
for (const a of list.slice(0, 40)) {
|
|
1274
|
+
const al = a.aliases.length ? ` ~${a.aliases.join(",")}` : "";
|
|
1275
|
+
const imp = a.importance ? ` [${a.importance}]` : "";
|
|
1276
|
+
out.push(` ${a.id} ${a.name}${imp} ${a.states.length}st${al}`);
|
|
1277
|
+
}
|
|
1278
|
+
if (list.length > 40)
|
|
1279
|
+
out.push(` \u2026 ${list.length - 40} more`);
|
|
1280
|
+
}
|
|
1281
|
+
out.push("", "episodes");
|
|
1282
|
+
for (const e of doc.episodes) {
|
|
1283
|
+
const c = e.scenes.reduce((n, s) => n + chars(s), 0);
|
|
1284
|
+
out.push(` ${e.no} ${e.id} ${e.scenes.length} scenes ${c} chars${e.title ? ` ${e.title}` : ""}` + (e.synopsis ? ` ${clip(e.synopsis, 120)}` : ""));
|
|
1285
|
+
}
|
|
1286
|
+
return out.join("\n");
|
|
1287
|
+
}
|
|
1288
|
+
function readEpisode(doc, ep, opts) {
|
|
1289
|
+
const head = `# EP${String(ep.no).padStart(2, "0")} ${ep.id} ${ep.scenes.length} scenes` + (ep.title ? ` ${ep.title}` : "");
|
|
1290
|
+
if (opts.depth === "outline") {
|
|
1291
|
+
return [head, ...ep.scenes.map((s) => formatSceneHead(doc, s))].join("\n");
|
|
1292
|
+
}
|
|
1293
|
+
return [head, ...ep.scenes.map((s) => formatScene(doc, s, opts))].join("\n\n");
|
|
1294
|
+
}
|
|
1295
|
+
function assetCard(doc, a) {
|
|
1296
|
+
const role = a.raw.role_type ? ` <${a.raw.role_type}>` : "";
|
|
1297
|
+
const out = [`${a.id} ${a.name} ${a.kind}${a.importance ? ` [${a.importance}]` : ""}${role}`];
|
|
1298
|
+
if (a.aliases.length)
|
|
1299
|
+
out.push(`aliases: ${a.aliases.join(", ")}`);
|
|
1300
|
+
if (a.desc)
|
|
1301
|
+
out.push(a.desc);
|
|
1302
|
+
out.push(`states (${a.states.length})`);
|
|
1303
|
+
for (const s of a.states)
|
|
1304
|
+
out.push(` ${s.id} ${s.name} ${clip(s.desc, 160)}`);
|
|
1305
|
+
const hits = [];
|
|
1306
|
+
const byState = /* @__PURE__ */ new Map();
|
|
1307
|
+
for (const ep of doc.episodes)
|
|
1308
|
+
for (const s of ep.scenes) {
|
|
1309
|
+
const ref2 = [s.location, ...s.actors, ...s.props].find((r) => r && r.id === a.id);
|
|
1310
|
+
if (!ref2)
|
|
1311
|
+
continue;
|
|
1312
|
+
hits.push(s);
|
|
1313
|
+
for (const st of ref2.states)
|
|
1314
|
+
byState.set(st, (byState.get(st) ?? 0) + 1);
|
|
1315
|
+
}
|
|
1316
|
+
if (hits.length) {
|
|
1317
|
+
const eps = [...new Set(hits.map((s) => s.ep.no))];
|
|
1318
|
+
out.push(`appears: ${hits.length} scenes across ${eps.length} ep first ${hits[0].ep.no}-${hits[0].no} last ${hits.at(-1).ep.no}-${hits.at(-1).no}`);
|
|
1319
|
+
out.push(`state usage: ${[...byState].map(([k, v]) => `${stateName(doc, a.id, k)}\xD7${v}`).join(", ")}`);
|
|
1320
|
+
const unused = a.states.filter((s) => !byState.has(s.id));
|
|
1321
|
+
if (unused.length)
|
|
1322
|
+
out.push(`unused states: ${unused.map((s) => `${s.id}(${s.name})`).join(", ")}`);
|
|
1323
|
+
} else {
|
|
1324
|
+
out.push("appears: never referenced");
|
|
1325
|
+
}
|
|
1326
|
+
return out.join("\n");
|
|
1327
|
+
}
|
|
1328
|
+
function stateCard(doc, a, st) {
|
|
1329
|
+
const scenes = [];
|
|
1330
|
+
for (const ep of doc.episodes)
|
|
1331
|
+
for (const s of ep.scenes) {
|
|
1332
|
+
const ref2 = [s.location, ...s.actors, ...s.props].find((r) => r && r.id === a.id);
|
|
1333
|
+
if (ref2?.states.includes(st.id))
|
|
1334
|
+
scenes.push(s);
|
|
1335
|
+
}
|
|
1336
|
+
const speakers = allActions(doc).filter((x) => x.speaker?.id === a.id && x.speaker.state === st.id);
|
|
1337
|
+
return [
|
|
1338
|
+
`${a.id}/${st.id} ${st.name} (${a.kind} ${a.name})`,
|
|
1339
|
+
st.desc,
|
|
1340
|
+
scenes.length ? `bound in ${scenes.length} scene(s): ${scenes.slice(0, 12).map((s) => `${s.ep.no}-${s.no}`).join(", ")}` + (scenes.length > 12 ? ` \u2026` : "") : "bound in no scene",
|
|
1341
|
+
speakers.length ? `named by ${speakers.length} speaker reference(s)` : ""
|
|
1342
|
+
].filter(Boolean).join("\n");
|
|
1343
|
+
}
|
|
1344
|
+
function clip(s, n) {
|
|
1345
|
+
return s.length > n ? s.slice(0, n) + "\u2026" : s;
|
|
1346
|
+
}
|
|
1347
|
+
function filterActions(doc, opts) {
|
|
1348
|
+
let pool2 = allActions(doc);
|
|
1349
|
+
step("find", `pool = all actions (${pool2.length})`);
|
|
1350
|
+
if (opts.in) {
|
|
1351
|
+
const node = resolve(doc, opts.in);
|
|
1352
|
+
if (node.t === "ep")
|
|
1353
|
+
pool2 = node.ep.scenes.flatMap((s) => s.actions);
|
|
1354
|
+
else if (node.t === "scene")
|
|
1355
|
+
pool2 = node.scene.actions;
|
|
1356
|
+
else if (node.t === "action")
|
|
1357
|
+
pool2 = [node.action];
|
|
1358
|
+
else
|
|
1359
|
+
throw new Error(`--in must address an episode, scene or action (got ${node.t})`);
|
|
1360
|
+
step("find", `--in ${opts.in} narrows pool to ${pool2.length}`);
|
|
1361
|
+
}
|
|
1362
|
+
if (opts.type) {
|
|
1363
|
+
pool2 = pool2.filter((a) => a.type === opts.type);
|
|
1364
|
+
step("find", `--type ${opts.type} -> ${pool2.length}`);
|
|
1365
|
+
}
|
|
1366
|
+
if (opts.speaker) {
|
|
1367
|
+
const who = resolve(doc, opts.speaker);
|
|
1368
|
+
if (who.t !== "asset")
|
|
1369
|
+
throw new Error(`--speaker must name an actor`);
|
|
1370
|
+
pool2 = pool2.filter((a) => a.speaker?.id === who.asset.id);
|
|
1371
|
+
step("find", `--speaker ${opts.speaker} -> ${who.asset.id} -> ${pool2.length}`);
|
|
1372
|
+
}
|
|
1373
|
+
return pool2;
|
|
1374
|
+
}
|
|
1375
|
+
function find(doc, query, opts = {}) {
|
|
1376
|
+
const pool2 = filterActions(doc, opts);
|
|
1377
|
+
const re = query ? new RegExp(query, "u") : null;
|
|
1378
|
+
const hits = pool2.filter((a) => !re || re.test(a.content));
|
|
1379
|
+
step("find", `regex /${query}/ -> ${hits.length} hits`);
|
|
1380
|
+
const limit = opts.limit ?? 40;
|
|
1381
|
+
const w = opts.window ?? 16;
|
|
1382
|
+
const shown = hits.slice(0, limit);
|
|
1383
|
+
const gutter = Math.max(0, ...shown.map((a) => a.anchor.length));
|
|
1384
|
+
const head = `${hits.length} hit${hits.length === 1 ? "" : "s"}` + (query ? ` for /${query}/` : "") + (hits.length > shown.length ? ` (showing ${shown.length})` : "");
|
|
1385
|
+
const lines = shown.map((a) => {
|
|
1386
|
+
const ord = `${a.scene.ep.no}-${a.scene.no}-${a.no}`;
|
|
1387
|
+
const pos = ord === a.anchor ? "" : ord.padEnd(8) + " ";
|
|
1388
|
+
const who = a.speaker?.id ? assetName(doc, a.speaker.id) + "\uFF1A" : "";
|
|
1389
|
+
const body = opts.full || !re ? a.content : windowed(a.content, re, w);
|
|
1390
|
+
return `${a.anchor.padEnd(gutter)} ${pos}${who}${body}`;
|
|
1391
|
+
});
|
|
1392
|
+
return [head, ...lines].join("\n");
|
|
1393
|
+
}
|
|
1394
|
+
function windowed(text, re, w) {
|
|
1395
|
+
const m = text.match(re);
|
|
1396
|
+
if (!m || m.index === void 0)
|
|
1397
|
+
return clip(text, w * 2);
|
|
1398
|
+
const a = Math.max(0, m.index - w), b = Math.min(text.length, m.index + m[0].length + w);
|
|
1399
|
+
return (a > 0 ? "\u2026" : "") + text.slice(a, b) + (b < text.length ? "\u2026" : "");
|
|
1400
|
+
}
|
|
1401
|
+
function span(doc, range) {
|
|
1402
|
+
const pool2 = filterActions(doc, { in: range });
|
|
1403
|
+
const row = resolveSpan(spanPoints(doc), spanAddresses(doc, pool2));
|
|
1404
|
+
return SPAN_COLUMNS.map((c) => `${c}=${row[c]}`).join(" ");
|
|
1405
|
+
}
|
|
1406
|
+
var ACTION_FIELDS = ["content", "paren", "speaker", "ts", "type", "transition"];
|
|
1407
|
+
var ASSET_FIELDS = ["name", "desc", "importance", "aliases", "role"];
|
|
1408
|
+
var STATE_FIELDS = ["name", "desc"];
|
|
1409
|
+
var EPISODE_FIELDS = ["title", "synopsis"];
|
|
1410
|
+
var SCENE_FIELDS = ["space", "time"];
|
|
1411
|
+
var SCRIPT_FIELDS = ["title", "worldview", "style", "synopsis", "logline", "theme", "main"];
|
|
1412
|
+
function only(at, kind, fields, allowed) {
|
|
1413
|
+
const given = Object.keys(fields).filter((k) => fields[k] !== void 0);
|
|
1414
|
+
if (!given.length)
|
|
1415
|
+
throw new Error(`Nothing to change on ${at}; pass at least one field`);
|
|
1416
|
+
const bad = given.filter((k) => !allowed.includes(k));
|
|
1417
|
+
if (bad.length) {
|
|
1418
|
+
const art = /^[aeiou]/.test(kind) ? "an" : "a";
|
|
1419
|
+
throw new Error(`${at} is ${art} ${kind}; it has no ${bad.join(", ")} \u2014 ${art} ${kind} takes ${allowed.join(", ")}`);
|
|
1420
|
+
}
|
|
1421
|
+
return given;
|
|
1422
|
+
}
|
|
1423
|
+
function edit(doc, at, fields) {
|
|
1424
|
+
const node = resolve(doc, at);
|
|
1425
|
+
if (node.t === "asset")
|
|
1426
|
+
return editAsset(doc, at, node.asset, fields);
|
|
1427
|
+
if (node.t === "state")
|
|
1428
|
+
return editState(doc, at, node.asset, node.state, fields);
|
|
1429
|
+
if (node.t === "script")
|
|
1430
|
+
return editPlain(doc, at || "script", "script", node.doc.raw, fields, SCRIPT_FIELDS);
|
|
1431
|
+
if (node.t === "ep")
|
|
1432
|
+
return editPlain(doc, at, "episode", node.ep.raw, fields, EPISODE_FIELDS);
|
|
1433
|
+
if (node.t === "scene")
|
|
1434
|
+
return editScene(doc, at, node.scene, fields);
|
|
1435
|
+
const a = node.action;
|
|
1436
|
+
const raw = a.raw;
|
|
1437
|
+
const changed = only(a.anchor, "action", fields, ACTION_FIELDS);
|
|
1438
|
+
const base = baseline(doc);
|
|
1439
|
+
step("edit", `${a.anchor}: ${changed.join(", ")}`);
|
|
1440
|
+
if (fields.type !== void 0)
|
|
1441
|
+
raw.type = oneOf("type", fields.type, ACTION_TYPE_SET, ACTION_TYPES);
|
|
1442
|
+
if (fields.content !== void 0)
|
|
1443
|
+
raw.content = fields.content;
|
|
1444
|
+
if (fields.paren !== void 0)
|
|
1445
|
+
setParen(doc, raw, fields.paren);
|
|
1446
|
+
if (fields.speaker !== void 0)
|
|
1447
|
+
raw.speaker = speakerJson(doc, fields.speaker);
|
|
1448
|
+
if (fields.ts !== void 0)
|
|
1449
|
+
setTs(raw, fields.ts);
|
|
1450
|
+
if (fields.transition !== void 0) {
|
|
1451
|
+
if (fields.transition === null)
|
|
1452
|
+
delete raw.transition_prompt;
|
|
1453
|
+
else {
|
|
1454
|
+
const [target, process2, contrast] = fields.transition.split("|").map((x) => x.trim());
|
|
1455
|
+
if (!target || !process2 || !contrast) {
|
|
1456
|
+
throw new Error(`transition takes "target|process|contrast" (got "${fields.transition}")`);
|
|
1457
|
+
}
|
|
1458
|
+
const t = resolve(doc, target);
|
|
1459
|
+
if (t.t !== "asset")
|
|
1460
|
+
throw new Error(`transition target must be an asset (got ${target})`);
|
|
1461
|
+
raw.transition_prompt = { target: t.asset.id, process: process2, contrast };
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
save(doc);
|
|
1465
|
+
return `${a.anchor} ${changed.join("+")} ${preview(raw)}` + since(doc, base);
|
|
1466
|
+
}
|
|
1467
|
+
function editPlain(doc, at, kind, raw, fields, allowed) {
|
|
1468
|
+
const changed = only(at, kind, fields, allowed);
|
|
1469
|
+
const base = baseline(doc);
|
|
1470
|
+
for (const k of changed) {
|
|
1471
|
+
if (k === "main") {
|
|
1472
|
+
raw.main_characters = fields.main.split(",").map((pair) => {
|
|
1473
|
+
const [name, role] = pair.split("=").map((x) => x.trim());
|
|
1474
|
+
if (!name || !role)
|
|
1475
|
+
throw new Error(`main takes "\u540D\u5B57=\u89D2\u8272\u5B9A\u4F4D,\u2026" (got "${pair}")`);
|
|
1476
|
+
return { name, role };
|
|
1477
|
+
});
|
|
1478
|
+
} else if (k === "aliases" || k === "role") {
|
|
1479
|
+
throw new Error(`${at} is ${kind}; it has no ${k}`);
|
|
1480
|
+
} else {
|
|
1481
|
+
raw[k] = fields[k];
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
step("edit", `${kind} ${at}: ${changed.join(", ")}`);
|
|
1485
|
+
save(doc);
|
|
1486
|
+
return `${at} ${changed.join("+")}` + since(doc, base);
|
|
1487
|
+
}
|
|
1488
|
+
function editScene(doc, at, scene, fields) {
|
|
1489
|
+
const changed = only(at, "scene", fields, SCENE_FIELDS);
|
|
1490
|
+
const base = baseline(doc);
|
|
1491
|
+
const env = { ...scene.raw.environment ?? {} };
|
|
1492
|
+
if (fields.space !== void 0)
|
|
1493
|
+
env.space = oneOf("space", fields.space, SCENE_SPACE_SET, SCENE_SPACE_VALUES);
|
|
1494
|
+
if (fields.time !== void 0)
|
|
1495
|
+
env.time = oneOf("time", fields.time, SCENE_TIME_SET, SCENE_TIME_VALUES);
|
|
1496
|
+
scene.raw.environment = env;
|
|
1497
|
+
step("edit", `scene ${scene.anchor}: ${changed.join(", ")}`);
|
|
1498
|
+
save(doc);
|
|
1499
|
+
return `${scene.anchor} ${changed.join("+")} ${[env.space, env.time].filter(Boolean).join("/")}` + since(doc, base);
|
|
1500
|
+
}
|
|
1501
|
+
function editAsset(doc, at, a, fields) {
|
|
1502
|
+
const changed = only(at, "asset", fields, ASSET_FIELDS);
|
|
1503
|
+
const base = baseline(doc);
|
|
1504
|
+
if (fields.name !== void 0) {
|
|
1505
|
+
const clash = doc.assets.find((x) => x !== a && (x.name === fields.name || x.aliases.includes(fields.name)));
|
|
1506
|
+
if (clash)
|
|
1507
|
+
throw new Error(`"${fields.name}" already names ${clash.kind} ${clash.id}`);
|
|
1508
|
+
a.raw[`${a.kind}_name`] = fields.name;
|
|
1509
|
+
}
|
|
1510
|
+
if (fields.desc !== void 0)
|
|
1511
|
+
a.raw.description = fields.desc;
|
|
1512
|
+
if (fields.importance !== void 0)
|
|
1513
|
+
a.raw.importance = checkImportance(fields.importance);
|
|
1514
|
+
if (fields.role !== void 0) {
|
|
1515
|
+
if (a.kind !== "actor")
|
|
1516
|
+
throw new Error(`role only applies to actors (${a.name} is a ${a.kind})`);
|
|
1517
|
+
if (fields.role)
|
|
1518
|
+
a.raw.role_type = oneOf("role", fields.role, ROLE_TYPE_SET, ROLE_TYPES);
|
|
1519
|
+
else
|
|
1520
|
+
delete a.raw.role_type;
|
|
1521
|
+
}
|
|
1522
|
+
if (fields.aliases !== void 0) {
|
|
1523
|
+
if (fields.aliases.length)
|
|
1524
|
+
a.raw.aliases = fields.aliases;
|
|
1525
|
+
else
|
|
1526
|
+
delete a.raw.aliases;
|
|
1527
|
+
}
|
|
1528
|
+
step("edit", `asset ${a.id}: ${changed.join(", ")}`);
|
|
1529
|
+
save(doc);
|
|
1530
|
+
return `${a.id} ${changed.join("+")} ${fields.name ?? a.name}` + since(doc, base);
|
|
1531
|
+
}
|
|
1532
|
+
function editState(doc, at, a, st, fields) {
|
|
1533
|
+
const changed = only(at, "state", fields, STATE_FIELDS);
|
|
1534
|
+
const base = baseline(doc);
|
|
1535
|
+
const raw = a.raw.states.find((x) => x.state_id === st.id);
|
|
1536
|
+
if (fields.name !== void 0) {
|
|
1537
|
+
if (a.states.some((x) => x.id !== st.id && x.name === fields.name)) {
|
|
1538
|
+
throw new Error(`${a.name} already has a state named "${fields.name}"`);
|
|
1539
|
+
}
|
|
1540
|
+
raw.state_name = fields.name;
|
|
1541
|
+
}
|
|
1542
|
+
if (fields.desc !== void 0)
|
|
1543
|
+
raw.description = fields.desc;
|
|
1544
|
+
step("edit", `state ${a.id}/${st.id}: ${changed.join(", ")}`);
|
|
1545
|
+
save(doc);
|
|
1546
|
+
return `${a.id}/${st.id} ${changed.join("+")} ${fields.name ?? st.name}` + since(doc, base);
|
|
1547
|
+
}
|
|
1548
|
+
function insert(doc, at, where, type, content, speaker, paren) {
|
|
1549
|
+
const node = resolve(doc, at);
|
|
1550
|
+
if (node.t !== "action")
|
|
1551
|
+
throw new Error(`${at} is a ${node.t}; insert needs a sibling action`);
|
|
1552
|
+
const anchor = node.action;
|
|
1553
|
+
const base = baseline(doc);
|
|
1554
|
+
const raw = { type: oneOf("type", type, ACTION_TYPE_SET, ACTION_TYPES), content };
|
|
1555
|
+
if (doc.version === 4)
|
|
1556
|
+
raw.action_id = newId("ac", new Set(doc.actions.keys()));
|
|
1557
|
+
if (type === "dialogue" || type === "inner_thought") {
|
|
1558
|
+
if (!speaker)
|
|
1559
|
+
throw new Error(`a ${type} needs a speaker`);
|
|
1560
|
+
raw.speaker = speakerJson(doc, speaker);
|
|
1561
|
+
if (paren !== void 0)
|
|
1562
|
+
setParen(doc, raw, paren);
|
|
1563
|
+
} else if (speaker || paren) {
|
|
1564
|
+
throw new Error(`a ${type} takes neither speaker nor paren`);
|
|
1565
|
+
}
|
|
1566
|
+
const list = anchor.scene.raw.actions;
|
|
1567
|
+
const index = list.indexOf(anchor.raw) + (where === "before" ? 0 : 1);
|
|
1568
|
+
list.splice(index, 0, raw);
|
|
1569
|
+
step("insert", `${raw.action_id ?? `${anchor.scene.anchor}#${index}`} ${where} ${anchor.anchor}`);
|
|
1570
|
+
save(doc);
|
|
1571
|
+
return `${raw.action_id ?? `${anchor.scene.anchor}#${index}`} ${where} ${anchor.anchor} ${preview(raw)}` + since(doc, base);
|
|
1572
|
+
}
|
|
1573
|
+
function move(doc, at, to, where = "after") {
|
|
1574
|
+
const src = resolve(doc, at);
|
|
1575
|
+
if (src.t !== "action")
|
|
1576
|
+
throw new Error(`${at} is a ${src.t}; move targets one action`);
|
|
1577
|
+
const dst = resolve(doc, to);
|
|
1578
|
+
if (dst.t !== "action")
|
|
1579
|
+
throw new Error(`${to} is a ${dst.t}; move --to takes a sibling action`);
|
|
1580
|
+
if (src.action === dst.action)
|
|
1581
|
+
throw new Error(`${at} cannot move relative to itself`);
|
|
1582
|
+
const base = baseline(doc);
|
|
1583
|
+
const from = src.action.scene.raw.actions;
|
|
1584
|
+
from.splice(from.indexOf(src.action.raw), 1);
|
|
1585
|
+
const into = dst.action.scene.raw.actions;
|
|
1586
|
+
into.splice(into.indexOf(dst.action.raw) + (where === "before" ? 0 : 1), 0, src.action.raw);
|
|
1587
|
+
step("move", `${src.action.scene.anchor} -> ${dst.action.scene.anchor}, identity ${src.action.anchor} preserved`);
|
|
1588
|
+
save(doc);
|
|
1589
|
+
return `${src.action.anchor} ${where} ${dst.action.anchor} (${src.action.scene.anchor} -> ${dst.action.scene.anchor})` + since(doc, base);
|
|
1590
|
+
}
|
|
1591
|
+
function setParen(doc, raw, v) {
|
|
1592
|
+
const key = doc.version === 4 ? "parenthetical" : "emotion";
|
|
1593
|
+
if (v === null)
|
|
1594
|
+
delete raw[key];
|
|
1595
|
+
else
|
|
1596
|
+
raw[key] = v;
|
|
1597
|
+
}
|
|
1598
|
+
function setTs(raw, v) {
|
|
1599
|
+
if (v === null) {
|
|
1600
|
+
if (raw.extend)
|
|
1601
|
+
delete raw.extend.timestamp;
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
let canonical;
|
|
1605
|
+
try {
|
|
1606
|
+
canonical = parseTimestamp(v).canonical;
|
|
1607
|
+
} catch (error) {
|
|
1608
|
+
if (!(error instanceof TimestampParseError))
|
|
1609
|
+
throw error;
|
|
1610
|
+
throw new Error(`${error.message} (got ${JSON.stringify(v)})`);
|
|
1611
|
+
}
|
|
1612
|
+
raw.extend = { ...raw.extend ?? {}, timestamp: canonical };
|
|
1613
|
+
}
|
|
1614
|
+
function speakerJson(doc, name) {
|
|
1615
|
+
const whole = doc.assets.find((a) => a.name === name || a.aliases.includes(name));
|
|
1616
|
+
const [base, qual] = whole ? [name, void 0] : splitOnce(name, "\xB7");
|
|
1617
|
+
const kind = NON_ACTOR_KIND[base];
|
|
1618
|
+
if (kind)
|
|
1619
|
+
return qual ? { kind, label: qual } : { kind };
|
|
1620
|
+
const n = resolve(doc, base);
|
|
1621
|
+
if (n.t !== "asset" || n.asset.kind !== "actor")
|
|
1622
|
+
throw new Error(`Not an actor: ${name}`);
|
|
1623
|
+
const out = { actor_id: n.asset.id };
|
|
1624
|
+
if (qual) {
|
|
1625
|
+
const st = n.asset.states.find((s) => s.name === qual || s.id === qual);
|
|
1626
|
+
if (!st)
|
|
1627
|
+
throw new Error(`Unknown state "${qual}" on ${n.asset.name}`);
|
|
1628
|
+
out.state_id = st.id;
|
|
1629
|
+
}
|
|
1630
|
+
return out;
|
|
1631
|
+
}
|
|
1632
|
+
function splitOnce(v, sep) {
|
|
1633
|
+
const i = v.indexOf(sep);
|
|
1634
|
+
return i < 0 ? [v, void 0] : [v.slice(0, i), v.slice(i + sep.length)];
|
|
1635
|
+
}
|
|
1636
|
+
function preview(raw) {
|
|
1637
|
+
return clip(String(raw.content ?? ""), 40);
|
|
1638
|
+
}
|
|
1639
|
+
function ref(doc, sceneAddr, assetAddr, states, remove = false) {
|
|
1640
|
+
const node = resolve(doc, sceneAddr);
|
|
1641
|
+
if (node.t !== "scene")
|
|
1642
|
+
throw new Error(`${sceneAddr} is a ${node.t}; ref targets one scene`);
|
|
1643
|
+
const scene = node.scene;
|
|
1644
|
+
const target = resolve(doc, assetAddr);
|
|
1645
|
+
if (target.t !== "asset")
|
|
1646
|
+
throw new Error(`${assetAddr} is a ${target.t}; ref takes an actor, location or prop`);
|
|
1647
|
+
const a = target.asset;
|
|
1648
|
+
const field = a.kind === "actor" ? "actors" : a.kind === "prop" ? "props" : "location";
|
|
1649
|
+
const base = baseline(doc);
|
|
1650
|
+
if (remove) {
|
|
1651
|
+
if (a.kind === "location") {
|
|
1652
|
+
if (scene.location?.id !== a.id) {
|
|
1653
|
+
throw new Error(`${scene.anchor}'s location is ${scene.location ? assetName(doc, scene.location.id) : "empty"}, not ${a.name}`);
|
|
1654
|
+
}
|
|
1655
|
+
scene.raw.location = null;
|
|
1656
|
+
} else {
|
|
1657
|
+
const list = scene.raw[field];
|
|
1658
|
+
const i = list.findIndex((r) => r[`${a.kind}_id`] === a.id);
|
|
1659
|
+
if (i < 0)
|
|
1660
|
+
throw new Error(`${scene.anchor} does not reference ${a.name}`);
|
|
1661
|
+
list.splice(i, 1);
|
|
1662
|
+
}
|
|
1663
|
+
step("ref", `${scene.anchor} -= ${a.id}`);
|
|
1664
|
+
save(doc);
|
|
1665
|
+
return `${scene.anchor} removed ${a.name}` + since(doc, base);
|
|
1666
|
+
}
|
|
1667
|
+
if (!states.length) {
|
|
1668
|
+
throw new Error(`ref needs at least one state for ${a.name} (has: ${a.states.map((st) => st.name).join(", ")}) \u2014 nothing is implied, not even default`);
|
|
1669
|
+
}
|
|
1670
|
+
const ids = [...new Set(states.map((sn) => {
|
|
1671
|
+
const hit = a.states.filter((st) => st.name === sn || st.id === sn);
|
|
1672
|
+
if (!hit.length) {
|
|
1673
|
+
throw new Error(`${a.name} has no state "${sn}" (has: ${a.states.map((x) => x.name).join(", ")})`);
|
|
1674
|
+
}
|
|
1675
|
+
if (hit.length > 1)
|
|
1676
|
+
throw new Error(`${a.name} has ${hit.length} states named "${sn}"; use the state id`);
|
|
1677
|
+
return hit[0].id;
|
|
1678
|
+
}))].sort();
|
|
1679
|
+
const key = `${a.kind}_id`;
|
|
1680
|
+
const entry = doc.version === 4 ? { [key]: a.id, state_ids: ids } : { [key]: a.id, state_id: ids[0] };
|
|
1681
|
+
if (doc.version === 3 && ids.length > 1) {
|
|
1682
|
+
throw new Error(`Script v3 holds one state per reference; pass a single state`);
|
|
1683
|
+
}
|
|
1684
|
+
let verb;
|
|
1685
|
+
if (a.kind === "location") {
|
|
1686
|
+
verb = scene.location ? scene.location.id === a.id ? "restated" : "replaced" : "set";
|
|
1687
|
+
scene.raw.location = entry;
|
|
1688
|
+
} else {
|
|
1689
|
+
const list = scene.raw[field];
|
|
1690
|
+
const i = list.findIndex((r) => r[key] === a.id);
|
|
1691
|
+
if (i < 0) {
|
|
1692
|
+
list.push(entry);
|
|
1693
|
+
verb = "added";
|
|
1694
|
+
} else {
|
|
1695
|
+
list[i] = entry;
|
|
1696
|
+
verb = "restated";
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
step("ref", `${scene.anchor} ${verb} ${a.id} = [${ids.join(", ")}]`);
|
|
1700
|
+
save(doc);
|
|
1701
|
+
return `${scene.anchor} ${verb} ${a.name}(${ids.map((i2) => stateName(doc, a.id, i2)).join("+")})` + since(doc, base);
|
|
1702
|
+
}
|
|
1703
|
+
function replace(doc, query, to, opts = {}) {
|
|
1704
|
+
if (!query)
|
|
1705
|
+
throw new Error("replace needs a pattern; an empty one would rewrite every action");
|
|
1706
|
+
const re = new RegExp(query, "gu");
|
|
1707
|
+
const base = baseline(doc);
|
|
1708
|
+
const pool2 = filterActions(doc, opts);
|
|
1709
|
+
const hits = pool2.filter((a) => {
|
|
1710
|
+
re.lastIndex = 0;
|
|
1711
|
+
return re.test(a.content);
|
|
1712
|
+
});
|
|
1713
|
+
step("replace", `/${query}/ matched ${hits.length} of ${pool2.length} actions in scope`);
|
|
1714
|
+
if (!hits.length)
|
|
1715
|
+
return `0 hits for /${query}/ \u2014 nothing to do`;
|
|
1716
|
+
const gutter = Math.max(...hits.map((a) => a.anchor.length));
|
|
1717
|
+
const lines = hits.map((a) => {
|
|
1718
|
+
const next = a.content.replace(re, to);
|
|
1719
|
+
return `${a.anchor.padEnd(gutter)} ${a.scene.ep.no}-${a.scene.no}-${a.no}
|
|
1720
|
+
${" ".repeat(gutter)} - ${a.content}
|
|
1721
|
+
${" ".repeat(gutter)} + ${next}`;
|
|
1722
|
+
});
|
|
1723
|
+
if (!opts.apply) {
|
|
1724
|
+
return `${hits.length} action(s) would change (dry run \u2014 pass apply to write)
|
|
1725
|
+
` + lines.join("\n");
|
|
1726
|
+
}
|
|
1727
|
+
for (const a of hits) {
|
|
1728
|
+
re.lastIndex = 0;
|
|
1729
|
+
a.raw.content = a.content.replace(re, to);
|
|
1730
|
+
}
|
|
1731
|
+
save(doc);
|
|
1732
|
+
step("replace", `wrote ${hits.length} actions`);
|
|
1733
|
+
return `${hits.length} action(s) changed` + since(doc, base) + "\n" + lines.join("\n");
|
|
1734
|
+
}
|
|
1735
|
+
function oneOf(what, v, set, allowed) {
|
|
1736
|
+
if (!set.has(v))
|
|
1737
|
+
throw new Error(`${what} must be one of ${allowed.join(" | ")} (got "${v}")`);
|
|
1738
|
+
return v;
|
|
1739
|
+
}
|
|
1740
|
+
function checkImportance(v) {
|
|
1741
|
+
return oneOf("importance", v, IMPORTANCE_TIER_SET, IMPORTANCE_TIERS);
|
|
1742
|
+
}
|
|
1743
|
+
function validateDoc(doc) {
|
|
1744
|
+
return doc.version === 4 ? validateScriptV4(doc.raw) : validateScriptV3(doc.raw);
|
|
1745
|
+
}
|
|
1746
|
+
function validate(doc) {
|
|
1747
|
+
const r = validateDoc(doc);
|
|
1748
|
+
const head = `${doc.episodes.length} episodes / ${doc.scenes.size ? [...new Set(doc.episodes.flatMap((e) => e.scenes))].length : 0} scenes / ${allActions(doc).length} actions \u2014 ${r.ok ? "ok" : `${r.errors.length} error(s)`}` + (r.warnings.length ? `, ${r.warnings.length} warning(s)` : "");
|
|
1749
|
+
return [head, ...fmtIssues([
|
|
1750
|
+
...r.errors.map((i) => ["error", i]),
|
|
1751
|
+
...r.warnings.map((i) => ["warning", i])
|
|
1752
|
+
])].join("\n");
|
|
1753
|
+
}
|
|
1754
|
+
function issues(doc, opts = {}) {
|
|
1755
|
+
const r = validateDoc(doc);
|
|
1756
|
+
let rows = [
|
|
1757
|
+
...r.errors.map((i) => ["error", i]),
|
|
1758
|
+
...r.warnings.map((i) => ["warning", i])
|
|
1759
|
+
];
|
|
1760
|
+
if (opts.severity)
|
|
1761
|
+
rows = rows.filter(([s]) => s === opts.severity);
|
|
1762
|
+
if (opts.code)
|
|
1763
|
+
rows = rows.filter(([, i]) => i.code === opts.code);
|
|
1764
|
+
step("issues", `${rows.length} matching issue(s)`);
|
|
1765
|
+
return rows.length ? fmtIssues(rows).join("\n") : "No issues.";
|
|
1766
|
+
}
|
|
1767
|
+
function fmtIssues(rows) {
|
|
1768
|
+
return rows.map(([sev, i]) => `${sev} ${i.code} ${i.path} ${i.message}` + (i.repair_hint ? `
|
|
1769
|
+
fix: ${i.repair_hint}` : ""));
|
|
1770
|
+
}
|
|
1771
|
+
function baseline(doc) {
|
|
1772
|
+
return validateDoc(doc);
|
|
1773
|
+
}
|
|
1774
|
+
function since(doc, base) {
|
|
1775
|
+
const now = validateDoc(doc);
|
|
1776
|
+
const fresh = [
|
|
1777
|
+
...issuesIntroduced(base.errors, now.errors).map((i) => ["error", i]),
|
|
1778
|
+
...issuesIntroduced(base.warnings, now.warnings).map((i) => ["warning", i])
|
|
1779
|
+
];
|
|
1780
|
+
if (!fresh.length)
|
|
1781
|
+
return "";
|
|
1782
|
+
step("check", `${fresh.length} issue(s) introduced by this edit`);
|
|
1783
|
+
return `
|
|
1784
|
+
introduced ${fresh.length} issue(s):
|
|
1785
|
+
` + fmtIssues(fresh).join("\n ");
|
|
1786
|
+
}
|
|
1787
|
+
var ASSET_FIELD = { actor: "actors", location: "locations", prop: "props" };
|
|
1788
|
+
function state(doc, assetName2, name, desc) {
|
|
1789
|
+
const n = resolve(doc, assetName2);
|
|
1790
|
+
if (n.t !== "asset")
|
|
1791
|
+
throw new Error(`${assetName2} is a ${n.t}; state targets an asset`);
|
|
1792
|
+
const base = baseline(doc);
|
|
1793
|
+
if (n.asset.states.some((s) => s.name === name)) {
|
|
1794
|
+
throw new Error(`${n.asset.name} already has a state named "${name}"`);
|
|
1795
|
+
}
|
|
1796
|
+
const id = doc.version === 4 ? newId("st", new Set(n.asset.states.map((s) => s.id))) : `st_${String(n.asset.states.length).padStart(3, "0")}`;
|
|
1797
|
+
n.asset.raw.states.push({ state_id: id, state_name: name, description: desc });
|
|
1798
|
+
step("state", `${n.asset.id} + ${id} "${name}"`);
|
|
1799
|
+
save(doc);
|
|
1800
|
+
return `${n.asset.id}/${id} ${name} (${n.asset.name} now has ${n.asset.states.length + 1} states)` + since(doc, base);
|
|
1801
|
+
}
|
|
1802
|
+
function del(doc, addr, opts = {}) {
|
|
1803
|
+
const n = resolve(doc, addr);
|
|
1804
|
+
const base = baseline(doc);
|
|
1805
|
+
if (n.t === "action") {
|
|
1806
|
+
const list = n.action.scene.raw.actions;
|
|
1807
|
+
list.splice(list.indexOf(n.action.raw), 1);
|
|
1808
|
+
step("del", `action ${n.action.anchor} from ${n.action.scene.anchor}`);
|
|
1809
|
+
save(doc);
|
|
1810
|
+
return `deleted ${n.action.anchor} ${preview(n.action.raw)}` + since(doc, base);
|
|
1811
|
+
}
|
|
1812
|
+
if (n.t === "scene") {
|
|
1813
|
+
if (n.scene.actions.length && !opts.force) {
|
|
1814
|
+
throw new Error(`${n.scene.anchor} still holds ${n.scene.actions.length} action(s) \u2014 pass --force to delete the scene and everything in it`);
|
|
1815
|
+
}
|
|
1816
|
+
const list = n.scene.ep.raw.scenes;
|
|
1817
|
+
list.splice(list.indexOf(n.scene.raw), 1);
|
|
1818
|
+
step("del", `scene ${n.scene.anchor} (${n.scene.actions.length} actions)`);
|
|
1819
|
+
save(doc);
|
|
1820
|
+
return `deleted scene ${n.scene.anchor} with ${n.scene.actions.length} action(s)` + since(doc, base);
|
|
1821
|
+
}
|
|
1822
|
+
if (n.t === "ep") {
|
|
1823
|
+
const acts = n.ep.scenes.reduce((m, s) => m + s.actions.length, 0);
|
|
1824
|
+
if (n.ep.scenes.length && !opts.force) {
|
|
1825
|
+
throw new Error(`episode ${n.ep.no} still holds ${n.ep.scenes.length} scene(s) / ${acts} action(s) \u2014 pass --force to delete the episode and everything in it`);
|
|
1826
|
+
}
|
|
1827
|
+
const list = doc.raw.episodes;
|
|
1828
|
+
list.splice(list.indexOf(n.ep.raw), 1);
|
|
1829
|
+
step("del", `episode ${n.ep.no}`);
|
|
1830
|
+
save(doc);
|
|
1831
|
+
return `deleted episode ${n.ep.no} (${n.ep.id}) with ${n.ep.scenes.length} scene(s) / ${acts} action(s)`;
|
|
1832
|
+
}
|
|
1833
|
+
if (n.t === "asset")
|
|
1834
|
+
return delAsset(doc, n.asset, opts) + since(doc, base);
|
|
1835
|
+
if (n.t === "state")
|
|
1836
|
+
return delState(doc, n.asset, n.state, opts) + since(doc, base);
|
|
1837
|
+
throw new Error(`del takes an action, scene, episode, asset or state (got ${n.t})`);
|
|
1838
|
+
}
|
|
1839
|
+
function assetUses(doc, id) {
|
|
1840
|
+
const scenes = [];
|
|
1841
|
+
for (const ep of doc.episodes)
|
|
1842
|
+
for (const s of ep.scenes) {
|
|
1843
|
+
if ([s.location, ...s.actors, ...s.props].some((r) => r && r.id === id))
|
|
1844
|
+
scenes.push(s);
|
|
1845
|
+
}
|
|
1846
|
+
return { scenes, speakers: allActions(doc).filter((a) => a.speaker?.id === id) };
|
|
1847
|
+
}
|
|
1848
|
+
function delAsset(doc, a, opts) {
|
|
1849
|
+
const { scenes, speakers } = assetUses(doc, a.id);
|
|
1850
|
+
const used = scenes.length + speakers.length;
|
|
1851
|
+
if (used && !opts.replacement && !opts.force) {
|
|
1852
|
+
throw new Error(`${a.name} is still referenced by ${scenes.length} scene(s) and ${speakers.length} speaker(s) \u2014 merge it into another asset with \`merge ${a.id} <other>\`, or pass --force to strip every reference`);
|
|
1853
|
+
}
|
|
1854
|
+
if (opts.replacement)
|
|
1855
|
+
return merge(doc, a.id, opts.replacement);
|
|
1856
|
+
for (const s of scenes) {
|
|
1857
|
+
if (s.raw.location?.[`${a.kind}_id`] === a.id)
|
|
1858
|
+
s.raw.location = null;
|
|
1859
|
+
for (const f of ["actors", "props"]) {
|
|
1860
|
+
const list2 = s.raw[f];
|
|
1861
|
+
if (!list2)
|
|
1862
|
+
continue;
|
|
1863
|
+
const i = list2.findIndex((r) => r[`${a.kind}_id`] === a.id);
|
|
1864
|
+
if (i >= 0)
|
|
1865
|
+
list2.splice(i, 1);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
for (const act of speakers)
|
|
1869
|
+
delete act.raw.speaker;
|
|
1870
|
+
const field = ASSET_FIELD[a.kind];
|
|
1871
|
+
const list = doc.raw[field];
|
|
1872
|
+
list.splice(list.indexOf(a.raw), 1);
|
|
1873
|
+
step("del", `asset ${a.id}, stripped ${scenes.length} scene refs + ${speakers.length} speakers`);
|
|
1874
|
+
save(doc);
|
|
1875
|
+
return `deleted ${a.kind} ${a.id} ${a.name}` + (used ? `
|
|
1876
|
+
stripped ${scenes.length} scene reference(s) and ${speakers.length} speaker(s)` : "");
|
|
1877
|
+
}
|
|
1878
|
+
function delState(doc, a, st, opts) {
|
|
1879
|
+
if (st.id === "default")
|
|
1880
|
+
throw new Error(`every asset keeps its default state; delete the asset instead`);
|
|
1881
|
+
const scenes = doc.episodes.flatMap((e) => e.scenes).filter((s) => [s.location, ...s.actors, ...s.props].some((r) => r && r.id === a.id && r.states.includes(st.id)));
|
|
1882
|
+
const speakers = allActions(doc).filter((x) => x.speaker?.id === a.id && x.speaker.state === st.id);
|
|
1883
|
+
const used = scenes.length + speakers.length;
|
|
1884
|
+
let rewired = "";
|
|
1885
|
+
if (used) {
|
|
1886
|
+
if (!opts.replacement && !opts.force) {
|
|
1887
|
+
throw new Error(`${a.name}/${st.name} is bound in ${scenes.length} scene(s) and named by ${speakers.length} speaker(s) \u2014 pass --replacement <state> to rewire them, or --force to drop the bindings`);
|
|
1888
|
+
}
|
|
1889
|
+
let to;
|
|
1890
|
+
if (opts.replacement) {
|
|
1891
|
+
const hit = a.states.filter((x) => x.id === opts.replacement || x.name === opts.replacement);
|
|
1892
|
+
if (hit.length !== 1)
|
|
1893
|
+
throw new Error(`${a.name} has no single state "${opts.replacement}"`);
|
|
1894
|
+
if (hit[0].id === st.id)
|
|
1895
|
+
throw new Error(`replacement must differ from the state being deleted`);
|
|
1896
|
+
to = hit[0].id;
|
|
1897
|
+
}
|
|
1898
|
+
for (const s of scenes) {
|
|
1899
|
+
for (const r of [s.raw.location, ...s.raw.actors ?? [], ...s.raw.props ?? []]) {
|
|
1900
|
+
if (!r || r[`${a.kind}_id`] !== a.id)
|
|
1901
|
+
continue;
|
|
1902
|
+
if (doc.version === 4) {
|
|
1903
|
+
const next = new Set(r.state_ids.filter((x) => x !== st.id));
|
|
1904
|
+
if (to)
|
|
1905
|
+
next.add(to);
|
|
1906
|
+
if (!next.size)
|
|
1907
|
+
next.add("default");
|
|
1908
|
+
r.state_ids = [...next].sort();
|
|
1909
|
+
} else
|
|
1910
|
+
r[`state_id`] = to ?? "default";
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
for (const x of speakers) {
|
|
1914
|
+
if (to)
|
|
1915
|
+
x.raw.speaker.state_id = to;
|
|
1916
|
+
else
|
|
1917
|
+
delete x.raw.speaker.state_id;
|
|
1918
|
+
}
|
|
1919
|
+
rewired = `
|
|
1920
|
+
${to ? `rewired` : `cleared`} ${scenes.length} scene binding(s) and ${speakers.length} speaker(s)${to ? ` to ${stateName(doc, a.id, to)}` : ""}`;
|
|
1921
|
+
}
|
|
1922
|
+
const list = a.raw.states;
|
|
1923
|
+
list.splice(list.findIndex((x) => x.state_id === st.id), 1);
|
|
1924
|
+
step("del", `state ${a.id}/${st.id}`);
|
|
1925
|
+
save(doc);
|
|
1926
|
+
return `deleted state ${a.id}/${st.id} ${st.name}` + rewired;
|
|
1927
|
+
}
|
|
1928
|
+
function merge(doc, srcAddr, dstAddr) {
|
|
1929
|
+
const s = resolve(doc, srcAddr), d = resolve(doc, dstAddr);
|
|
1930
|
+
if (s.t !== "asset" || d.t !== "asset")
|
|
1931
|
+
throw new Error(`merge takes two assets`);
|
|
1932
|
+
if (s.asset === d.asset)
|
|
1933
|
+
throw new Error(`cannot merge ${s.asset.name} into itself`);
|
|
1934
|
+
if (s.asset.kind !== d.asset.kind) {
|
|
1935
|
+
throw new Error(`cannot merge a ${s.asset.kind} into a ${d.asset.kind}`);
|
|
1936
|
+
}
|
|
1937
|
+
const base = baseline(doc);
|
|
1938
|
+
const src = s.asset, dst = d.asset;
|
|
1939
|
+
const kindKey = `${src.kind}_id`;
|
|
1940
|
+
const taken = new Set(dst.states.map((x) => x.id));
|
|
1941
|
+
const map = /* @__PURE__ */ new Map();
|
|
1942
|
+
for (const st of src.states) {
|
|
1943
|
+
const same = dst.states.find((x) => x.name === st.name);
|
|
1944
|
+
if (same) {
|
|
1945
|
+
map.set(st.id, same.id);
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1948
|
+
if (st.id === "default") {
|
|
1949
|
+
map.set(st.id, "default");
|
|
1950
|
+
continue;
|
|
1951
|
+
}
|
|
1952
|
+
const id = doc.version === 4 ? newId("st", taken) : `st_${String(dst.states.length + map.size).padStart(3, "0")}`;
|
|
1953
|
+
dst.raw.states.push({ state_id: id, state_name: st.name, description: st.desc });
|
|
1954
|
+
map.set(st.id, id);
|
|
1955
|
+
}
|
|
1956
|
+
const { scenes, speakers } = assetUses(doc, src.id);
|
|
1957
|
+
for (const scene of scenes) {
|
|
1958
|
+
for (const field of ["location", "actors", "props"]) {
|
|
1959
|
+
const slot = scene.raw[field];
|
|
1960
|
+
const list2 = field === "location" ? slot ? [slot] : [] : slot ?? [];
|
|
1961
|
+
for (const r of list2) {
|
|
1962
|
+
if (r[kindKey] !== src.id)
|
|
1963
|
+
continue;
|
|
1964
|
+
r[kindKey] = dst.id;
|
|
1965
|
+
if (doc.version === 4) {
|
|
1966
|
+
r.state_ids = [...new Set(r.state_ids.map((x) => map.get(x) ?? "default"))].sort();
|
|
1967
|
+
} else
|
|
1968
|
+
r.state_id = map.get(r.state_id) ?? "default";
|
|
1969
|
+
}
|
|
1970
|
+
if (field !== "location" && Array.isArray(slot)) {
|
|
1971
|
+
const merged = [];
|
|
1972
|
+
for (const r of slot) {
|
|
1973
|
+
const prev = merged.find((x) => x[kindKey] === r[kindKey]);
|
|
1974
|
+
if (!prev) {
|
|
1975
|
+
merged.push(r);
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
if (doc.version === 4) {
|
|
1979
|
+
prev.state_ids = [.../* @__PURE__ */ new Set([...prev.state_ids, ...r.state_ids])].sort();
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
scene.raw[field] = merged;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
for (const a of speakers) {
|
|
1987
|
+
a.raw.speaker.actor_id = dst.id;
|
|
1988
|
+
if (a.raw.speaker.state_id)
|
|
1989
|
+
a.raw.speaker.state_id = map.get(a.raw.speaker.state_id) ?? "default";
|
|
1990
|
+
}
|
|
1991
|
+
const list = doc.raw[ASSET_FIELD[src.kind]];
|
|
1992
|
+
list.splice(list.indexOf(src.raw), 1);
|
|
1993
|
+
step("merge", `${src.id} -> ${dst.id}: ${scenes.length} scenes, ${speakers.length} speakers`);
|
|
1994
|
+
save(doc);
|
|
1995
|
+
return `merged ${src.id} ${src.name} into ${dst.id} ${dst.name}
|
|
1996
|
+
moved ${scenes.length} scene reference(s), ${speakers.length} speaker(s), ${[...map].filter(([k, v]) => k !== v).length} state mapping(s)` + since(doc, base);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// packages/review-agent/src/finding.ts
|
|
2000
|
+
import { z } from "zod";
|
|
2001
|
+
var CATEGORIES = [
|
|
2002
|
+
"scene-reference",
|
|
2003
|
+
"actor-identity",
|
|
2004
|
+
"wardrobe",
|
|
2005
|
+
"asset",
|
|
2006
|
+
"dialogue-action",
|
|
2007
|
+
"scene-boundary",
|
|
2008
|
+
"timeline",
|
|
2009
|
+
"other"
|
|
2010
|
+
];
|
|
2011
|
+
var OUTCOMES = ["fixed", "unchanged", "unresolved"];
|
|
2012
|
+
var CONFIDENCES = ["high", "medium", "low"];
|
|
2013
|
+
var SUBJECT_KINDS = ["actor", "state", "location", "prop", "action", "scene"];
|
|
2014
|
+
var EVIDENCE_KINDS = ["scriptctl", "video", "gemini", "frame", "structure"];
|
|
2015
|
+
var FINDING_INPUT = z.object({
|
|
2016
|
+
category: z.enum(CATEGORIES),
|
|
2017
|
+
title: z.string().min(1).describe("\u4E00\u53E5\u8BDD\u6807\u9898"),
|
|
2018
|
+
issue: z.string().min(1).describe("\u95EE\u9898\u662F\u4EC0\u4E48"),
|
|
2019
|
+
impact: z.string().min(1).describe("\u4E0D\u4FEE\u4F1A\u5F71\u54CD\u4EC0\u4E48"),
|
|
2020
|
+
resolution: z.string().min(1).describe("\u505A\u4E86\u4EC0\u4E48\uFF0C\u6216\u4E3A\u4EC0\u4E48\u4E0D\u505A"),
|
|
2021
|
+
outcome: z.enum(OUTCOMES),
|
|
2022
|
+
confidence: z.enum(CONFIDENCES),
|
|
2023
|
+
at: z.string().min(1).describe("\u5B9A\u4F4D\u5730\u5740\uFF1A\u96C6 `3`\u3001\u573A `3-2`\u3001\u5355\u6761 `3-2-5`\u3001\u6B63\u5F0F ID\u3001\u6216 `<\u8D44\u4EA7>/<\u72B6\u6001>`"),
|
|
2024
|
+
subject: z.string().optional().describe("\u6D89\u53CA\u7684\u8D44\u4EA7\u540D\u6216 ID\uFF0C\u4F8B\u5982 `\u5411\u5317` \u6216 `actQYEeTr/default`"),
|
|
2025
|
+
before: z.string().optional(),
|
|
2026
|
+
after: z.string().optional(),
|
|
2027
|
+
unresolved_reason: z.string().optional().describe("outcome=unresolved \u65F6\u5FC5\u586B"),
|
|
2028
|
+
evidence: z.array(z.string()).optional().describe("\u6BCF\u6761\u5199\u6210 `kind|label|detail`\uFF0Ckind \u2208 scriptctl|video|gemini|frame|structure")
|
|
2029
|
+
});
|
|
2030
|
+
var EMPTY_ANCHOR = (episode) => ({
|
|
2031
|
+
episode_number: episode,
|
|
2032
|
+
episode_id: null,
|
|
2033
|
+
scene_number: null,
|
|
2034
|
+
scene_id: null,
|
|
2035
|
+
action_id: null,
|
|
2036
|
+
timestamp: null,
|
|
2037
|
+
end_timestamp: null
|
|
2038
|
+
});
|
|
2039
|
+
function anchorOf(doc, at, episode) {
|
|
2040
|
+
const anchor = EMPTY_ANCHOR(episode);
|
|
2041
|
+
let node;
|
|
2042
|
+
try {
|
|
2043
|
+
node = resolve(doc, at);
|
|
2044
|
+
} catch {
|
|
2045
|
+
return anchor;
|
|
2046
|
+
}
|
|
2047
|
+
if (node.t === "ep") {
|
|
2048
|
+
return { ...anchor, episode_number: node.ep.no, episode_id: node.ep.id };
|
|
2049
|
+
}
|
|
2050
|
+
if (node.t === "scene") {
|
|
2051
|
+
const first = node.scene.actions.find((a) => a.ts);
|
|
2052
|
+
const last = [...node.scene.actions].reverse().find((a) => a.ts);
|
|
2053
|
+
return {
|
|
2054
|
+
...anchor,
|
|
2055
|
+
episode_number: node.scene.ep.no,
|
|
2056
|
+
episode_id: node.scene.ep.id,
|
|
2057
|
+
scene_number: node.scene.no,
|
|
2058
|
+
scene_id: node.scene.id,
|
|
2059
|
+
timestamp: first?.ts ?? null,
|
|
2060
|
+
end_timestamp: last?.ts ?? null
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
if (node.t === "action") {
|
|
2064
|
+
const a = node.action;
|
|
2065
|
+
return {
|
|
2066
|
+
...anchor,
|
|
2067
|
+
episode_number: a.scene.ep.no,
|
|
2068
|
+
episode_id: a.scene.ep.id,
|
|
2069
|
+
scene_number: a.scene.no,
|
|
2070
|
+
scene_id: a.scene.id,
|
|
2071
|
+
action_id: doc.version === 4 ? a.anchor : null,
|
|
2072
|
+
timestamp: a.ts ?? null,
|
|
2073
|
+
end_timestamp: null
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
return anchor;
|
|
2077
|
+
}
|
|
2078
|
+
function subjectOf(doc, raw) {
|
|
2079
|
+
if (!raw?.trim()) return null;
|
|
2080
|
+
try {
|
|
2081
|
+
const node = resolve(doc, raw);
|
|
2082
|
+
if (node.t === "asset") return { kind: node.asset.kind, id: node.asset.id, name: node.asset.name };
|
|
2083
|
+
if (node.t === "state") {
|
|
2084
|
+
return { kind: "state", id: `${node.asset.id}/${node.state.id}`, name: node.state.name };
|
|
2085
|
+
}
|
|
2086
|
+
if (node.t === "action") return { kind: "action", id: node.action.anchor, name: raw };
|
|
2087
|
+
if (node.t === "scene") return { kind: "scene", id: node.scene.id, name: raw };
|
|
2088
|
+
} catch {
|
|
2089
|
+
}
|
|
2090
|
+
return { kind: "actor", id: null, name: raw };
|
|
2091
|
+
}
|
|
2092
|
+
function evidenceOf(lines) {
|
|
2093
|
+
return (lines ?? []).map((line) => {
|
|
2094
|
+
const [rawKind, label, ...rest] = line.split("|");
|
|
2095
|
+
const kind = EVIDENCE_KINDS.includes(rawKind?.trim()) ? rawKind.trim() : "structure";
|
|
2096
|
+
return {
|
|
2097
|
+
kind,
|
|
2098
|
+
label: (label ?? rawKind ?? "").trim() || "evidence",
|
|
2099
|
+
detail: rest.join("|").trim() || (label ?? "").trim(),
|
|
2100
|
+
timestamp: null,
|
|
2101
|
+
end_timestamp: null,
|
|
2102
|
+
evidence_id: null
|
|
2103
|
+
};
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
function toFinding(doc, input, episode, seq) {
|
|
2107
|
+
return {
|
|
2108
|
+
id: `episode-${episode}-${String(seq).padStart(3, "0")}-${input.category}`,
|
|
2109
|
+
category: input.category,
|
|
2110
|
+
title: input.title,
|
|
2111
|
+
issue: input.issue,
|
|
2112
|
+
impact: input.impact,
|
|
2113
|
+
resolution: input.resolution,
|
|
2114
|
+
outcome: input.outcome,
|
|
2115
|
+
confidence: input.confidence,
|
|
2116
|
+
anchor: anchorOf(doc, input.at, episode),
|
|
2117
|
+
subject: subjectOf(doc, input.subject),
|
|
2118
|
+
before: input.before ?? null,
|
|
2119
|
+
after: input.after ?? null,
|
|
2120
|
+
unresolved_reason: input.unresolved_reason ?? null,
|
|
2121
|
+
evidence: evidenceOf(input.evidence)
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// packages/review-agent/src/media/clip.ts
|
|
2126
|
+
import { spawn } from "node:child_process";
|
|
2127
|
+
import * as fs2 from "node:fs";
|
|
2128
|
+
import * as os from "node:os";
|
|
2129
|
+
import * as path from "node:path";
|
|
2130
|
+
var CLIP_FPS = 2;
|
|
2131
|
+
var MAX_WINDOW_S = 15;
|
|
2132
|
+
function seconds(value) {
|
|
2133
|
+
const raw = value.trim().replace(/s$/, "");
|
|
2134
|
+
if (!raw) throw new Error("timestamp is empty");
|
|
2135
|
+
const parts = raw.split(":");
|
|
2136
|
+
if (parts.length > 3) throw new Error(`unparseable timestamp: ${value}`);
|
|
2137
|
+
return parts.map((p) => {
|
|
2138
|
+
const n = Number(p);
|
|
2139
|
+
if (!Number.isFinite(n) || n < 0) throw new Error(`unparseable timestamp: ${value}`);
|
|
2140
|
+
return n;
|
|
2141
|
+
}).reduce((total, n) => total * 60 + n, 0);
|
|
2142
|
+
}
|
|
2143
|
+
function ffmpegArgs(src, from, to, out) {
|
|
2144
|
+
return [
|
|
2145
|
+
"-y",
|
|
2146
|
+
"-nostdin",
|
|
2147
|
+
"-loglevel",
|
|
2148
|
+
"error",
|
|
2149
|
+
"-ss",
|
|
2150
|
+
String(seconds(from)),
|
|
2151
|
+
"-to",
|
|
2152
|
+
String(seconds(to)),
|
|
2153
|
+
"-i",
|
|
2154
|
+
src,
|
|
2155
|
+
"-map",
|
|
2156
|
+
"0:v:0",
|
|
2157
|
+
"-map",
|
|
2158
|
+
"0:a:0?",
|
|
2159
|
+
"-vf",
|
|
2160
|
+
`fps=${CLIP_FPS},scale=-2:min(360\\,ih),setsar=1`,
|
|
2161
|
+
"-c:v",
|
|
2162
|
+
"libx264",
|
|
2163
|
+
"-preset",
|
|
2164
|
+
"ultrafast",
|
|
2165
|
+
"-crf",
|
|
2166
|
+
"30",
|
|
2167
|
+
"-pix_fmt",
|
|
2168
|
+
"yuv420p",
|
|
2169
|
+
"-c:a",
|
|
2170
|
+
"aac",
|
|
2171
|
+
"-b:a",
|
|
2172
|
+
"48k",
|
|
2173
|
+
"-ar",
|
|
2174
|
+
"24000",
|
|
2175
|
+
"-ac",
|
|
2176
|
+
"1",
|
|
2177
|
+
"-movflags",
|
|
2178
|
+
"+faststart",
|
|
2179
|
+
out
|
|
2180
|
+
];
|
|
2181
|
+
}
|
|
2182
|
+
async function cutClip(src, from, to) {
|
|
2183
|
+
const span2 = seconds(to) - seconds(from);
|
|
2184
|
+
if (span2 <= 0) throw new Error(`window end must follow its start (${from}..${to})`);
|
|
2185
|
+
if (span2 > MAX_WINDOW_S) {
|
|
2186
|
+
throw new Error(`window ${from}..${to} is ${span2.toFixed(1)}s; a media check covers at most ${MAX_WINDOW_S}s \u2014 split it into overlapping windows`);
|
|
2187
|
+
}
|
|
2188
|
+
if (!fs2.existsSync(src)) throw new Error(`source video not found: ${src}`);
|
|
2189
|
+
const dir = fs2.mkdtempSync(path.join(os.tmpdir(), "review-clip-"));
|
|
2190
|
+
const out = path.join(dir, "evidence.mp4");
|
|
2191
|
+
const cleanup = () => fs2.rmSync(dir, { recursive: true, force: true });
|
|
2192
|
+
try {
|
|
2193
|
+
await run("ffmpeg", ffmpegArgs(src, from, to, out));
|
|
2194
|
+
} catch (error) {
|
|
2195
|
+
cleanup();
|
|
2196
|
+
throw error;
|
|
2197
|
+
}
|
|
2198
|
+
if (!fs2.existsSync(out) || fs2.statSync(out).size === 0) {
|
|
2199
|
+
cleanup();
|
|
2200
|
+
throw new Error(`ffmpeg produced no output for ${from}..${to}`);
|
|
2201
|
+
}
|
|
2202
|
+
return { path: out, bytes: fs2.statSync(out).size, cleanup };
|
|
2203
|
+
}
|
|
2204
|
+
async function grabFrame(src, at, out) {
|
|
2205
|
+
if (!fs2.existsSync(src)) throw new Error(`source video not found: ${src}`);
|
|
2206
|
+
fs2.mkdirSync(path.dirname(out), { recursive: true });
|
|
2207
|
+
await run("ffmpeg", [
|
|
2208
|
+
"-y",
|
|
2209
|
+
"-nostdin",
|
|
2210
|
+
"-loglevel",
|
|
2211
|
+
"error",
|
|
2212
|
+
"-ss",
|
|
2213
|
+
String(seconds(at)),
|
|
2214
|
+
"-i",
|
|
2215
|
+
src,
|
|
2216
|
+
"-frames:v",
|
|
2217
|
+
"1",
|
|
2218
|
+
"-vf",
|
|
2219
|
+
"scale=-2:min(480\\,ih)",
|
|
2220
|
+
"-q:v",
|
|
2221
|
+
"3",
|
|
2222
|
+
"-f",
|
|
2223
|
+
"image2",
|
|
2224
|
+
out
|
|
2225
|
+
]);
|
|
2226
|
+
if (!fs2.existsSync(out) || fs2.statSync(out).size === 0) throw new Error(`no frame at ${at}`);
|
|
2227
|
+
return out;
|
|
2228
|
+
}
|
|
2229
|
+
function run(bin, args) {
|
|
2230
|
+
return new Promise((resolve2, reject) => {
|
|
2231
|
+
const child = spawn(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
2232
|
+
let stderr = "";
|
|
2233
|
+
child.stderr?.on("data", (chunk) => {
|
|
2234
|
+
stderr += String(chunk);
|
|
2235
|
+
});
|
|
2236
|
+
child.on("error", (error) => reject(new Error(`${bin} failed to start: ${error.message}`)));
|
|
2237
|
+
child.on("close", (code) => code === 0 ? resolve2() : reject(new Error(`${bin} exited ${code}: ${stderr.trim().slice(0, 300)}`)));
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// packages/review-agent/src/media/watch.ts
|
|
2242
|
+
import * as crypto from "node:crypto";
|
|
2243
|
+
import * as fs3 from "node:fs";
|
|
2244
|
+
|
|
2245
|
+
// packages/review-agent/src/media/net.ts
|
|
2246
|
+
import { ProxyAgent } from "undici";
|
|
2247
|
+
var cached;
|
|
2248
|
+
function proxyUrl() {
|
|
2249
|
+
for (const name of ["https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY"]) {
|
|
2250
|
+
const value = (process.env[name] ?? "").trim();
|
|
2251
|
+
if (value && /^https?:\/\//i.test(value)) return value;
|
|
2252
|
+
}
|
|
2253
|
+
return void 0;
|
|
2254
|
+
}
|
|
2255
|
+
function dispatcher() {
|
|
2256
|
+
if (cached === void 0) {
|
|
2257
|
+
const url = proxyUrl();
|
|
2258
|
+
cached = url ? new ProxyAgent(url) : null;
|
|
2259
|
+
}
|
|
2260
|
+
return cached ?? void 0;
|
|
2261
|
+
}
|
|
2262
|
+
async function post(url, body, timeoutMs = 18e4) {
|
|
2263
|
+
return fetch(url, {
|
|
2264
|
+
method: "POST",
|
|
2265
|
+
headers: { "Content-Type": "application/json" },
|
|
2266
|
+
body: JSON.stringify(body),
|
|
2267
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
2268
|
+
...dispatcher() ? { dispatcher: dispatcher() } : {}
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// packages/review-agent/src/media/watch.ts
|
|
2273
|
+
var SYSTEM = "\u4F60\u662F\u5267\u672C\u5BA1\u6838\u7684\u89C6\u9891\u8BC1\u636E\u6838\u5BF9\u5668\u3002\u53EA\u62A5\u544A\u89C6\u9891\u7A97\u53E3\u4E2D\u53EF\u76F4\u63A5\u89C2\u5BDF\u6216\u542C\u5230\u7684\u4E8B\u5B9E\uFF1B\u65E0\u6CD5\u786E\u8BA4\u65F6\u660E\u786E\u8BF4\u660E\uFF0C\u4E0D\u63A8\u6D4B\u5267\u60C5\u3002";
|
|
2274
|
+
function traceKey(video, window, model, question) {
|
|
2275
|
+
return "watch:" + crypto.createHash("sha256").update([video, window, model, question].join("\n")).digest("hex");
|
|
2276
|
+
}
|
|
2277
|
+
async function watchWindow(video, from, to, question, opts) {
|
|
2278
|
+
const model = opts.model ?? "gemini-3.7-flash";
|
|
2279
|
+
const base = opts.baseUrl ?? "https://generativelanguage.googleapis.com";
|
|
2280
|
+
const clip2 = await cutClip(video, from, to);
|
|
2281
|
+
const started = Date.now();
|
|
2282
|
+
try {
|
|
2283
|
+
const body = {
|
|
2284
|
+
contents: [{
|
|
2285
|
+
role: "user",
|
|
2286
|
+
parts: [
|
|
2287
|
+
{
|
|
2288
|
+
inline_data: { mime_type: "video/mp4", data: fs3.readFileSync(clip2.path, "base64") },
|
|
2289
|
+
video_metadata: { fps: CLIP_FPS }
|
|
2290
|
+
},
|
|
2291
|
+
{ text: question }
|
|
2292
|
+
]
|
|
2293
|
+
}],
|
|
2294
|
+
systemInstruction: { parts: [{ text: SYSTEM }] },
|
|
2295
|
+
generationConfig: {
|
|
2296
|
+
temperature: 0.1,
|
|
2297
|
+
maxOutputTokens: 2048,
|
|
2298
|
+
responseMimeType: "application/json",
|
|
2299
|
+
responseSchema: {
|
|
2300
|
+
type: "object",
|
|
2301
|
+
properties: { answer: { type: "string" } },
|
|
2302
|
+
required: ["answer"]
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
const response = await post(
|
|
2307
|
+
`${base}/v1beta/models/${model}:generateContent?key=${opts.apiKey}`,
|
|
2308
|
+
body
|
|
2309
|
+
);
|
|
2310
|
+
const payload = await response.json().catch(() => ({}));
|
|
2311
|
+
if (!response.ok || payload?.error) {
|
|
2312
|
+
throw new Error(`media check failed (${response.status}): ` + String(payload?.error?.message ?? "unknown").slice(0, 200));
|
|
2313
|
+
}
|
|
2314
|
+
const text = payload?.candidates?.[0]?.content?.parts?.map((p) => p?.text).filter(Boolean).join("") ?? "";
|
|
2315
|
+
let answer = text.trim();
|
|
2316
|
+
try {
|
|
2317
|
+
const parsed = JSON.parse(answer);
|
|
2318
|
+
if (typeof parsed?.answer === "string") answer = parsed.answer;
|
|
2319
|
+
} catch {
|
|
2320
|
+
}
|
|
2321
|
+
if (!answer) throw new Error("media check returned no answer");
|
|
2322
|
+
const meta = payload?.usageMetadata ?? {};
|
|
2323
|
+
return {
|
|
2324
|
+
answer,
|
|
2325
|
+
usage: {
|
|
2326
|
+
call_key: opts.callKey,
|
|
2327
|
+
trace_key: traceKey(video, `${from}..${to}`, model, question),
|
|
2328
|
+
provider: "google-gemini",
|
|
2329
|
+
model,
|
|
2330
|
+
input_tokens: Number(meta.promptTokenCount ?? 0),
|
|
2331
|
+
cached_input_tokens: Number(meta.cachedContentTokenCount ?? 0),
|
|
2332
|
+
output_tokens: Number(meta.candidatesTokenCount ?? 0),
|
|
2333
|
+
reasoning_tokens: Number(meta.thoughtsTokenCount ?? 0),
|
|
2334
|
+
clip_bytes: clip2.bytes,
|
|
2335
|
+
wall_ms: Date.now() - started
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
} finally {
|
|
2339
|
+
clip2.cleanup();
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// packages/review-agent/src/prompt.ts
|
|
2344
|
+
var DOCTRINE = `
|
|
2345
|
+
## \u804C\u8D23
|
|
2346
|
+
|
|
2347
|
+
\u53EA\u505A\u5FE0\u5B9E\u5EA6\u4E0E\u7ED3\u6784\u6821\u51C6\u3002\u4E0D\u8BC4\u4EF7\u5267\u60C5\u8D28\u91CF\uFF0C\u4E0D\u6DA6\u8272\uFF0C\u4E0D\u8865\u6897\u6982\u6216\u4EBA\u7269\u63CF\u8FF0\u3002
|
|
2348
|
+
|
|
2349
|
+
## \u4E0D\u53D8\u91CF
|
|
2350
|
+
|
|
2351
|
+
- \u89C6\u9891\u753B\u9762\u4E0E\u539F\u58F0\u662F\u5FE0\u5B9E\u5EA6\u7684\u4E8B\u5B9E\u6E90\uFF1B\u5267\u672C\u4E0A\u4E0B\u6587\u53EA\u7528\u4E8E**\u53D1\u73B0**\u98CE\u9669\uFF0C\u4E0D\u7528\u4E8E\u786E\u8BA4\u3002
|
|
2352
|
+
- \u9AD8\u3001\u4E2D\u7F6E\u4FE1\u4E14\u901A\u8FC7\u8BC1\u636E\u51C6\u5165\u7684\u95EE\u9898\u624D\u53EF\u4EE5\u4FEE\uFF1B\u4F4E\u7F6E\u4FE1\u3001\u5A92\u4F53\u7F3A\u5931\u6216\u8BC1\u636E\u51B2\u7A81\u4E00\u5F8B\u4FDD\u6301\u539F\u6837\u3002
|
|
2353
|
+
- \u540C\u503C\u65F6\u95F4\u6233\u5408\u6CD5\u3002\u53EA\u6709\u5012\u5E8F\u3001\u7F3A\u5931\u3001\u8D85\u51FA\u89C6\u9891\u65F6\u957F\u6216\u8D85\u8FC7 15 \u79D2\u7684\u65E0\u4E8B\u4EF6\u7A7A\u6863\u7B97\u65F6\u95F4\u8F74\u98CE\u9669\u3002
|
|
2354
|
+
- \u6BCF\u6279\u4FEE\u6539\u4E4B\u540E\u8C03\u7528 validate \u8BFB\u56DE\u3002
|
|
2355
|
+
- **\u96F6\u4FEE\u6539\u662F\u5408\u683C\u7ED3\u679C\u3002** \u6CA1\u53D1\u73B0\u95EE\u9898\u5C31\u5982\u5B9E\u8BB0 unchanged\uFF0C\u4E0D\u8981\u4E3A\u4E86\u4EA4\u5DEE\u5236\u9020\u4FEE\u6539\u3002
|
|
2356
|
+
|
|
2357
|
+
## \u8BC1\u636E\u51C6\u5165
|
|
2358
|
+
|
|
2359
|
+
\u53EA\u6709\u6EE1\u8DB3\u5176\u4E00\u624D\u5141\u8BB8\u4FEE\u6539\uFF1A
|
|
2360
|
+
|
|
2361
|
+
1. \u65E0\u6B67\u4E49\u7684\u5185\u90E8\u7ED3\u6784\u4E8B\u5B9E\uFF08\u8BF4\u8BDD\u4EBA\u672A\u88AB\u672C\u573A\u5F15\u7528\u3001\u65F6\u95F4\u6233\u5012\u5E8F\u3001\u60AC\u7A7A\u5F15\u7528\uFF09\u3002
|
|
2362
|
+
2. \u4E00\u4E2A\u77ED\u539F\u7247\u7A97\u53E3\u6E05\u695A\u8BC1\u660E\u4E86\u5B9E\u9645\u8BF4\u8BDD\u4EBA / \u51FA\u573A\u8005 / \u9020\u578B / \u52A8\u4F5C / \u5730\u70B9 / \u9053\u5177\u3002
|
|
2363
|
+
3. \u591A\u4E2A\u76F8\u4E92\u72EC\u7ACB\u4E14\u4E00\u81F4\u7684\u8BC1\u636E\uFF0C\u4E14\u6CA1\u6709\u540C\u7B49\u5F3A\u5EA6\u7684\u53CD\u8BC1\u3002
|
|
2364
|
+
|
|
2365
|
+
\u8EAB\u4EFD\u5224\u5B9A\u5FC5\u987B\u5148\u8BB0\u5F55**\u53EF\u89C2\u5BDF\u7684**\u5916\u89C2\u3001\u4F4D\u7F6E\u548C\u58F0\u97F3\uFF0C\u518D\u901A\u8FC7\u72EC\u7ACB\u951A\u70B9\u6620\u5C04\u5230\u4EBA\u7269\u540D\u3002
|
|
2366
|
+
**\u5F53\u524D\u90A3\u4E2A\uFF08\u53EF\u80FD\u662F\u9519\u7684\uFF09\u6807\u7B7E\u6C38\u8FDC\u4E0D\u80FD\u5F53\u951A\u70B9\u3002** \u753B\u9762\u91CC\u7684\u4EBA\u786E\u8BA4\u4E86\u4F46\u65E0\u6CD5\u53EF\u9760\u6620\u5C04\u5230\u540D\u5B57 \u2192 unresolved\u3002
|
|
2367
|
+
|
|
2368
|
+
\u7F6E\u4FE1\u5EA6\uFF1Ahigh = \u76F4\u63A5\u65E0\u6B67\u4E49\uFF1Bmedium = \u82E5\u5E72\u4E00\u81F4\u7684\u95F4\u63A5\u8BC1\u636E\uFF1Blow = \u5355\u4E00\u5F31\u7EBF\u7D22\u6216\u5B58\u5728\u53CD\u8BC1\u3002
|
|
2369
|
+
|
|
2370
|
+
## \u56FA\u5B9A\u68C0\u67E5
|
|
2371
|
+
|
|
2372
|
+
- Scene \u4EBA\u7269\u3001\u72B6\u6001\u3001\u8BF4\u8BDD\u4EBA\u4E0E Action \u4E3B\u4F53\u662F\u5426\u4E00\u81F4\u3002
|
|
2373
|
+
- \u4EBA\u7269\u8EAB\u4EFD\u3001\u58F0\u97F3\u6765\u6E90\u3001\u9020\u578B\u3001\u5730\u70B9\u3001\u9053\u5177\u662F\u5426\u8BEF\u5EFA\u3001\u91CD\u590D\u6216\u9519\u6302\u3002
|
|
2374
|
+
- \u5BF9\u767D\u662F\u5426\u9010\u5B57\u5FE0\u5B9E\u4E8E\u539F\u58F0\uFF0C\u52A8\u4F5C\u662F\u5426\u5FE0\u5B9E\u4E8E\u53EF\u89C1\u4E8B\u5B9E\uFF1B\u6709\u65E0\u9057\u6F0F\u3001\u91CD\u590D\u6216\u5E7B\u89C9\u3002
|
|
2375
|
+
- \u573A\u754C\u662F\u5426\u5BF9\u5E94\u771F\u5B9E\u7684\u65F6\u7A7A\u5207\u6362\u3002
|
|
2376
|
+
- \u65F6\u95F4\u6233\u662F\u5426\u7F3A\u5931\u3001\u5012\u5E8F\u3001\u8D8A\u754C\u6216\u4EA7\u751F\u8D85\u8FC7 15 \u79D2\u7684\u7A7A\u6863\uFF08\u76F8\u90BB\u540C\u503C\u5408\u6CD5\uFF09\u3002
|
|
2377
|
+
- \u591A\u72B6\u6001\u4EBA\u7269\u7684\u76F8\u4F3C\u9020\u578B\u5FC5\u987B\u53D6\u5F97\u4EE3\u8868\u6027\u89C6\u9891\u7A97\u53E3\u540E\u518D\u5224\u65AD\uFF0C\u4E0D\u51ED\u540D\u79F0\u76F4\u63A5\u5408\u5E76\u3002
|
|
2378
|
+
|
|
2379
|
+
## \u4FEE\u590D\u89C4\u5219
|
|
2380
|
+
|
|
2381
|
+
- \u4EBA\u7269\u8BB0\u5F55\u91CD\u590D \u2192 \u5408\u5E76\u753B\u50CF\u4E0E\u5F15\u7528\uFF0C\u518D\u5220\u6389\u5DF2\u65E0\u5F15\u7528\u7684\u90A3\u4E2A\uFF0C\u4FDD\u7559\u66F4\u7A33\u5B9A/\u5F15\u7528\u66F4\u591A\u7684 ID\u3002
|
|
2382
|
+
- \u4E24\u4E2A\u4E0D\u540C\u4EBA\u7269\u4E4B\u95F4\u7684\u90E8\u5206\u8BEF\u8BA4 \u2192 \u4E24\u4E2A Actor \u90FD\u7559\u4E0B\uFF0C\u53EA\u4FEE\u9519\u7684\u90A3\u4E00\u96C6/\u90A3\u4E00\u573A\u7684\u8BF4\u8BDD\u4EBA\u3001\u6B63\u6587\u4E0E\u5F15\u7528\u3002
|
|
2383
|
+
- \u58F0\u97F3\u6765\u6E90\u88AB\u8BEF\u5EFA\u6210\u4EBA\u7269 \u2192 \u628A\u5BF9\u767D\u6307\u56DE\u6B63\u786E\u7684\u8BF4\u8BDD\u4EBA\u7C7B\u522B\u6216\u771F\u5B9E\u4EBA\u7269\uFF0C\u5F15\u7528\u6E05\u7A7A\u540E\u518D\u5220\u7A7A\u58F3\u3002
|
|
2384
|
+
- \u9020\u578B\u72B6\u6001\u91CD\u590D \u2192 \u628A\u5F15\u7528\u8FC1\u79FB\u5230\u6709\u8BC1\u636E\u652F\u6301\u7684\u90A3\u4E2A\u72B6\u6001\uFF0C\u518D\u5220\u91CD\u590D\u7684\u3002
|
|
2385
|
+
- Scene \u7F3A\u4EBA\u7269\u5F15\u7528 \u2192 \u53EA\u6709\u8BE5\u4EBA\u7269\u786E\u5B9E\u51FA\u573A\u6216\u5F00\u53E3\u624D\u8865\uFF1B\u6B63\u6587\u91CC\u88AB\u63D0\u5230\u4E0D\u6784\u6210\u4F9D\u636E\u3002
|
|
2386
|
+
- \u7F3A\u5931\u7684 Action / \u5BF9\u767D \u2192 \u53EA\u5728\u539F\u7247\u660E\u786E\u7684\u65F6\u95F4\u70B9\u63D2\u5165\uFF1B\u4E0D\u5F97\u6539\u5199\u539F\u58F0\uFF0C\u4E0D\u5F97\u51ED\u7A7A\u8865\u4E0D\u53EF\u89C1\u7684\u4E8B\u5B9E\u3002
|
|
2387
|
+
`.trim();
|
|
2388
|
+
var MEDIA = `
|
|
2389
|
+
## \u5A92\u4F53\u8BC1\u636E
|
|
2390
|
+
|
|
2391
|
+
\u53EA\u5728\u6587\u672C\u548C\u7ED3\u6784\u8BC1\u636E\u4E0D\u80FD\u6536\u53E3\u65F6\u624D\u52A8\u5A92\u4F53\u5DE5\u5177\u3002
|
|
2392
|
+
|
|
2393
|
+
- \`span\` \u628A action \u6362\u7B97\u6210\u89C6\u9891\u7A97\u53E3\uFF0C\u5148\u7528\u5B83\u5B9A\u4F4D\u518D\u770B\u7247\u3002
|
|
2394
|
+
- \`watch\` \u770B\u5E26\u58F0\u97F3\u7684\u77ED\u7A97\u53E3\uFF0C\u6700\u8D35\uFF1A\u7A97\u53E3\u8981\u8986\u76D6\u5019\u9009\u7684\u58F0\u97F3\u6216\u52A8\u4F5C\uFF0C\u524D\u540E\u5404\u7559\u6700\u591A 3 \u79D2\uFF0C
|
|
2395
|
+
\u5355\u6B21\u4E0D\u8D85\u8FC7 15 \u79D2\uFF1B\u4E00\u53E5\u53F0\u8BCD\u8986\u76D6\u4E0D\u5B8C\u5C31\u62C6\u6210\u91CD\u53E0\u7A97\u53E3\u3002
|
|
2396
|
+
- \`frames\` \u62BD\u9759\u5E27\u76F4\u63A5\u8FDB\u4F60\u7684\u4E0A\u4E0B\u6587\uFF0C\u4FBF\u5B9C\u4E14\u7CBE\u786E\uFF0C\u9002\u5408\u6838\u5BF9\u9020\u578B\u3001\u4EBA\u6570\u3001\u573A\u666F\uFF0C\u4F46\u6CA1\u6709\u58F0\u97F3\u3002
|
|
2397
|
+
|
|
2398
|
+
\u5A92\u4F53\u9884\u7B97\u7528\u5B8C\u6216\u4E00\u6B21\u6838\u5BF9\u4ECD\u4E0D\u8DB3 \u2192 \u8BB0 unresolved \u5E76\u8BF4\u660E\u539F\u56E0\uFF0C\u4E0D\u8981\u53CD\u590D\u8BD5\u3002
|
|
2399
|
+
`.trim();
|
|
2400
|
+
var NO_MEDIA = `
|
|
2401
|
+
## \u6CA1\u6709\u539F\u89C6\u9891
|
|
2402
|
+
|
|
2403
|
+
\u672C\u6B21\u6CA1\u6709\u63D0\u4F9B\u539F\u7247\uFF0C\u5A92\u4F53\u5DE5\u5177\u672A\u6CE8\u518C\u3002\u56E0\u6B64**\u4E0D\u5F97\u58F0\u79F0\u4EFB\u4F55\u89C6\u9891\u8BC1\u636E**\uFF1A
|
|
2404
|
+
\u53EA\u80FD\u4F9D\u636E\u65E0\u6B67\u4E49\u7684\u5185\u90E8\u7ED3\u6784\u4E8B\u5B9E\u4FEE\u6539\uFF08\u8BF4\u8BDD\u4EBA\u672A\u88AB\u672C\u573A\u5F15\u7528\u3001\u65F6\u95F4\u6233\u5012\u5E8F\u3001\u60AC\u7A7A\u5F15\u7528\u7B49\uFF09\u3002
|
|
2405
|
+
\u4EFB\u4F55\u9700\u8981\u770B\u753B\u9762\u6216\u542C\u58F0\u97F3\u624D\u80FD\u5224\u5B9A\u7684\u5019\u9009\uFF0C\u4E00\u5F8B\u8BB0 unresolved\uFF0C\u539F\u56E0\u5199"\u65E0\u539F\u7247"\u3002
|
|
2406
|
+
`.trim();
|
|
2407
|
+
function systemPrompt(input) {
|
|
2408
|
+
return [
|
|
2409
|
+
"\u4F60\u6B63\u5728\u6267\u884C\u65E0\u4EBA\u503C\u5B88\u7684\u5267\u672C\u5BA1\u6838\u4E0E\u4FEE\u590D\u3002",
|
|
2410
|
+
DOCTRINE,
|
|
2411
|
+
input.video ? MEDIA : NO_MEDIA,
|
|
2412
|
+
"## \u5168\u5267\u8BBE\u5B9A\uFF08\u6BCF\u96C6\u76F8\u540C\uFF09",
|
|
2413
|
+
input.seriesCard
|
|
2414
|
+
].join("\n\n");
|
|
2415
|
+
}
|
|
2416
|
+
function episodeTask(input) {
|
|
2417
|
+
if (input.episode === 0) {
|
|
2418
|
+
return [
|
|
2419
|
+
"## \u672C\u6B21\u4EFB\u52A1\uFF1A\u5168\u5267\u6536\u655B",
|
|
2420
|
+
"\u53EA\u5904\u7406\u8DE8\u96C6\u4E00\u81F4\u6027\uFF1A\u4EBA\u7269\u8EAB\u4EFD\u91CD\u590D\u3001\u9020\u578B\u72B6\u6001\u91CD\u590D\u3001\u5730\u70B9/\u9053\u5177\u91CD\u590D\u3001\u6B8B\u4F59\u5F15\u7528\u3002",
|
|
2421
|
+
"\u4E0D\u8981\u91CD\u590D\u9010\u96C6\u5185\u5BB9\u5BA1\u6838\u3002\u672C\u9636\u6BB5\u53EF\u4EE5\u4F7F\u7528 merge \u4E0E\u8D44\u4EA7/\u72B6\u6001\u7EA7\u7684 edit / del\u3002",
|
|
2422
|
+
"\u6BCF\u6536\u53E3\u4E00\u4E2A\u95EE\u9898\u8C03\u7528\u4E00\u6B21 finding\uFF0C\u6700\u540E\u8DD1\u4E00\u6B21 validate\u3002"
|
|
2423
|
+
].join("\n");
|
|
2424
|
+
}
|
|
2425
|
+
return [
|
|
2426
|
+
`## \u672C\u6B21\u4EFB\u52A1\uFF1A\u7B2C ${input.episode} \u96C6`,
|
|
2427
|
+
input.video ? `\u539F\u89C6\u9891\uFF1A${input.video}` : "\uFF08\u65E0\u539F\u89C6\u9891\uFF09",
|
|
2428
|
+
input.video ? `\u5A92\u4F53\u6838\u5BF9\u9884\u7B97\uFF1A${input.mediaBudget} \u6B21` : "",
|
|
2429
|
+
"",
|
|
2430
|
+
"\u5148 scan \u672C\u96C6\u5EFA\u7ACB\u4E0A\u4E0B\u6587\uFF0C\u518D\u9010\u6761\u68C0\u67E5 Dialogue\u3001inner thought \u4E0E Action\u3002",
|
|
2431
|
+
"\u5148\u5EFA\u7ACB\u4EBA\u7269\u76EE\u6807\u3001\u5DF2\u77E5\u4FE1\u606F\u3001\u5173\u7CFB\u548C\u76F8\u90BB\u884C\u4E3A\u7684\u56E0\u679C\uFF0C\u518D\u5224\u65AD\u5355\u6761\u662F\u5426\u5FE0\u5B9E\u3002",
|
|
2432
|
+
input.neighbours ? `
|
|
2433
|
+
\u76F8\u90BB\u96C6\u6897\u6982\uFF08\u53EA\u7528\u4E8E\u7406\u89E3\u56E0\u679C\uFF09\uFF1A
|
|
2434
|
+
${input.neighbours}` : "",
|
|
2435
|
+
"",
|
|
2436
|
+
"\u6BCF\u6536\u53E3\u4E00\u4E2A\u95EE\u9898\u5C31\u8C03\u7528\u4E00\u6B21 finding\uFF0C\u4E0D\u8981\u6512\u5230\u6700\u540E\u3002\u6539\u5B8C\u8BFB\u56DE\u5E76\u8DD1 validate\u3002"
|
|
2437
|
+
].filter((line) => line !== "").join("\n");
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// packages/review-agent/src/tools.ts
|
|
2441
|
+
import * as fs4 from "node:fs";
|
|
2442
|
+
import * as os2 from "node:os";
|
|
2443
|
+
import * as path2 from "node:path";
|
|
2444
|
+
import { createTool } from "@mastra/core/tools";
|
|
2445
|
+
import { z as z2 } from "zod";
|
|
2446
|
+
var ADDR = "\u5730\u5740\uFF1A\u96C6 `3`\u3001\u573A `3-2`\u3001\u5355\u6761 `3-2-5`\u3001\u6B63\u5F0F ID\uFF08`acB4xR8p`/`scnQ7mN2v`\uFF09\u3001\u8D44\u4EA7\u540D\u3001\u6216 `<\u8D44\u4EA7>/<\u72B6\u6001>`";
|
|
2447
|
+
function newContext(init) {
|
|
2448
|
+
return {
|
|
2449
|
+
...init,
|
|
2450
|
+
findings: [],
|
|
2451
|
+
usage: [],
|
|
2452
|
+
mediaCalls: 0,
|
|
2453
|
+
frameDir: fs4.mkdtempSync(path2.join(os2.tmpdir(), "review-frames-"))
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
function scanEpisode(doc, episode) {
|
|
2457
|
+
const addr = String(episode);
|
|
2458
|
+
const outline = read(doc, addr, { depth: "outline" });
|
|
2459
|
+
const body = read(doc, addr);
|
|
2460
|
+
const problems = issues(doc, {});
|
|
2461
|
+
return [
|
|
2462
|
+
`## \u573A\u7ED3\u6784
|
|
2463
|
+
${outline}`,
|
|
2464
|
+
`## \u6B63\u6587
|
|
2465
|
+
${body}`,
|
|
2466
|
+
`## \u5F53\u524D\u6821\u9A8C\u95EE\u9898\uFF08\u5168\u5267\uFF09
|
|
2467
|
+
${problems}`
|
|
2468
|
+
].join("\n\n");
|
|
2469
|
+
}
|
|
2470
|
+
function buildTools(ctx) {
|
|
2471
|
+
const tools = {};
|
|
2472
|
+
tools.scan = createTool({
|
|
2473
|
+
id: "scan",
|
|
2474
|
+
description: "\u8BFB\u4E00\u96C6\u7684\u5B8C\u6574\u4E0A\u4E0B\u6587\uFF1A\u573A\u7ED3\u6784 + \u5168\u90E8\u6B63\u6587 + \u5F53\u524D\u6821\u9A8C\u95EE\u9898\u3002\u5F00\u59CB\u5BA1\u6838\u4E00\u96C6\u65F6\u5148\u8C03\u7528\u5B83\u3002",
|
|
2475
|
+
inputSchema: z2.object({ episode: z2.number().int().min(1).describe("\u96C6\u53F7\uFF0C\u4ECE 1 \u5F00\u59CB") }),
|
|
2476
|
+
execute: async ({ episode }) => scanEpisode(ctx.doc, episode)
|
|
2477
|
+
});
|
|
2478
|
+
tools.read = createTool({
|
|
2479
|
+
id: "read",
|
|
2480
|
+
description: `\u8BFB\u4EFB\u610F\u8282\u70B9\u3002${ADDR}\u3002\u4E0D\u4F20\u5730\u5740\u8FD4\u56DE\u5267\u672C\u5361\uFF08\u9898\u6750/\u8D44\u4EA7\u8868/\u6BCF\u96C6\u4E00\u884C\uFF09\u3002\u8D44\u4EA7\u5730\u5740\u8FD4\u56DE\u4EBA\u7269\u5361\uFF08\u63CF\u8FF0 + \u5168\u90E8\u72B6\u6001 + \u51FA\u573A\u7EDF\u8BA1 + \u672A\u4F7F\u7528\u72B6\u6001\uFF09\u3002`,
|
|
2481
|
+
inputSchema: z2.object({
|
|
2482
|
+
addr: z2.string().optional().describe(ADDR),
|
|
2483
|
+
context: z2.number().int().min(0).max(20).optional().describe("\u5355\u6761 action \u524D\u540E\u5404\u5E26\u51E0\u6761\uFF0C\u9ED8\u8BA4 3")
|
|
2484
|
+
}),
|
|
2485
|
+
execute: async ({ addr, context }) => read(ctx.doc, addr ?? "", context === void 0 ? {} : { context })
|
|
2486
|
+
});
|
|
2487
|
+
tools.find = createTool({
|
|
2488
|
+
id: "find",
|
|
2489
|
+
description: "\u6309\u6B63\u6587\u6B63\u5219 / \u8BF4\u8BDD\u4EBA / \u7C7B\u578B\u5B9A\u4F4D action\uFF0C\u8FD4\u56DE\u951A\u70B9 + \u4F4D\u7F6E + \u547D\u4E2D\u7A97\u53E3\u3002",
|
|
2490
|
+
inputSchema: z2.object({
|
|
2491
|
+
query: z2.string().describe("\u6B63\u5219\uFF1B\u53EA\u6309 speaker/type \u8FC7\u6EE4\u65F6\u4F20\u7A7A\u4E32"),
|
|
2492
|
+
in: z2.string().optional().describe(`\u9650\u5236\u8303\u56F4\uFF0C${ADDR}`),
|
|
2493
|
+
speaker: z2.string().optional().describe("\u4EBA\u7269\u540D\u6216 actor id"),
|
|
2494
|
+
type: z2.enum(["action", "dialogue", "inner_thought", "transition"]).optional(),
|
|
2495
|
+
limit: z2.number().int().min(1).max(200).optional()
|
|
2496
|
+
}),
|
|
2497
|
+
execute: async (input) => find(ctx.doc, input.query, {
|
|
2498
|
+
...input.in ? { in: input.in } : {},
|
|
2499
|
+
...input.speaker ? { speaker: input.speaker } : {},
|
|
2500
|
+
...input.type ? { type: input.type } : {},
|
|
2501
|
+
...input.limit ? { limit: input.limit } : {}
|
|
2502
|
+
})
|
|
2503
|
+
});
|
|
2504
|
+
tools.span = createTool({
|
|
2505
|
+
id: "span",
|
|
2506
|
+
description: "\u628A\u4E00\u6BB5 action \u6362\u7B97\u6210\u96C6\u5185\u89C6\u9891\u65F6\u95F4\u7A97\u53E3\uFF08\u534A\u5F00\u533A\u95F4\uFF09\uFF0C\u9009 watch/frames \u7684\u7A97\u53E3\u9760\u5B83\u3002",
|
|
2507
|
+
inputSchema: z2.object({ range: z2.string().describe(`${ADDR}\uFF0C\u6216 \`\u8D77ID..\u6B62ID\``) }),
|
|
2508
|
+
execute: async ({ range }) => span(ctx.doc, range)
|
|
2509
|
+
});
|
|
2510
|
+
tools.issues = createTool({
|
|
2511
|
+
id: "issues",
|
|
2512
|
+
description: "\u5217\u5F53\u524D\u6821\u9A8C\u95EE\u9898\uFF0C\u53EF\u6309 severity / code \u8FC7\u6EE4\u3002",
|
|
2513
|
+
inputSchema: z2.object({
|
|
2514
|
+
severity: z2.enum(["error", "warning"]).optional(),
|
|
2515
|
+
code: z2.string().optional()
|
|
2516
|
+
}),
|
|
2517
|
+
execute: async (input) => issues(ctx.doc, input)
|
|
2518
|
+
});
|
|
2519
|
+
tools.validate = createTool({
|
|
2520
|
+
id: "validate",
|
|
2521
|
+
description: "\u8DD1\u6574\u672C\u6821\u9A8C\u3002\u6BCF\u6279\u4FEE\u6539\u4E4B\u540E\u5FC5\u987B\u8C03\u7528\u4E00\u6B21\u3002",
|
|
2522
|
+
inputSchema: z2.object({}),
|
|
2523
|
+
execute: async () => validate(ctx.doc)
|
|
2524
|
+
});
|
|
2525
|
+
const editFields = {
|
|
2526
|
+
content: z2.string().optional().describe("**\u6574\u6761\u8986\u5199** action \u6B63\u6587\uFF1A\u4F20\u5165\u7684\u5B57\u7B26\u4E32\u4F1A\u6210\u4E3A\u8FD9\u6761 action \u7684\u5168\u90E8\u5185\u5BB9\uFF0C\u539F\u6587\u88AB\u4E22\u5F03\u3002\u53EA\u60F3\u6539\u5176\u4E2D\u4E00\u4E2A\u8BCD\u6216\u4E00\u6BB5\uFF0C\u7528 `replace`\uFF0C\u4E0D\u8981\u7528\u8FD9\u4E2A\u5B57\u6BB5\u3002"),
|
|
2527
|
+
paren: z2.string().nullable().optional().describe("\u8868\u6F14\u63D0\u793A\uFF1Bnull \u5220\u9664\u8BE5\u5B57\u6BB5"),
|
|
2528
|
+
speaker: z2.string().optional().describe("\u4EBA\u7269\u540D\u6216 `\u540D\u5B57\xB7\u72B6\u6001`\uFF1B\u65C1\u767D/\u5E7F\u64AD/\u753B\u5916/\u7FA4\u58F0\u4EA6\u53EF"),
|
|
2529
|
+
ts: z2.string().nullable().optional().describe("\u65F6\u95F4\u6233\uFF1Bnull \u5220\u9664"),
|
|
2530
|
+
type: z2.enum(["action", "dialogue", "inner_thought", "transition"]).optional(),
|
|
2531
|
+
space: z2.enum(["interior", "exterior"]).optional().describe("\u573A\u5730\u5740\uFF1A\u5185\u5916\u666F"),
|
|
2532
|
+
time: z2.enum(["day", "night", "dawn", "dusk"]).optional().describe("\u573A\u5730\u5740\uFF1A\u65E5\u591C")
|
|
2533
|
+
};
|
|
2534
|
+
const assetFields = {
|
|
2535
|
+
name: z2.string().optional(),
|
|
2536
|
+
desc: z2.string().optional(),
|
|
2537
|
+
importance: z2.enum(["featured", "background"]).optional(),
|
|
2538
|
+
role: z2.enum(["\u4E3B\u89D2", "\u914D\u89D2"]).optional()
|
|
2539
|
+
};
|
|
2540
|
+
tools.edit = createTool({
|
|
2541
|
+
id: "edit",
|
|
2542
|
+
description: ctx.phase === "converge" ? `\u6539\u4E00\u4E2A\u8282\u70B9\u7684\u5B57\u6BB5\u3002${ADDR}\uFF1B\u5730\u5740\u51B3\u5B9A\u54EA\u4E9B\u5B57\u6BB5\u5408\u6CD5\uFF08action / \u573A / \u8D44\u4EA7 / \u72B6\u6001\uFF09\u3002` : `\u6539\u4E00\u6761 action \u6216\u4E00\u4E2A\u573A\u7684\u5B57\u6BB5\u3002${ADDR}\u3002\u8D44\u4EA7\u548C\u72B6\u6001\u672C\u8EAB\u7559\u7ED9\u5168\u5267\u6536\u655B\u9636\u6BB5\u3002`,
|
|
2543
|
+
inputSchema: z2.object({
|
|
2544
|
+
at: z2.string().describe(ADDR),
|
|
2545
|
+
...editFields,
|
|
2546
|
+
...ctx.phase === "converge" ? assetFields : {}
|
|
2547
|
+
}),
|
|
2548
|
+
execute: async ({ at, ...fields }) => {
|
|
2549
|
+
const clean = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== void 0));
|
|
2550
|
+
return edit(ctx.doc, at, clean);
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2553
|
+
tools.replace = createTool({
|
|
2554
|
+
id: "replace",
|
|
2555
|
+
description: "\u628A\u4E00\u6BB5\u6587\u672C\u6362\u6210\u53E6\u4E00\u6BB5\uFF0C\u53EA\u52A8\u5339\u914D\u5230\u7684\u90E8\u5206\uFF0C\u6B63\u6587\u5176\u4F59\u5185\u5BB9\u4E0D\u53D8\u3002\u6539\u4EBA\u540D\u3001\u5730\u540D\u3001\u79F0\u8C13\u8FD9\u7C7B\u300E\u4E00\u4E2A\u8BCD\u9519\u4E86\u300F\u7684\u95EE\u9898\u7528\u5B83\uFF0C\u4E0D\u8981\u7528 edit \u6574\u6761\u8986\u5199\u3002 \u8FC7\u6EE4\u53C2\u6570\u4E0E find \u76F8\u540C\uFF0C\u53EF\u7528 in \u9650\u5B9A\u5230\u67D0\u96C6\u6216\u67D0\u573A\u3002",
|
|
2556
|
+
inputSchema: z2.object({
|
|
2557
|
+
pattern: z2.string().describe("\u8981\u88AB\u66FF\u6362\u6389\u7684\u6587\u672C\uFF08\u6B63\u5219\uFF09"),
|
|
2558
|
+
to: z2.string().describe("\u66FF\u6362\u6210\u4EC0\u4E48"),
|
|
2559
|
+
in: z2.string().optional().describe(`\u9650\u5236\u8303\u56F4\uFF0C${ADDR}\uFF1B\u4E0D\u4F20\u5219\u6574\u672C`),
|
|
2560
|
+
speaker: z2.string().optional().describe("\u53EA\u6539\u67D0\u4EBA\u7269\u7684\u53F0\u8BCD"),
|
|
2561
|
+
type: z2.enum(["action", "dialogue", "inner_thought", "transition"]).optional(),
|
|
2562
|
+
apply: z2.boolean().optional().describe("false \u6216\u7701\u7565\u53EA\u8FD4\u56DE\u9010\u884C before/after \u9884\u89C8\uFF1Btrue \u624D\u771F\u7684\u5199\u5165")
|
|
2563
|
+
}),
|
|
2564
|
+
execute: async ({ pattern, to, apply, ...filters }) => replace(ctx.doc, pattern, to, {
|
|
2565
|
+
...Object.fromEntries(Object.entries(filters).filter(([, v]) => v !== void 0)),
|
|
2566
|
+
...apply ? { apply: true } : {}
|
|
2567
|
+
})
|
|
2568
|
+
});
|
|
2569
|
+
tools.ref = createTool({
|
|
2570
|
+
id: "ref",
|
|
2571
|
+
description: "\u6539\u4E00\u4E2A\u573A\u5BF9\u67D0\u8D44\u4EA7\u7684\u5F15\u7528\u3002states \u662F\u5B8C\u6574\u72B6\u6001\u96C6\u5408\u800C\u975E\u8FFD\u52A0\uFF0C`default` \u4E5F\u8981\u663E\u5F0F\u5199\u3002",
|
|
2572
|
+
inputSchema: z2.object({
|
|
2573
|
+
scene: z2.string().describe("\u573A\u5730\u5740"),
|
|
2574
|
+
asset: z2.string().describe("\u4EBA\u7269 / \u5730\u70B9 / \u9053\u5177\u7684\u540D\u5B57\u6216 ID"),
|
|
2575
|
+
states: z2.array(z2.string()).optional().describe("\u5B8C\u6574\u72B6\u6001\u96C6\u5408\uFF08\u540D\u5B57\u6216 ID\uFF09"),
|
|
2576
|
+
remove: z2.boolean().optional().describe("true \u5220\u9664\u6574\u6761\u5F15\u7528")
|
|
2577
|
+
}),
|
|
2578
|
+
execute: async ({ scene, asset, states, remove }) => ref(ctx.doc, scene, asset, states ?? [], remove ?? false)
|
|
2579
|
+
});
|
|
2580
|
+
tools.insert = createTool({
|
|
2581
|
+
id: "insert",
|
|
2582
|
+
description: "\u5728\u67D0\u6761 action \u524D/\u540E\u63D2\u5165\u4E00\u6761\u65B0 action\uFF0C\u7528\u4E8E\u8865\u539F\u7247\u6709\u4F46\u5267\u672C\u6F0F\u6389\u7684\u5185\u5BB9\u3002",
|
|
2583
|
+
inputSchema: z2.object({
|
|
2584
|
+
at: z2.string().describe("\u76F8\u90BB action \u7684\u5730\u5740"),
|
|
2585
|
+
where: z2.enum(["before", "after"]),
|
|
2586
|
+
type: z2.enum(["action", "dialogue", "inner_thought", "transition"]),
|
|
2587
|
+
content: z2.string(),
|
|
2588
|
+
speaker: z2.string().optional(),
|
|
2589
|
+
paren: z2.string().optional(),
|
|
2590
|
+
ts: z2.string().optional()
|
|
2591
|
+
}),
|
|
2592
|
+
execute: async ({ at, where, type, content, speaker, paren, ts }) => {
|
|
2593
|
+
const out = insert(ctx.doc, at, where, type, content, speaker, paren);
|
|
2594
|
+
if (ts) {
|
|
2595
|
+
const anchor = out.match(/\b(ac[1-9A-HJ-NP-Za-km-z]{6}|\d+-\d+-\d+)\b/)?.[1];
|
|
2596
|
+
if (anchor) edit(ctx.doc, anchor, { ts });
|
|
2597
|
+
}
|
|
2598
|
+
return out;
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
tools.move = createTool({
|
|
2602
|
+
id: "move",
|
|
2603
|
+
description: "\u628A\u4E00\u6761 action \u632A\u5230\u53E6\u4E00\u6761\u65C1\u8FB9\uFF1B\u8DE8\u573A\u4E5F\u4FDD\u6301\u5B83\u7684 action_id\u3002",
|
|
2604
|
+
inputSchema: z2.object({
|
|
2605
|
+
at: z2.string(),
|
|
2606
|
+
to: z2.string().describe("\u76EE\u6807\u90BB\u5C45 action"),
|
|
2607
|
+
where: z2.enum(["before", "after"]).optional()
|
|
2608
|
+
}),
|
|
2609
|
+
execute: async ({ at, to, where }) => move(ctx.doc, at, to, where ?? "after")
|
|
2610
|
+
});
|
|
2611
|
+
tools.del = createTool({
|
|
2612
|
+
id: "del",
|
|
2613
|
+
description: ctx.phase === "converge" ? "\u5220\u9664\u8282\u70B9\uFF1Aaction / \u573A / \u8D44\u4EA7 / \u72B6\u6001\u3002\u88AB\u5F15\u7528\u7684\u8D44\u4EA7\u6216\u72B6\u6001\u9700\u8981 replacement \u6539\u5199\u6216 force\u3002" : "\u5220\u9664\u4E00\u6761 action\uFF0C\u6216\u4E00\u4E2A\u7A7A\u573A\u3002\u8D44\u4EA7\u548C\u72B6\u6001\u7684\u5220\u9664\u7559\u7ED9\u5168\u5267\u6536\u655B\u9636\u6BB5\u3002",
|
|
2614
|
+
inputSchema: z2.object({
|
|
2615
|
+
addr: z2.string().describe(ADDR),
|
|
2616
|
+
replacement: ctx.phase === "converge" ? z2.string().optional().describe("\u5220\u72B6\u6001\u65F6\u628A\u5F15\u7528\u6539\u5199\u5230\u8FD9\u4E2A\u72B6\u6001") : z2.undefined().optional(),
|
|
2617
|
+
force: z2.boolean().optional()
|
|
2618
|
+
}),
|
|
2619
|
+
execute: async ({ addr, replacement, force }) => {
|
|
2620
|
+
if (ctx.phase !== "converge") {
|
|
2621
|
+
const node = resolve(ctx.doc, addr);
|
|
2622
|
+
if (node.t === "asset" || node.t === "state") {
|
|
2623
|
+
throw new Error("\u8D44\u4EA7\u4E0E\u72B6\u6001\u7684\u5220\u9664\u53EA\u5728\u5168\u5267\u6536\u655B\u9636\u6BB5\u53EF\u7528\uFF1B\u672C\u96C6\u53EA\u5904\u7406 action \u4E0E\u573A\u3002");
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
return del(ctx.doc, addr, {
|
|
2627
|
+
...replacement ? { replacement } : {},
|
|
2628
|
+
...force ? { force } : {}
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
});
|
|
2632
|
+
if (ctx.phase === "converge") {
|
|
2633
|
+
tools.merge = createTool({
|
|
2634
|
+
id: "merge",
|
|
2635
|
+
description: "\u628A\u4E00\u4E2A\u8D44\u4EA7\u5E76\u5165\u53E6\u4E00\u4E2A\uFF1A\u5168\u90E8\u573A\u666F\u5F15\u7528\u4E0E speaker \u6539\u5199\u8FC7\u53BB\uFF0C\u540C\u540D\u72B6\u6001\u81EA\u52A8\u5BF9\u9F50\u3002",
|
|
2636
|
+
inputSchema: z2.object({ src: z2.string(), dst: z2.string() }),
|
|
2637
|
+
execute: async ({ src, dst }) => merge(ctx.doc, src, dst)
|
|
2638
|
+
});
|
|
2639
|
+
tools.state = createTool({
|
|
2640
|
+
id: "state",
|
|
2641
|
+
description: "\u7ED9\u5DF2\u6709\u8D44\u4EA7\u52A0\u4E00\u4E2A\u72B6\u6001\uFF08\u9020\u578B\u6863\u4F4D\uFF09\u3002",
|
|
2642
|
+
inputSchema: z2.object({ asset: z2.string(), name: z2.string(), desc: z2.string() }),
|
|
2643
|
+
execute: async ({ asset, name, desc }) => state(ctx.doc, asset, name, desc)
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
if (ctx.video && ctx.geminiKey) {
|
|
2647
|
+
tools.watch = createTool({
|
|
2648
|
+
id: "watch",
|
|
2649
|
+
description: `\u770B\u539F\u7247\u7684\u4E00\u4E2A\u77ED\u7A97\u53E3\uFF08\u6709\u58F0\u97F3\uFF09\uFF0C\u6838\u5BF9\u8BF4\u8BDD\u4EBA / \u8EAB\u4EFD / \u9020\u578B / \u52A8\u4F5C / \u8D44\u4EA7\u4E8B\u5B9E\u3002 \u5355\u6B21\u7A97\u53E3\u4E0D\u8D85\u8FC7 15 \u79D2\uFF1B\u672C\u96C6\u9884\u7B97 ${ctx.mediaBudget} \u6B21\u3002\u53EA\u5728\u6587\u672C\u548C\u7ED3\u6784\u8BC1\u636E\u4E0D\u80FD\u6536\u53E3\u65F6\u7528\u3002`,
|
|
2650
|
+
inputSchema: z2.object({
|
|
2651
|
+
from: z2.string().describe("\u7A97\u53E3\u8D77\u70B9\uFF0C`M:SS` \u6216 `H:MM:SS`"),
|
|
2652
|
+
to: z2.string().describe("\u7A97\u53E3\u7EC8\u70B9\uFF1B\u8986\u76D6\u5019\u9009\u58F0\u97F3\u6216\u52A8\u4F5C\uFF0C\u524D\u540E\u5404\u7559\u6700\u591A 3 \u79D2"),
|
|
2653
|
+
question: z2.string().describe("\u8981\u6838\u5BF9\u4EC0\u4E48\u3002\u8981\u6C42\u5B83\u53EA\u62A5\u53EF\u76F4\u63A5\u770B\u5230/\u542C\u5230\u7684\u4E8B\u5B9E")
|
|
2654
|
+
}),
|
|
2655
|
+
execute: async ({ from, to, question }) => {
|
|
2656
|
+
if (ctx.mediaCalls >= ctx.mediaBudget) {
|
|
2657
|
+
return `\u5A92\u4F53\u6838\u5BF9\u9884\u7B97\u5DF2\u7528\u5B8C\uFF08${ctx.mediaBudget} \u6B21\uFF09\u3002\u8BC1\u636E\u4E0D\u8DB3\u7684\u5019\u9009\u8BB0\u4E3A unresolved\u3002`;
|
|
2658
|
+
}
|
|
2659
|
+
ctx.mediaCalls += 1;
|
|
2660
|
+
const result = await watchWindow(ctx.video, from, to, question, {
|
|
2661
|
+
apiKey: ctx.geminiKey,
|
|
2662
|
+
model: ctx.mediaModel,
|
|
2663
|
+
callKey: `review:ep${ctx.episode}:watch:${ctx.mediaCalls}`
|
|
2664
|
+
});
|
|
2665
|
+
ctx.usage.push(result.usage);
|
|
2666
|
+
return result.answer;
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2669
|
+
tools.frames = createTool({
|
|
2670
|
+
id: "frames",
|
|
2671
|
+
description: "\u62BD\u53D6\u539F\u7247\u82E5\u5E72\u65F6\u523B\u7684\u9759\u5E27\uFF0C\u76F4\u63A5\u8FDB\u4F60\u7684\u4E0A\u4E0B\u6587\u4F9B\u4F60\u81EA\u5DF1\u770B\u3002\u6CA1\u6709\u58F0\u97F3\uFF0C\u4F46\u7CBE\u786E\u4E14\u4FBF\u5B9C\u3002",
|
|
2672
|
+
inputSchema: z2.object({
|
|
2673
|
+
timestamps: z2.array(z2.string()).min(1).max(6).describe("\u65F6\u523B\u5217\u8868\uFF0C`M:SS`"),
|
|
2674
|
+
note: z2.string().optional().describe("\u4F60\u60F3\u5728\u8FD9\u4E9B\u5E27\u91CC\u786E\u8BA4\u4EC0\u4E48")
|
|
2675
|
+
}),
|
|
2676
|
+
// The frames themselves reach the model through toModelOutput; the plain
|
|
2677
|
+
// return value is only what a transcript reader sees.
|
|
2678
|
+
execute: async ({ timestamps, note }) => {
|
|
2679
|
+
const images = [];
|
|
2680
|
+
const failed = [];
|
|
2681
|
+
for (const at of timestamps) {
|
|
2682
|
+
try {
|
|
2683
|
+
const out = path2.join(ctx.frameDir, `${ctx.episode}-${at.replace(/[:.]/g, "_")}.jpg`);
|
|
2684
|
+
await grabFrame(ctx.video, at, out);
|
|
2685
|
+
images.push({ at, data: fs4.readFileSync(out).toString("base64") });
|
|
2686
|
+
} catch (error) {
|
|
2687
|
+
failed.push(`${at}: ${error.message}`);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
return { images, failed, note: note ?? "" };
|
|
2691
|
+
},
|
|
2692
|
+
toModelOutput: ({ images, failed, note }) => ({
|
|
2693
|
+
type: "content",
|
|
2694
|
+
value: [
|
|
2695
|
+
{ type: "text", text: `\u539F\u7247\u9759\u5E27 ${images.map((i) => i.at).join(", ")}` + (note ? `\uFF08${note}\uFF09` : "") + (failed.length ? `
|
|
2696
|
+
\u62BD\u5E27\u5931\u8D25\uFF1A${failed.join("; ")}` : "") },
|
|
2697
|
+
...images.map((image) => ({
|
|
2698
|
+
type: "image-data",
|
|
2699
|
+
data: image.data,
|
|
2700
|
+
mimeType: "image/jpeg"
|
|
2701
|
+
}))
|
|
2702
|
+
]
|
|
2703
|
+
})
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
tools.finding = createTool({
|
|
2707
|
+
id: "finding",
|
|
2708
|
+
description: "\u8BB0\u5F55\u4E00\u6761\u5BA1\u6838\u7ED3\u8BBA\u3002\u6BCF\u6536\u53E3\u4E00\u4E2A\u95EE\u9898\u5C31\u8C03\u7528\u4E00\u6B21\uFF0C\u4E0D\u8981\u6512\u5230\u6700\u540E\u3002 outcome=fixed \u8868\u793A\u5DF2\u7ECF\u6539\u5B8C\u5E76\u8BFB\u56DE\uFF1Bunchanged \u8868\u793A\u6838\u5BF9\u540E\u65E0\u9700\u4FEE\u6539\uFF1B unresolved \u8868\u793A\u8BC1\u636E\u4E0D\u8DB3\u3001\u4FDD\u6301\u539F\u6837\u5E76\u8BF4\u660E\u539F\u56E0\u3002",
|
|
2709
|
+
inputSchema: FINDING_INPUT,
|
|
2710
|
+
execute: async (input) => {
|
|
2711
|
+
if (input.outcome === "fixed" && input.confidence === "low") {
|
|
2712
|
+
return "\u4F4E\u7F6E\u4FE1\u4E0D\u5F97\u81EA\u52A8\u4FEE\u6539\u3002\u8981\u4E48\u8865\u8DB3\u8BC1\u636E\u628A\u7F6E\u4FE1\u63D0\u5230 medium \u4EE5\u4E0A\uFF0C\u8981\u4E48\u628A\u8FD9\u6761\u8BB0\u4E3A unresolved\u3002";
|
|
2713
|
+
}
|
|
2714
|
+
if (input.outcome === "unresolved" && !input.unresolved_reason?.trim()) {
|
|
2715
|
+
return "unresolved \u5FC5\u987B\u5199 unresolved_reason\u3002";
|
|
2716
|
+
}
|
|
2717
|
+
const finding = toFinding(ctx.doc, input, ctx.episode, ctx.findings.length + 1);
|
|
2718
|
+
ctx.findings.push(finding);
|
|
2719
|
+
return `\u5DF2\u8BB0\u5F55 ${finding.id}\uFF08\u951A\u70B9 ${finding.anchor.episode_id ?? "?"}${finding.anchor.action_id ? "/" + finding.anchor.action_id : ""}\uFF09\u3002\u672C\u96C6\u5DF2\u8BB0 ${ctx.findings.length} \u6761\u3002`;
|
|
2720
|
+
}
|
|
2721
|
+
});
|
|
2722
|
+
return tools;
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
// packages/review-agent/src/agent.ts
|
|
2726
|
+
import { Agent } from "@mastra/core/agent";
|
|
2727
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
2728
|
+
import { z as z3 } from "zod";
|
|
2729
|
+
function seriesCard(doc) {
|
|
2730
|
+
return read(doc, "").split("\nepisodes\n")[0].trim();
|
|
2731
|
+
}
|
|
2732
|
+
function neighbourSynopses(doc, episode) {
|
|
2733
|
+
return doc.episodes.filter((ep) => Math.abs(ep.no - episode) <= 1 && ep.synopsis).map((ep) => `\u7B2C ${ep.no} \u96C6\uFF1A${ep.synopsis}`).join("\n");
|
|
2734
|
+
}
|
|
2735
|
+
function timeoutFinding(episode, minutes) {
|
|
2736
|
+
return {
|
|
2737
|
+
id: `episode-${episode}-timeout`,
|
|
2738
|
+
category: "other",
|
|
2739
|
+
title: "\u5BA1\u6838\u8D85\u65F6",
|
|
2740
|
+
issue: `\u7B2C ${episode} \u96C6\u5BA1\u6838\u8D85\u8FC7 ${minutes} \u5206\u949F`,
|
|
2741
|
+
impact: "\u672C\u96C6\u53EF\u80FD\u4ECD\u6709\u672A\u6536\u53E3\u7684\u5019\u9009",
|
|
2742
|
+
resolution: "\u5DF2\u4E2D\u65AD\u5E76\u4FDD\u7559\u6B64\u524D\u7ED3\u8BBA",
|
|
2743
|
+
outcome: "unresolved",
|
|
2744
|
+
confidence: "low",
|
|
2745
|
+
anchor: {
|
|
2746
|
+
episode_number: episode,
|
|
2747
|
+
episode_id: null,
|
|
2748
|
+
scene_number: null,
|
|
2749
|
+
scene_id: null,
|
|
2750
|
+
action_id: null,
|
|
2751
|
+
timestamp: null,
|
|
2752
|
+
end_timestamp: null
|
|
2753
|
+
},
|
|
2754
|
+
subject: null,
|
|
2755
|
+
before: null,
|
|
2756
|
+
after: null,
|
|
2757
|
+
unresolved_reason: `\u5355\u96C6\u5BA1\u6838\u8D85\u8FC7 ${minutes} \u5206\u949F\u786C\u8D85\u65F6`,
|
|
2758
|
+
evidence: []
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
async function reviewUnit(doc, episode, video, config) {
|
|
2762
|
+
const started = Date.now();
|
|
2763
|
+
const ctx = newContext({
|
|
2764
|
+
doc,
|
|
2765
|
+
episode,
|
|
2766
|
+
phase: episode === 0 ? "converge" : "episode",
|
|
2767
|
+
...video ? { video, geminiKey: config.apiKey } : {},
|
|
2768
|
+
mediaModel: config.mediaModel,
|
|
2769
|
+
mediaBudget: config.mediaBudget
|
|
2770
|
+
});
|
|
2771
|
+
const google = createGoogleGenerativeAI({
|
|
2772
|
+
apiKey: config.apiKey,
|
|
2773
|
+
...config.baseUrl ? { baseURL: config.baseUrl } : {},
|
|
2774
|
+
fetch: ((url, init) => fetch(url, {
|
|
2775
|
+
...init,
|
|
2776
|
+
...dispatcher() ? { dispatcher: dispatcher() } : {}
|
|
2777
|
+
}))
|
|
2778
|
+
});
|
|
2779
|
+
const input = {
|
|
2780
|
+
seriesCard: seriesCard(doc),
|
|
2781
|
+
episode,
|
|
2782
|
+
...video ? { video } : {},
|
|
2783
|
+
mediaBudget: config.mediaBudget,
|
|
2784
|
+
neighbours: neighbourSynopses(doc, episode)
|
|
2785
|
+
};
|
|
2786
|
+
const agent = new Agent({
|
|
2787
|
+
id: `review-ep${episode}`,
|
|
2788
|
+
name: `Script review \xB7 episode ${episode}`,
|
|
2789
|
+
instructions: systemPrompt(input),
|
|
2790
|
+
model: google(config.model),
|
|
2791
|
+
tools: buildTools(ctx)
|
|
2792
|
+
});
|
|
2793
|
+
let state2 = "completed";
|
|
2794
|
+
let message = "";
|
|
2795
|
+
let error = null;
|
|
2796
|
+
let steps = 0;
|
|
2797
|
+
const usage = [];
|
|
2798
|
+
const minutes = Math.round(config.timeoutMs / 6e4);
|
|
2799
|
+
try {
|
|
2800
|
+
const response = await agent.generate(episodeTask(input), {
|
|
2801
|
+
maxSteps: config.maxSteps,
|
|
2802
|
+
modelSettings: { temperature: 0, timeout: { totalMs: config.timeoutMs } },
|
|
2803
|
+
structuredOutput: {
|
|
2804
|
+
schema: z3.object({ message: z3.string().describe("\u4E00\u4E24\u53E5\u8BDD\u603B\u7ED3\u672C\u96C6\u5BA1\u6838\u7ED3\u679C") }),
|
|
2805
|
+
errorStrategy: "warn"
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
steps = response?.steps?.length ?? 0;
|
|
2809
|
+
message = response?.object?.message || response?.text?.trim() || `\u7B2C ${episode} \u96C6\u5BA1\u6838\u5B8C\u6210`;
|
|
2810
|
+
const u = response?.usage ?? {};
|
|
2811
|
+
usage.push({
|
|
2812
|
+
call_key: `review:ep${episode}:agent`,
|
|
2813
|
+
provider: "google-gemini",
|
|
2814
|
+
model: config.model,
|
|
2815
|
+
input_tokens: Number(u.inputTokens ?? u.promptTokens ?? 0),
|
|
2816
|
+
cached_input_tokens: Number(u.cachedInputTokens ?? 0),
|
|
2817
|
+
output_tokens: Number(u.outputTokens ?? u.completionTokens ?? 0),
|
|
2818
|
+
reasoning_tokens: Number(u.reasoningTokens ?? 0)
|
|
2819
|
+
});
|
|
2820
|
+
} catch (thrown) {
|
|
2821
|
+
const detail = thrown instanceof Error ? thrown.message : String(thrown);
|
|
2822
|
+
const timedOut = /timeout|aborted|AbortError/i.test(detail);
|
|
2823
|
+
state2 = timedOut ? "completed" : "failed";
|
|
2824
|
+
error = detail.slice(0, 300);
|
|
2825
|
+
message = timedOut ? `\u5BA1\u6838\u5728 ${minutes} \u5206\u949F\u8D85\u65F6\u540E\u4E2D\u65AD\uFF0C\u5DF2\u4FDD\u7559\u6B64\u524D\u7ED3\u8BBA` : `\u7B2C ${episode} \u96C6\u5BA1\u6838\u5931\u8D25`;
|
|
2826
|
+
if (timedOut) ctx.findings.push(timeoutFinding(episode, minutes));
|
|
2827
|
+
}
|
|
2828
|
+
for (const media of ctx.usage) {
|
|
2829
|
+
usage.push({
|
|
2830
|
+
call_key: media.call_key,
|
|
2831
|
+
provider: media.provider,
|
|
2832
|
+
model: media.model,
|
|
2833
|
+
input_tokens: media.input_tokens,
|
|
2834
|
+
cached_input_tokens: media.cached_input_tokens,
|
|
2835
|
+
output_tokens: media.output_tokens,
|
|
2836
|
+
reasoning_tokens: media.reasoning_tokens
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
return {
|
|
2840
|
+
episode_number: episode,
|
|
2841
|
+
state: state2,
|
|
2842
|
+
message,
|
|
2843
|
+
error,
|
|
2844
|
+
findings: ctx.findings,
|
|
2845
|
+
wall_ms: Date.now() - started,
|
|
2846
|
+
steps,
|
|
2847
|
+
media_calls: ctx.mediaCalls,
|
|
2848
|
+
usage
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// packages/review-agent/src/review.ts
|
|
2853
|
+
import * as fs5 from "node:fs";
|
|
2854
|
+
import * as path3 from "node:path";
|
|
2855
|
+
function episodeVideos(source, count) {
|
|
2856
|
+
if (!source) return Array.from({ length: count }, () => void 0);
|
|
2857
|
+
const stat = fs5.statSync(source);
|
|
2858
|
+
if (stat.isFile()) return Array.from({ length: count }, () => source);
|
|
2859
|
+
const files = fs5.readdirSync(source).filter((name) => /\.(mp4|mov|mkv|m4v)$/i.test(name)).sort().map((name) => path3.join(source, name));
|
|
2860
|
+
if (files.length < count) {
|
|
2861
|
+
throw new Error(`${source} holds ${files.length} videos for ${count} episodes`);
|
|
2862
|
+
}
|
|
2863
|
+
return files.slice(0, count);
|
|
2864
|
+
}
|
|
2865
|
+
function statistics(units) {
|
|
2866
|
+
const findings = units.flatMap((unit) => unit.findings);
|
|
2867
|
+
return {
|
|
2868
|
+
fixed: findings.filter((f) => f.outcome === "fixed").length,
|
|
2869
|
+
unchanged: findings.filter((f) => f.outcome === "unchanged").length,
|
|
2870
|
+
unresolved: findings.filter((f) => f.outcome === "unresolved").length,
|
|
2871
|
+
failed_episodes: units.filter((u) => u.state === "failed" && u.episode_number > 0).length
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
async function pool(items, limit, run2) {
|
|
2875
|
+
const queue = [...items];
|
|
2876
|
+
const workers = Array.from({ length: Math.max(1, Math.min(limit, queue.length)) }, async () => {
|
|
2877
|
+
for (; ; ) {
|
|
2878
|
+
const item = queue.shift();
|
|
2879
|
+
if (item === void 0) return;
|
|
2880
|
+
await run2(item);
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2883
|
+
await Promise.all(workers);
|
|
2884
|
+
}
|
|
2885
|
+
async function reviewScript(opts) {
|
|
2886
|
+
const started = Date.now();
|
|
2887
|
+
const doc = parse(JSON.parse(fs5.readFileSync(opts.scriptPath, "utf8")));
|
|
2888
|
+
const config = {
|
|
2889
|
+
apiKey: opts.apiKey,
|
|
2890
|
+
model: opts.model ?? "gemini-3.8-flash",
|
|
2891
|
+
mediaModel: opts.mediaModel ?? opts.model ?? "gemini-3.8-flash",
|
|
2892
|
+
...opts.baseUrl ? { baseUrl: opts.baseUrl } : {},
|
|
2893
|
+
maxSteps: opts.maxSteps ?? 60,
|
|
2894
|
+
mediaBudget: opts.mediaBudget ?? 1,
|
|
2895
|
+
timeoutMs: opts.episodeTimeoutMs ?? 8 * 6e4
|
|
2896
|
+
};
|
|
2897
|
+
const all = doc.episodes.map((ep) => ep.no);
|
|
2898
|
+
const selected = opts.episodes?.length ? opts.episodes.filter((n) => all.includes(n)) : all;
|
|
2899
|
+
const videos = opts.videos?.length ? opts.videos : episodeVideos(opts.source, doc.episodes.length);
|
|
2900
|
+
const events = [];
|
|
2901
|
+
const emit = (event) => {
|
|
2902
|
+
events.push(event);
|
|
2903
|
+
opts.onEvent?.(event);
|
|
2904
|
+
};
|
|
2905
|
+
const units = [];
|
|
2906
|
+
await pool(selected, opts.concurrency ?? 5, async (episode) => {
|
|
2907
|
+
const unit = await reviewUnit(doc, episode, videos[episode - 1], config);
|
|
2908
|
+
units.push(unit);
|
|
2909
|
+
emit({ type: "episode", ...unit });
|
|
2910
|
+
});
|
|
2911
|
+
if (opts.converge !== false) {
|
|
2912
|
+
const unit = await reviewUnit(doc, 0, void 0, config);
|
|
2913
|
+
units.push(unit);
|
|
2914
|
+
emit({ type: "global", ...unit });
|
|
2915
|
+
}
|
|
2916
|
+
commit(doc, opts.scriptPath);
|
|
2917
|
+
const usage_total = units.flatMap((unit) => unit.usage);
|
|
2918
|
+
emit({
|
|
2919
|
+
type: "done",
|
|
2920
|
+
statistics: statistics(units),
|
|
2921
|
+
usage_total,
|
|
2922
|
+
wall_ms: Date.now() - started
|
|
2923
|
+
});
|
|
2924
|
+
return events;
|
|
2925
|
+
}
|
|
2926
|
+
export {
|
|
2927
|
+
CATEGORIES,
|
|
2928
|
+
CLIP_FPS,
|
|
2929
|
+
CONFIDENCES,
|
|
2930
|
+
EVIDENCE_KINDS,
|
|
2931
|
+
FINDING_INPUT,
|
|
2932
|
+
MAX_WINDOW_S,
|
|
2933
|
+
OUTCOMES,
|
|
2934
|
+
SUBJECT_KINDS,
|
|
2935
|
+
anchorOf,
|
|
2936
|
+
buildTools,
|
|
2937
|
+
cutClip,
|
|
2938
|
+
dispatcher,
|
|
2939
|
+
episodeTask,
|
|
2940
|
+
episodeVideos,
|
|
2941
|
+
ffmpegArgs,
|
|
2942
|
+
grabFrame,
|
|
2943
|
+
neighbourSynopses,
|
|
2944
|
+
newContext,
|
|
2945
|
+
post,
|
|
2946
|
+
proxyUrl,
|
|
2947
|
+
reviewScript,
|
|
2948
|
+
reviewUnit,
|
|
2949
|
+
seconds,
|
|
2950
|
+
seriesCard,
|
|
2951
|
+
systemPrompt,
|
|
2952
|
+
toFinding,
|
|
2953
|
+
traceKey,
|
|
2954
|
+
watchWindow
|
|
2955
|
+
};
|
|
2956
|
+
//# sourceMappingURL=review-agent.js.map
|