@lingjingai/scriptctl 0.49.6 → 0.49.8

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.
Files changed (49) hide show
  1. package/changes/0.49.7.md +14 -0
  2. package/changes/0.49.8.md +13 -0
  3. package/dist/cli.js +39 -28
  4. package/dist/cli.js.map +1 -1
  5. package/dist/common.js +2 -4
  6. package/dist/common.js.map +1 -1
  7. package/dist/domain/script/patch/helpers.d.ts +8 -1
  8. package/dist/domain/script/patch/helpers.js +79 -7
  9. package/dist/domain/script/patch/helpers.js.map +1 -1
  10. package/dist/domain/script/patch/ops-asset.js +34 -10
  11. package/dist/domain/script/patch/ops-asset.js.map +1 -1
  12. package/dist/domain/script/patch/ops-scene.js +6 -1
  13. package/dist/domain/script/patch/ops-scene.js.map +1 -1
  14. package/dist/domain/script/patch/ops-state.js +28 -6
  15. package/dist/domain/script/patch/ops-state.js.map +1 -1
  16. package/dist/domain/script/schema.js +3 -3
  17. package/dist/domain/script/schema.js.map +1 -1
  18. package/dist/help-text.js +58 -31
  19. package/dist/help-text.js.map +1 -1
  20. package/dist/infra/script-output-api.d.ts +9 -6
  21. package/dist/infra/script-output-api.js +16 -3
  22. package/dist/infra/script-output-api.js.map +1 -1
  23. package/dist/infra/script-output-store.d.ts +16 -0
  24. package/dist/infra/script-output-store.js.map +1 -1
  25. package/dist/infra/script-working-copy.d.ts +22 -0
  26. package/dist/infra/script-working-copy.js +155 -0
  27. package/dist/infra/script-working-copy.js.map +1 -0
  28. package/dist/usecases/ingest/publish-command.d.ts +1 -4
  29. package/dist/usecases/ingest/publish-command.js +3 -76
  30. package/dist/usecases/ingest/publish-command.js.map +1 -1
  31. package/dist/usecases/script/context.js +7 -1
  32. package/dist/usecases/script/context.js.map +1 -1
  33. package/dist/usecases/script/create.js +12 -48
  34. package/dist/usecases/script/create.js.map +1 -1
  35. package/dist/usecases/script/lib.d.ts +2 -0
  36. package/dist/usecases/script/lib.js +35 -5
  37. package/dist/usecases/script/lib.js.map +1 -1
  38. package/dist/usecases/script/session.d.ts +2 -1
  39. package/dist/usecases/script/session.js +18 -49
  40. package/dist/usecases/script/session.js.map +1 -1
  41. package/dist/usecases/script/working-copy.d.ts +5 -0
  42. package/dist/usecases/script/working-copy.js +256 -0
  43. package/dist/usecases/script/working-copy.js.map +1 -0
  44. package/package.json +3 -1
  45. package/scripts/install-skill.mjs +3 -3
  46. package/skills/scriptctl/SKILL.md +28 -30
  47. package/skills/scriptctl/references/atomic-write-workflow.md +20 -10
  48. package/skills/scriptctl/references/ingest-workflow.md +8 -9
  49. package/skills/scriptctl/references/production-writing-standard.md +8 -8
@@ -0,0 +1,256 @@
1
+ import * as fs from "node:fs";
2
+ import { CliError, EXIT_INPUT, EXIT_OK, EXIT_RUNTIME, scriptJsonPath, } from "../../common.js";
3
+ import { isDict, strOf } from "../../domain/script/shared.js";
4
+ import { remoteScriptOutputStoreFromEnv, ScriptOutputApiError } from "../../infra/script-output-api.js";
5
+ import { assertLocalFileFingerprint, assertScriptUnchanged, atomicWriteJson, localFileFingerprint, readWorkingCopyMetadata, semanticScriptHash, workingCopyPaths, writeWorkingCopyMetadata, } from "../../infra/script-working-copy.js";
6
+ import { apiErrorToCli, validateScriptV3Report } from "./session.js";
7
+ function configuredProjectGroupNo(opts) {
8
+ return strOf(opts["project_group_no"] || process.env.SANDBOX_PROJECT_GROUP_NO).trim();
9
+ }
10
+ function remoteClient(projectGroupNo) {
11
+ try {
12
+ return remoteScriptOutputStoreFromEnv(projectGroupNo);
13
+ }
14
+ catch (error) {
15
+ if (error instanceof ScriptOutputApiError) {
16
+ throw apiErrorToCli("SCRIPT API BLOCKED: gateway not configured", error);
17
+ }
18
+ throw error;
19
+ }
20
+ }
21
+ function clientForMetadata(opts, metadata) {
22
+ const configuredProject = configuredProjectGroupNo(opts);
23
+ if (configuredProject && configuredProject !== metadata.projectGroupNo) {
24
+ throw remoteMismatch(metadata, `configured projectGroupNo=${configuredProject}`);
25
+ }
26
+ const client = remoteClient(metadata.projectGroupNo);
27
+ if (client.gatewayBase !== metadata.apiBaseUrl) {
28
+ throw remoteMismatch(metadata, `configured apiBaseUrl=${client.gatewayBase}`);
29
+ }
30
+ return client;
31
+ }
32
+ function remoteMismatch(metadata, received) {
33
+ return new CliError("SCRIPT BLOCKED: working copy remote mismatch", "The configured remote does not match this working copy.", {
34
+ exitCode: EXIT_INPUT,
35
+ errorCode: "WORKING_COPY_REMOTE_MISMATCH",
36
+ required: [`projectGroupNo=${metadata.projectGroupNo}`, `apiBaseUrl=${metadata.apiBaseUrl}`],
37
+ received: [received],
38
+ nextSteps: ["Restore the checkout environment for this working copy, or checkout another file."],
39
+ });
40
+ }
41
+ function readLocalScript(scriptPath) {
42
+ let value;
43
+ try {
44
+ value = JSON.parse(fs.readFileSync(scriptPath, "utf-8"));
45
+ }
46
+ catch (error) {
47
+ throw new CliError("SCRIPT BLOCKED: script JSON invalid", "The local script is missing or invalid JSON.", {
48
+ exitCode: EXIT_INPUT,
49
+ errorCode: "SCRIPT_JSON_INVALID",
50
+ received: [scriptPath, error instanceof Error ? error.message : String(error)],
51
+ nextSteps: ["Fix the local script JSON before continuing."],
52
+ });
53
+ }
54
+ if (!isDict(value)) {
55
+ throw new CliError("SCRIPT BLOCKED: script root invalid", "The script root must be a JSON object.", {
56
+ exitCode: EXIT_INPUT,
57
+ errorCode: "SCRIPT_ROOT_INVALID",
58
+ received: [scriptPath, Array.isArray(value) ? "array" : typeof value],
59
+ nextSteps: ["Replace the file with a valid Script v3 JSON object."],
60
+ });
61
+ }
62
+ return value;
63
+ }
64
+ export async function commandCheckout(opts) {
65
+ const scriptPath = scriptJsonPath(opts);
66
+ const discardLocal = Boolean(opts["discard_local"]);
67
+ const paths = workingCopyPaths(scriptPath);
68
+ const metadata = readWorkingCopyMetadata(scriptPath);
69
+ const existed = fs.existsSync(scriptPath);
70
+ const beforeFingerprint = localFileFingerprint(scriptPath);
71
+ if (existed && metadata === null && !discardLocal) {
72
+ throw new CliError("CHECKOUT BLOCKED: local target exists", "The target is an unmanaged local script.", {
73
+ exitCode: EXIT_INPUT,
74
+ errorCode: "LOCAL_TARGET_EXISTS",
75
+ received: [scriptPath],
76
+ nextSteps: [`Run \`scriptctl checkout --script-path ${scriptPath} --discard-local\` only if the local file may be discarded.`],
77
+ });
78
+ }
79
+ if (existed && metadata !== null && !discardLocal) {
80
+ const currentHash = semanticScriptHash(readLocalScript(scriptPath));
81
+ if (currentHash !== metadata.baseContentHash) {
82
+ throw new CliError("CHECKOUT BLOCKED: local changes exist", "The working copy has unpublished local changes.", {
83
+ exitCode: EXIT_INPUT,
84
+ errorCode: "WORKING_COPY_DIRTY",
85
+ received: [scriptPath, `baseRevision=${metadata.baseRevision}`],
86
+ nextSteps: ["Publish the local changes, or pass --discard-local to replace them with the remote snapshot."],
87
+ });
88
+ }
89
+ }
90
+ const client = metadata !== null && !discardLocal
91
+ ? clientForMetadata(opts, metadata)
92
+ : remoteClient(configuredProjectGroupNo(opts) || null);
93
+ let snapshot;
94
+ try {
95
+ snapshot = await client.getSnapshot();
96
+ }
97
+ catch (error) {
98
+ if (error instanceof ScriptOutputApiError) {
99
+ throw apiErrorToCli("CHECKOUT BLOCKED: snapshot query failed", error);
100
+ }
101
+ throw error;
102
+ }
103
+ if (snapshot === null || !isDict(snapshot.script)) {
104
+ throw new CliError("CHECKOUT BLOCKED: remote script not found", "No remote script_v2 snapshot exists for this project.", {
105
+ exitCode: EXIT_INPUT,
106
+ errorCode: "REMOTE_SCRIPT_NOT_FOUND",
107
+ received: [`projectGroupNo=${client.projectGroupNo}`],
108
+ nextSteps: ["Use the ingest/create workflow and publish the first script instead of checkout."],
109
+ });
110
+ }
111
+ assertLocalFileFingerprint(scriptPath, beforeFingerprint);
112
+ const baseContentHash = semanticScriptHash(snapshot.script);
113
+ atomicWriteJson(scriptPath, snapshot.script);
114
+ writeWorkingCopyMetadata(scriptPath, {
115
+ schemaVersion: 1,
116
+ projectGroupNo: client.projectGroupNo,
117
+ apiBaseUrl: client.gatewayBase,
118
+ baseRevision: Number(snapshot.revision),
119
+ baseContentHash,
120
+ });
121
+ return [{
122
+ title: "CHECKOUT COMPLETE",
123
+ changed: true,
124
+ scriptPath,
125
+ managed: true,
126
+ dirty: false,
127
+ discardedLocal: discardLocal && existed,
128
+ projectGroupNo: client.projectGroupNo,
129
+ baseRevision: Number(snapshot.revision),
130
+ artifacts: [scriptPath, paths.metadataPath],
131
+ result: [
132
+ discardLocal && existed
133
+ ? `discarded the local file and checked out revision ${snapshot.revision} to ${scriptPath}`
134
+ : `checked out revision ${snapshot.revision} to ${scriptPath}`,
135
+ ],
136
+ }, EXIT_OK];
137
+ }
138
+ export function commandWorkingCopyStatus(opts) {
139
+ const scriptPath = scriptJsonPath(opts);
140
+ const exists = fs.existsSync(scriptPath);
141
+ const metadata = readWorkingCopyMetadata(scriptPath);
142
+ const dirty = exists && metadata !== null
143
+ ? semanticScriptHash(readLocalScript(scriptPath)) !== metadata.baseContentHash
144
+ : null;
145
+ return [{
146
+ title: "WORKING COPY STATUS",
147
+ scriptPath,
148
+ exists,
149
+ managed: metadata !== null,
150
+ dirty,
151
+ ...(metadata === null ? {} : {
152
+ projectGroupNo: metadata.projectGroupNo,
153
+ apiBaseUrl: metadata.apiBaseUrl,
154
+ baseRevision: metadata.baseRevision,
155
+ }),
156
+ result: [metadata === null
157
+ ? `${scriptPath}: unmanaged local script`
158
+ : `${scriptPath}: ${dirty ? "dirty" : "clean"} at revision ${metadata.baseRevision}`],
159
+ }, EXIT_OK];
160
+ }
161
+ export async function commandWorkingCopyPublish(opts) {
162
+ const scriptPath = scriptJsonPath(opts);
163
+ const script = readLocalScript(scriptPath);
164
+ const scriptHash = semanticScriptHash(script);
165
+ const metadata = readWorkingCopyMetadata(scriptPath);
166
+ if (metadata !== null && metadata.baseContentHash === scriptHash) {
167
+ return [{
168
+ title: "PUBLISH NO CHANGES",
169
+ changed: false,
170
+ published: false,
171
+ reason: "NO_CHANGES",
172
+ revision: metadata.baseRevision,
173
+ scriptPath,
174
+ result: [`no local changes; revision remains ${metadata.baseRevision}`],
175
+ }, EXIT_OK];
176
+ }
177
+ const validation = validateScriptV3Report(script);
178
+ if (!Boolean(validation["passed"])) {
179
+ throw new CliError("PUBLISH BLOCKED: v3 validation failed", "The local script does not pass Script v3 validation.", {
180
+ exitCode: EXIT_INPUT,
181
+ errorCode: "PUBLISH_VALIDATION_FAILED",
182
+ received: validation["issues"].slice(0, 5).map((issue) => `${issue["path"] ?? "<root>"}: ${issue["summary"]}`),
183
+ nextSteps: [`Run \`scriptctl validate --script-path ${scriptPath}\`, repair the issues, then publish again.`],
184
+ });
185
+ }
186
+ const client = metadata === null
187
+ ? remoteClient(configuredProjectGroupNo(opts) || null)
188
+ : clientForMetadata(opts, metadata);
189
+ const baseRevision = metadata?.baseRevision ?? 0;
190
+ assertScriptUnchanged(scriptPath, scriptHash);
191
+ let response;
192
+ try {
193
+ response = await client.publishScript({
194
+ baseRevision,
195
+ script,
196
+ validation: {
197
+ status: "valid",
198
+ summary: {
199
+ passed: true,
200
+ hasBlocking: false,
201
+ stats: validation["stats"] ?? {},
202
+ issueCount: 0,
203
+ },
204
+ },
205
+ });
206
+ }
207
+ catch (error) {
208
+ if (error instanceof ScriptOutputApiError) {
209
+ if (String(error.code) === "9002" || error.message.includes("版本冲突")) {
210
+ throw apiErrorToCli("PUBLISH BLOCKED: revision conflict", error);
211
+ }
212
+ const ambiguousSuccessResponse = error.status !== null
213
+ && error.status >= 200
214
+ && error.status < 300
215
+ && error.code === null;
216
+ if (error.status === null || error.status >= 500 || ambiguousSuccessResponse) {
217
+ throw new CliError("PUBLISH UNKNOWN: response not confirmed", "The publish outcome is unknown because no definitive response was received.", {
218
+ exitCode: EXIT_RUNTIME,
219
+ errorCode: "PUBLISH_OUTCOME_UNKNOWN",
220
+ received: [error.message, `baseRevision=${baseRevision}`],
221
+ nextSteps: [`Inspect the remote script, then run \`scriptctl checkout --script-path ${scriptPath} --discard-local\` to recover when appropriate.`],
222
+ });
223
+ }
224
+ throw apiErrorToCli("PUBLISH BLOCKED: write failed", error);
225
+ }
226
+ throw error;
227
+ }
228
+ const revision = isDict(response) ? Number(response["revision"]) : Number.NaN;
229
+ if (!Number.isInteger(revision) || revision <= baseRevision) {
230
+ throw new CliError("PUBLISH UNKNOWN: response invalid", "The gateway returned an invalid publish revision.", {
231
+ exitCode: EXIT_RUNTIME,
232
+ errorCode: "PUBLISH_OUTCOME_UNKNOWN",
233
+ received: [JSON.stringify(response)],
234
+ nextSteps: [`Inspect the remote script before retrying or discarding ${scriptPath}.`],
235
+ });
236
+ }
237
+ writeWorkingCopyMetadata(scriptPath, {
238
+ schemaVersion: 1,
239
+ projectGroupNo: client.projectGroupNo,
240
+ apiBaseUrl: client.gatewayBase,
241
+ baseRevision: revision,
242
+ baseContentHash: scriptHash,
243
+ });
244
+ return [{
245
+ title: "PUBLISH COMPLETE",
246
+ changed: true,
247
+ published: true,
248
+ created: metadata === null,
249
+ scriptPath,
250
+ projectGroupNo: client.projectGroupNo,
251
+ baseRevision,
252
+ revision,
253
+ result: [`published revision ${baseRevision} → ${revision}`],
254
+ }, EXIT_OK];
255
+ }
256
+ //# sourceMappingURL=working-copy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"working-copy.js","sourceRoot":"","sources":["../../../src/usecases/script/working-copy.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingjingai/scriptctl",
3
- "version": "0.49.6",
3
+ "version": "0.49.8",
4
4
  "description": "剧本阶段统一 CLI:直转外部素材到 script.json,以及当前最终剧本的读取、校验、原子编辑与批量 patch 精修。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,10 +41,12 @@
41
41
  "https-proxy-agent": "^9.1.0",
42
42
  "jszip": "^3.10.1",
43
43
  "pdfjs-dist": "^4.10.38",
44
+ "proper-lockfile": "^4.1.2",
44
45
  "yaml": "^2.8.1"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@types/node": "^20.14.0",
49
+ "@types/proper-lockfile": "^4.1.4",
48
50
  "c8": "^11.0.0",
49
51
  "typescript": "^5.5.0",
50
52
  "vitest": "^2.0.0"
@@ -49,13 +49,13 @@ function readVersion(root) {
49
49
  }
50
50
  }
51
51
 
52
- // 把源 SKILL.md 里的 `scriptctl_version:` frontmatter 值改成实际安装的版本,让装好的
52
+ // 把源 SKILL.md metadata 里的 `scriptctl_version:` 值改成实际安装的版本,让装好的
53
53
  // skill 一眼看清对应的是哪个 scriptctl。没有该行时不硬插(保持 frontmatter 干净)。
54
54
  function stampVersion(skillMdPath, version) {
55
55
  try {
56
56
  const text = fs.readFileSync(skillMdPath, "utf-8");
57
- if (!/^scriptctl_version:/m.test(text)) return;
58
- const stamped = text.replace(/^scriptctl_version:.*$/m, `scriptctl_version: "${version}"`);
57
+ if (!/^\s+scriptctl_version:/m.test(text)) return;
58
+ const stamped = text.replace(/^(\s+)scriptctl_version:.*$/m, `$1scriptctl_version: "${version}"`);
59
59
  fs.writeFileSync(skillMdPath, stamped, "utf-8");
60
60
  } catch {
61
61
  /* 戳版本失败不影响 skill 可用 */
@@ -1,14 +1,15 @@
1
1
  ---
2
2
  name: scriptctl
3
- description: "剧本 script.json 的读写 + 入库 CLI。转剧本:ingest(txt/md/docx 或视频,自动分流,产出 workspace/script.json)→ 标准化复核 → view 自查 → publish 入库。读:集/场/action/资产/状态/校验/时间戳/出场统计。写:台词/类型/归属/scene-ref 场景造型/内联发声源/importance/资产。选择器批量精修(读写共享 --in/谓词)、动词脚本 do(异构批量一事务)、结构调整(插/删/移/分/合)。从零创作、视频提取、小说与非标准剧本解析、洗稿/对标/转绘统一执行资产与 state 先行、场景引用完整、生产可执行正文规范。ingest status 从工作区真实文件报进度。沙箱里默认读写项目 DB;本地电脑默认 --script-path 直接读写本地 script.json。剧本阶段做任何剧本相关读写、以及操作两个 script.json 之前都应加载。正文由你用原子能力写入。"
4
- version: "0.23.0"
5
- scriptctl_version: "0.49.2"
6
- author: "official"
3
+ description: "剧本 script.json 的本地读写 + 入库 CLI。远端剧本先 checkout 成工作副本,所有原子读写只改本地 script.json,完成后 publish 严格按 revision 入库;新增 op 无需新增业务 API。转剧本:ingest(txt/md/docx 或视频,产出 workspace/script.json)→ 标准化复核 → view 自查 → publish。覆盖集/场/action/资产/state/scene-ref/结构调整/批量 do/patch/校验。剧本阶段做任何剧本相关读写、以及操作两个 script.json 之前都应加载。"
4
+ metadata:
5
+ version: "0.24.0"
6
+ scriptctl_version: "0.49.8"
7
+ author: "official"
7
8
  ---
8
9
 
9
10
  # scriptctl
10
11
 
11
- `scriptctl` 读写结构化剧本 `script.json`(Schema v3:资产 人物/场景/道具 + 分集→场景→action + 场景级 cast/造型 + 内联发声源 + 引用完整性 + 校验),并把外部素材**转成剧本、入库**。**每次调用只作用于一份剧本**(由目标 flag 选定),所以「换剧本 = 换目标」。
12
+ `scriptctl` 读写结构化剧本 `script.json`,并把外部素材转成剧本、入库。查询和原子编辑始终作用于本地文件;远端只在 `checkout` / `publish` 边界访问。
12
13
 
13
14
  两条主线:
14
15
  - **转剧本入库**:`ingest`(抽取,产出 `workspace/script.json`)→ 标准化复核 → `view`(自查)→ `publish`(入库)。见 [references/ingest-workflow.md](references/ingest-workflow.md)。
@@ -21,7 +22,7 @@ author: "official"
21
22
  1. **忠实理解输入**:创作时落实题材、集纲和人物关系;提取与解析时保留源素材可验证的对白、动作、顺序、场界和状态变化。把不确定信息标为待复核,不补写源素材未表达的事实。
22
23
  2. **注册资产**:先读现有人物、地点、道具;正文将出现的资产先用 `add-actor / add-location / add-prop` 注册。人物保留一个稳定的 `actor_name` 作为真名,称呼、绰号、职位放 `aliases[]`,同一人物共用一个 actor。
23
24
  3. **建好资产状态**:每个新资产自带 `default` state;先用 `state-rename` + `describe <kind:id/default>` 定义常态,持久、可复用、需要独立生成视觉资产的变体再用 `state-add` 建立。
24
- 4. **建立真实场并绑定资产**:`insert <ep> --location` 建 scene 后,用 `scene-ref <ep/scn> <kind:id> --state <state_id>` 绑定本场地点、全部人物、全部道具及其实际状态。正文中出现的资产属于本场引用集合。
25
+ 4. **建立真实场并绑定资产**:`insert <ep> --location` 建 scene 后,用 `scene-ref <ep/scn> <kind:id> --state <state_id>` 绑定本场地点、全部人物、全部道具及其实际状态。同一人物需要多份场景引用时,用 `--append` 逐份追加;正文中出现的资产属于本场引用集合。
25
26
  5. **写入或标准化正文**:资产批次落盘并读回确认后再写正文。发现新资产或新 state 时,先完成资产和 scene-ref,再继续正文。
26
27
  6. **逐场复核**:每批正文落盘后读回 `scenes / actions`;一集完成后运行 `validate`。`validate` 通过只代表结构与引用合法,仍需完成内容标准和源素材一致性复核。
27
28
 
@@ -36,29 +37,23 @@ author: "official"
36
37
 
37
38
  ### 场界判定
38
39
 
39
- 场是时间、空间和生产状态连续的单元。同一地点、同一连续时段、同一叙事线、同一组视觉状态且动作未中断时,继续向当前 scene 追加;笑点升级、攻防回合、角色进出和剧情节拍不是分场理由。地点切换、明显跳时、切到另一条同时叙事线、明确转场,或人物/地点/道具切换到另一项持久视觉 state 时建立新 scene。一集可以只有一场。
40
+ 场是时间、空间和叙事连续的单元。同一地点、同一连续时段、同一叙事线且动作未中断时,继续向当前 scene 追加;笑点升级、攻防回合、角色进出、剧情节拍和同场人物造型变化不是分场理由。地点切换、明显跳时、切到另一条同时叙事线或明确转场时建立新 scene。一集可以只有一场。
40
41
 
41
- location 表示可独立布置、可独立生成参考图、可重复引用的稳定物理空间;客厅、卧室、卫生间、大厅、广场分别注册。相关空间使用“豪宅·客厅”“豪宅·卧室”这类统一命名表达归属,当前 Schema 不提供地点组或父地点。每个 scene 只支持一个 location 和每项资产的一个 state;需要一个镜头同时引用多个地点图或同一资产的多个状态图时,报告当前结构限制。
42
+ location 表示可独立布置、可独立生成参考图、可重复引用的稳定物理空间;客厅、卧室、卫生间、大厅、广场分别注册。相关空间使用“豪宅·客厅”“豪宅·卧室”这类统一命名表达归属,当前 Schema 不提供地点组或父地点。每个 scene 只支持一个 location;人物引用可以重复,同一 actor 可按实际画面追加同 state 或不同 state 的多份引用。
42
43
 
43
44
  跨地点蒙太奇按剪辑顺序建立多个短 scene,每个 scene 绑定自己的地点、实际人物、道具和 state,并用 transition 明确蒙太奇开始、切换和结束。纯空镜 scene 绑定实际 location 与关键道具即可,不创建虚假人物。
44
45
 
45
- 人物、地点或关键道具发生持久视觉状态变化时,变化前 scene 绑定旧 state,变化动作写在该 scene 末尾,变化后 scene 绑定新 state;同地点、同时段的状态边界保持独立。收尾时用 `scenes --in <ep>` 复核相邻场界,只合并地点、时段、叙事线和全部资产 state 均连续的误拆场。完整命令顺序见 [references/atomic-write-workflow.md](references/atomic-write-workflow.md)。
46
+ 人物在同一连续场内需要多份视觉参考时,先绑定第一份引用,再用 `scene-ref ... actor:<id> --state <state_id> --append` 追加其余引用;用 `--occurrence <n>` 精确修改或删除第 n 份同人物引用。地点切换、跳时或叙事线切换仍建立新 scene。收尾时用 `scenes --in <ep>` 复核相邻场界。完整命令顺序见 [references/atomic-write-workflow.md](references/atomic-write-workflow.md)。
46
47
 
47
- ## 先定目标:作用在哪份剧本上
48
+ ## 先定本地文件
48
49
 
49
- `ingest`/`view`/`publish` 外,每条读写命令都要知道读写哪份剧本,从下面选一个(互斥):
50
-
51
- | 目标 | flag | 用在 |
52
- |---|---|---|
53
- | **项目 DB(最终剧本)** | `--remote [--project-group-no <no>]` | ☁️ **沙箱首选**。每次写进 DB revision,边写边入库 |
54
- | **本地文件** | `--script-path <file>` | 🖥️ **本地电脑首选**。直接读写这一个 JSON,无 store、无 revision。洗稿/对标/转绘、试验、离线全走它 |
55
- | 本地约定 store | `--local` | `SCRIPTCTL_OUTPUT_DIR` 下的约定 store |
56
-
57
- - ☁️ **沙箱里**:后端注入 `SANDBOX_PROJECT_GROUP_NO`,**裸命令自动指向该项目 DB**(自动 remote),无需任何 flag。
58
- - 🖥️ **本地电脑上**:几乎每条命令都带 `--script-path <file>`。下文示例为简洁常省略它,实际本地使用请补上。
59
- - **不给任何目标、又不在沙箱**:CLI 拒绝猜测并报 `SCRIPT_NO_TARGET`。选一个目标。
60
- - `--script-path`(本地文件)与 `--remote`/`--local`/`--project-group-no`(store)**互斥**,混用报 `TARGET_FLAG_CONFLICT`。
61
- - `--workspace-path <dir>` 只给 `ingest`/`view`/`publish`/`ingest status` 用,指 ingest 工作区,不是剧本目标。**不传就是 `<cwd>/workspace`**(默认值 `workspace` 相对当前目录解析);要放别处就显式传 `--workspace-path <dir>`,`ingest` 会在结果里回显解析后的绝对路径。`view`/`publish`/`ingest status` 要传**同一个** workspace 路径。
50
+ - 查询、`create`、全部原子编辑、`do`、`patch` 只接受 `--script-path <file>`;省略时统一使用 `<cwd>/workspace/script.json`。
51
+ - 编辑远端已有剧本:先 `scriptctl checkout [--script-path FILE]`,再对该文件运行任意原子命令,最后 `scriptctl publish [--script-path FILE]`。
52
+ - `checkout` 会建立 `.scriptctl/<filename>.working-copy.json`,记录远端项目、API 地址、base revision 和语义内容 hash。已有未管理文件或有本地修改时拒绝覆盖;只有明确 `--discard-local` 才丢弃。
53
+ - `status` 完全本地判断 managed / clean / dirty,不访问网络。
54
+ - `publish` 对已管理文件使用 metadata base revision 做严格 CAS;冲突保留本地文件。干净工作副本不访问网络。无 metadata 的文件只允许以 base revision 0 首次发布。
55
+ - 所有写命令按文件加锁;外部编辑器在命令期间改文件也会在落盘前被检测并拒绝覆盖。
56
+ - `--workspace-path` 仍只属于 `ingest` / `view` / `ingest status`;普通读写和 publish 用 `--script-path`。
62
57
 
63
58
  ### 两个 script.json:洗稿 / 对标 / 转绘
64
59
 
@@ -121,7 +116,7 @@ scriptctl scenes --in ep_003 --script-path "$NEW"
121
116
  | 改 action 归属角色(内联发声源) | `scriptctl actor <ep/scn#idx> <act_id\|none>` |
122
117
  | 改/清 action 情绪 | `scriptctl emotion <ep/scn#idx> "紧张"` / `--clear` |
123
118
  | **把某行设成对白 + 定内联发声源** | `scriptctl dialogue <ep/scn#idx> --actor act_001`(角色)/ `--kind <system\|broadcast\|offscreen\|group> [--label 广播]`(非角色声源)。同时把 type 翻成 dialogue |
124
- | **设/改场景里某资产的造型(state)** | `scriptctl scene-ref <ep/scn> <actor\|location\|prop>:<id> --state <state_id>`(新写正文时必须给实际 state;`--clear` / `--state none` 仅用于修复中间态,恢复 state 前不能继续插正文;`--remove` 整个移出场景)。location 单值(set 即替换) |
119
+ | **设/改场景里某资产的造型(state)** | `scriptctl scene-ref <ep/scn> <actor\|location\|prop>:<id> --state <state_id>`;同一 actor 再加一份用 `--append`,重复后用 `--occurrence <n>` 精确修改/清除/删除第 n 份。新写正文时必须给实际 state;`--clear` / `--state none` 仅用于修复中间态。location 单值(set 即替换) |
125
120
  | 改资产名/描述/别名/role | `scriptctl rename actor:<id> "X"` / `describe` / `alias --add X` / `role <主角\|配角>` |
126
121
  | **设资产重要度**(决定下游是否生成视觉资产+状态跟踪) | `scriptctl importance <actor\|location\|prop>:<id> <featured\|background>`(featured=主角/配角;background=龙套/背景,下游跳过) |
127
122
  | 给资产加 state | `scriptctl state-add actor:<id> "震惊"` |
@@ -219,8 +214,12 @@ scriptctl dialogue ep_001/scn_003#7 --kind broadcast --label 广播 # 非角
219
214
  ```bash
220
215
  scriptctl states act_001 # 先看有哪些 state
221
216
  scriptctl scene-ref ep_001/scn_003 actor:act_001 --state st_injured
217
+ scriptctl scene-ref ep_001/scn_003 actor:act_001 --state st_calm --append
218
+ scriptctl scene-ref ep_001/scn_003 actor:act_001 --state st_injured --occurrence 2
222
219
  ```
223
220
 
221
+ 同一人物可在同一 scene 中出现多份引用。`--occurrence` 从 1 开始;命中多份却不指定 occurrence 时,修改、清除和删除都会拒绝执行。
222
+
224
223
  **整理重复角色**:
225
224
  ```bash
226
225
  scriptctl refs actor:act_005 # 先看 act_005 出现在哪
@@ -272,14 +271,13 @@ scriptctl view # 生成 review.html
272
271
  scriptctl summary --script-path workspace/script.json # 抽完后当普通剧本读/改(注意目标是本地文件)
273
272
 
274
273
  # 3) 入库:校验 v3 通过后写进目标 store
275
- scriptctl publish # 沙箱:自动写项目 DB 新 revision
276
- scriptctl publish --local # 本地:写 SCRIPTCTL_OUTPUT_DIR
274
+ scriptctl publish --script-path workspace/script.json # 首次发布或更新受管工作副本
277
275
  ```
278
276
 
279
277
  - 🔴 **ingest 不入库**——它只产出 `<workspace>/script.json`。**入库是 `publish` 这独立一步**(沙箱里转完必须 publish,否则后端/前端看不到剧本)。
280
- - 🔴 **workspace 路径**:不传 `--workspace-path` 默认 `<cwd>/workspace`;`ingest` 结果第一行回显解析后的绝对路径。**`view`/`publish`/`ingest status` 必须传同一个 `--workspace-path`**(或在同一 cwd 下裸跑),否则会指到别的目录、看不到刚抽的产物。
281
- - 抽取产物在该 workspace 下。抽完后可用所有读写动词加 `--script-path <workspace>/script.json` 精修,再 `publish`;或 publish 入库后再对 DB(remote)精修。
282
- - publish 冲突(`SCRIPT_REVISION_CONFLICT`)= DB 被别的 revision 改过,重新拉取再 publish。
278
+ - 🔴 **workspace 路径**:不传 `--workspace-path` 默认 `<cwd>/workspace`;`ingest` 结果回显绝对路径。后续读写/publish 指向该目录下的 `script.json`。
279
+ - 抽取后用所有本地动词精修,再 publish。若后续继续编辑远端版本,checkout 到本地再改,不直接对 DB 跑原子 op。
280
+ - publish 冲突(`REVISION_CONFLICT`)表示远端 revision 已变化;本地稿仍保留,先确认远端变化再决定如何合并或显式丢弃重拉。
283
281
 
284
282
  ### `ingest status`:进度按真实文件判定
285
283
 
@@ -323,7 +321,7 @@ scriptctl ingest status --json # 机器:.status = {kind,state,phase,passes
323
321
  - 多态 verb(`delete`/`merge`/`move`/`describe`/`rename`/`insert`/`scene-ref`)按 address 格式分发;flag 用错 kind 会直接报错(不静默忽略)。
324
322
  - 新增正文前,`insert <ep/scn>` 要求本场已有地点,并且本场所有地点/人物/道具引用都绑定已物化 state;角色对白/心声的 actor 必须已在本场 cast 中。空镜、环境动作或非角色发声场可以没有人物引用。
325
323
  - `actor_name` 在全剧内唯一(Unicode 规范化和大小写折叠后仍不能重复);别名只放 `aliases[]`。`validate` 会报告历史数据里的重名人物和“说话人不在本场”。
326
- - 互斥 flag(`scene-ref` 的 --state/--clear/--remove;`dialogue` 的 --actor/--kind;`transition` 的 --process+--contrast/--clear)只能传一个,多传报 `*_FLAG_CONFLICT`。
324
+ - 互斥 flag(`scene-ref` 的 --state/--clear/--remove;`dialogue` 的 --actor/--kind;`transition` 的 --process+--contrast/--clear)只能传一个,多传报 `*_FLAG_CONFLICT`。`scene-ref --append` 只与 `--state` 同用;重复 actor 引用用 1-based `--occurrence` 定位。
327
325
  - `mock` provider 仅测试,禁止作为交付;provider 失败不降级 mock。
328
326
  - 工具失败排查根因,不手工拼装 JSON 绕过校验。
329
327
 
@@ -26,7 +26,7 @@ create(空白剧本,既有剧本略过)
26
26
  → publish 入库(沙箱必做)
27
27
  ```
28
28
 
29
- > 发声源写在**对白行内联**(`dialogue <at> --actor/--kind`,或 `insert --actor/--kind`);造型挂在**场景 cast 引用**上(`scene-ref <ep/scn> <kind:id> --state`)。正文插入前,本场必须已有地点,并且本场全部地点/人物/道具引用都绑定已建立的 state。空镜、环境动作或非角色发声场可以没有人物引用。
29
+ > 发声源写在**对白行内联**(`dialogue <at> --actor/--kind`,或 `insert --actor/--kind`);造型挂在**场景 cast 引用**上(`scene-ref <ep/scn> <kind:id> --state`)。同一人物需要多份引用时用 `--append` 追加,用 `--occurrence <n>` 定位第 n 份。正文插入前,本场必须已有地点,并且本场全部地点/人物/道具引用都绑定已建立的 state。空镜、环境动作或非角色发声场可以没有人物引用。
30
30
 
31
31
  资产/分集/场景的 id **自动按序分配**:第一个 actor 是 `act_001`、location 是 `loc_001`、prop 是 `prp_001`、episode 是 `ep_001`、它下面第一场是 `scn_001`……所以你能在后续命令里直接引用这些可预测的 id(也可以 `--id` 显式指定)。
32
32
 
@@ -61,6 +61,14 @@ scriptctl insert ep_001/scn_001 --type inner_thought --content "陈默又出现
61
61
  scriptctl validate
62
62
  ```
63
63
 
64
+ 同一人物在本场需要第二份参考时,继续追加而不是新建 actor:
65
+
66
+ ```bash
67
+ scriptctl scene-ref ep_001/scn_001 actor:act_001 --state st_rain --append
68
+ scriptctl scene-ref ep_001/scn_001 actor:act_001 --state default --occurrence 2
69
+ scriptctl scene-ref ep_001/scn_001 actor:act_001 --remove --occurrence 2
70
+ ```
71
+
64
72
  ### B. 整集 / 整本(推荐):资产与正文分两段 `do`
65
73
 
66
74
  一集几十个 action 用 `do` 批量写入。先独立落盘资产批次,读回自动分配的 id;再用正文批次建集、按真实场界建 scene、绑定 state 并写 action。这两次落盘让宿主先展示人物、地点、道具及状态,再展示正文。
@@ -115,7 +123,7 @@ insert ep_001/scn_001 --type action --content "雨夜,林夏推门进店。"
115
123
  insert ep_001/scn_001 --type dialogue --content "欢迎光临。" --actor act_002
116
124
  ```
117
125
 
118
- 地点、时段、叙事线和全部资产 state 连续时,后续节拍继续向 `ep_001/scn_001` 追加 action。持久视觉 state 变化时在变化动作后建立同地点的新 scene,并绑定新 state。互动式写作可把长场拆成多个 action 批次依次 `do --apply`,每批都指向同一 scene。场界规则以 SKILL.md「标准剧本通用流程」为准。
126
+ 地点、时段和叙事线连续时,后续节拍继续向 `ep_001/scn_001` 追加 action。同场人物需要多个造型或多份画面引用时,对同一 actor 追加 scene-ref,并在 Action 中写清变化顺序。互动式写作可把长场拆成多个 action 批次依次 `do --apply`,每批都指向同一 scene。场界规则以 SKILL.md「标准剧本通用流程」为准。
119
127
 
120
128
  动词与单条命令完全一致(参数见 `scriptctl <verb> --help`)。`do` 也读 stdin:`… | scriptctl do -`。
121
129
 
@@ -125,16 +133,18 @@ insert ep_001/scn_001 --type dialogue --content "欢迎光临。" --actor act_00
125
133
 
126
134
  ## 写到哪里去(落库)
127
135
 
128
- `create` 和所有编辑 verb 共用同一套目标解析,三选一:
136
+ `create` 和所有编辑 verb 只写本地 `--script-path <file>`;省略时是
137
+ `<cwd>/workspace/script.json`。远端剧本必须遵循:
129
138
 
130
- | 目标 | 怎么走 | 说明 |
131
- |---|---|---|
132
- | **本地文件** | `--script-path <file>` | 🖥️ **本地电脑首选**。直接读写这一个 JSON,无 store、无 revision。洗稿/对标/转绘、试验都走它 |
133
- | **DB(项目组)** | `--remote`(沙箱裸命令自动 remote),或 `--project-group-no <no>` | ☁️ **沙箱首选**。每个编辑 op 直接写进 DB 的新 revision,边写边入库 |
134
- | **本地约定 store** | `--local` | 写 `SCRIPTCTL_OUTPUT_DIR` 下的约定 store |
139
+ ```text
140
+ checkout → 本地查询/原子编辑/do/patch → validate → publish
141
+ ```
135
142
 
136
- - 🖥️ **本地**:`scriptctl create --script-path new.json` 起本,之后每条编辑都带同一个 `--script-path new.json`;想入库时用 `scriptctl publish`(读某个 `workspace/script.json` 写 store)或直接对 DB 写。
137
- - ☁️ **沙箱**:直接对 DB 写(`create` → 编辑,不加 flag = 自动 remote),写完就在库里。想先本地攒好再入库,就 `create --script-path draft.json` 全程 `--script-path draft.json` 编辑,最后把它放进 `workspace/` 用 `scriptctl publish` 入库。
143
+ - 已有远端剧本先 `scriptctl checkout --script-path draft.json`。
144
+ - 全程对同一个 `draft.json` 编辑。新增原子能力只需实现本地操作,不增加远端业务 API。
145
+ - `scriptctl status --script-path draft.json` 查看是否 dirty。
146
+ - `scriptctl publish --script-path draft.json` 使用 checkout 时记录的 base revision 严格发布;冲突不会覆盖本地稿。
147
+ - 本地新剧可直接 create/ingest 后首次 publish;没有 working-copy metadata 时只尝试从 revision 0 创建。
138
148
 
139
149
  ### 两份 script.json(洗稿 / 对标 / 转绘)
140
150
 
@@ -21,7 +21,7 @@ scriptctl ingest --source-path uploads/正片/
21
21
  ```
22
22
 
23
23
  常用 flag:
24
- - `--workspace-path <dir>`:工作区根(manifest + 各 pass 目录 + 产物)。**不传默认 `<cwd>/workspace`**;显式传可放别处,`ingest` 结果第一行回显解析后的绝对路径。🔴 `view`/`publish`/`ingest status` 要传**同一个**路径(或同 cwd 裸跑)。
24
+ - `--workspace-path <dir>`:工作区根(manifest + 各 pass 目录 + 产物)。**不传默认 `<cwd>/workspace`**;显式传可放别处。`view` / `ingest status` 使用同一 workspace;publish 用 `--script-path <workspace>/script.json`。
25
25
  - `--concurrency <n>`:文本/归并 fanout 并发(文本默认 80,视频默认 30)。
26
26
  - `--video-concurrency <n>`:视频上传+转录并发。默认 10。
27
27
  - `--fps <n>`:视频采样帧率(>1 解锁亚秒级时间码)。默认 3。
@@ -79,27 +79,26 @@ scriptctl summary --script-path workspace/script.json # 抽完当普通剧本
79
79
  ## 五、入库 `publish`
80
80
 
81
81
  ```bash
82
- scriptctl publish # 沙箱:自动写项目 DB 新 revision(SANDBOX_PROJECT_GROUP_NO 已注入)
83
- scriptctl publish --local # 本地:写 SCRIPTCTL_OUTPUT_DIR
84
- scriptctl publish --remote --project-group-no 123 # 显式指定项目组
82
+ scriptctl publish --script-path workspace/script.json
83
+ scriptctl publish --script-path workspace/script.json --project-group-no 123
85
84
  ```
86
85
 
87
- - `publish` `workspace/script.json`,**再跑一遍 v3 校验**,通过才写 store。
86
+ - `publish` 读本地 `--script-path`(默认 `workspace/script.json`),**再跑一遍 v3 校验**,通过才发布。
88
87
  - 沙箱里转完**必须 publish**,否则后端/前端看不到剧本。
89
- - `SCRIPT_REVISION_CONFLICT` = DB 被别的 revision 改过:重新拉取当前剧本、合并你的改动,再 publish。
90
- - publish 幂等:同一份 script 重复 publish 不会产生新 revision(按内容 sha 去重)。
88
+ - `REVISION_CONFLICT` = 远端被别人更新;本地稿保持不变。确认远端变化后合并,或明确 `checkout --discard-local` 放弃本地稿。
89
+ - 已管理且内容未变时 publish 本地直接 no-op,不发网络请求。服务端不保存 requestId,也不提供 publish 幂等重试。
91
90
 
92
91
  ## 六、复杂场景怎么接
93
92
 
94
93
  - **转完先精修再入库**:`ingest` → 用 `--script-path workspace/script.json` 跑读写动词修(改归属、并角色、标龙套、补造型……)→ `publish`。
95
- - **入库后再改**:`publish` 之后直接对 DB(沙箱裸命令 = remote)继续精修,每次改都是新 revision。
94
+ - **入库后再改**:继续编辑同一个受管本地文件再 publish;若工作副本不在,先 checkout,不能直接对 DB 跑原子命令。
96
95
  - **视频复核**:`view` 出的 `review.html` 视频版支持点击台词跳到对应画面时间码,逐场核对发声源/造型。
97
96
  - **超大剧集(几十集)**:并发默认已调好;失败就重跑到 `ingest status` 显示 `ready`。花名册归并只跑一次并复用,重跑很快。
98
97
  - **归并质量**:跨集角色归并靠文字锚点,相似角色可能误合/合过头。用 `roster.review.md`(视频)核对归并决策;发现误合,入库后用 `merge` 拆分/改名修正。
99
98
 
100
99
  ## 七、抽完后的读写
101
100
 
102
- 抽取产物就是标准 v3 剧本,用 SKILL.md 里所有读写能力精修(目标记得指 `--script-path workspace/script.json`,或 publish 后指 DB):
101
+ 抽取产物就是标准 v3 剧本,用 SKILL.md 里所有读写能力精修(目标指 `--script-path workspace/script.json`;远端稿先 checkout):
103
102
  - `summary` / `episodes` 看整本 + 分集梗概;`actions --in <ep>` 带 timestamp 看节奏。
104
103
  - `states <actor>` 看造型弧线;`actors --counts` 看出场频次决定 `importance`。
105
104
  - 归属/造型修正:`dialogue <at> --actor/--kind`、`scene-ref <ep/scn> <kind:id> --state`。
@@ -16,7 +16,7 @@
16
16
  开始每一集和每一场前:
17
17
 
18
18
  1. 读取现有人物、地点、道具及其 states。
19
- 2. 确定本场唯一地点、全部出场人物、全部使用道具和各自实际 state
19
+ 2. 确定本场唯一地点、全部出场人物、全部使用道具和各自实际 state;同一人物需要多份画面引用时列出每一份引用。
20
20
  3. 注册缺少的资产,定义需要独立视觉参考的持久状态。
21
21
  4. 建立 scene,绑定地点、人物、道具及实际 state。
22
22
  5. 读回 scene-ref,确认正文中的每项资产都能映射到本场引用。
@@ -64,13 +64,13 @@
64
64
  - 当前没有 montage group。使用相邻 scene、transition 和统一文本标记表达组合关系。
65
65
  - 纯空镜或完全无人镜头单独建 scene,绑定实际 location 与关键道具,不创建虚假人物。
66
66
 
67
- ## 八、场内状态变化
67
+ ## 八、同场人物多引用与状态变化
68
68
 
69
- - 一个 scene 中,每个人物、地点和道具绑定一个有效 state。
70
- - 需要更换视觉参考资产的持久状态变化构成生产场界,即使地点和时间连续也建立新 scene
71
- - 变化前 scene 绑定旧 state,在末尾写清变化动作;需要生成过程画面时给该 Action 添加 `transition_prompt`。
72
- - 变化后 scene 绑定新 state。同地点、同时段的状态边界保持独立,不参与场景合并。
73
- - 当前同一 scene 不能同时引用同一资产的多个 state。一个镜头需要变化前后两张状态图时报告结构限制。
69
+ - 一个 scene 可重复引用同一 actor;每份引用独立绑定一个有效 state,可使用相同或不同 state
70
+ - 先用 `scene-ref <ep/scn> actor:<id> --state <state_id>` 建第一份引用,再用同一命令加 `--append` 建后续引用。
71
+ - 同一 actor 已有多份引用时,用 1-based `--occurrence <n>` 精确修改、清除或删除某一份;不要省略 occurrence 让目标产生歧义。
72
+ - 同一连续场内的造型变化保留在同一 scene:绑定变化前后所需的人物引用,在 Action 中写清变化顺序;需要生成过程画面时给该 Action 添加 `transition_prompt`。
73
+ - 地点切换、明显跳时、叙事线切换或明确转场时建立新 scene。location 仍是单值引用。
74
74
 
75
75
  ## 九、完成门
76
76
 
@@ -78,6 +78,6 @@
78
78
  2. 检查每场唯一地点、全部出场人物、道具和 state 引用。
79
79
  3. 检查 Action 的真名、动作链、空间结果和可拍摄性。
80
80
  4. 检查 dialogue 的说话人、emotion 和群体发声类型。
81
- 5. 检查场界、蒙太奇顺序和状态变化边界。
81
+ 5. 检查场界、蒙太奇顺序、重复人物引用和状态变化顺序。
82
82
  6. 提取与解析任务对照源素材复核对白原文,以及事实、因果、顺序、动作结果和状态。
83
83
  7. 运行 `validate`。把结构通过与内容标准、源素材一致性分别报告。