@lingjingai/scriptctl 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,248 +0,0 @@
1
- /**
2
- * Asset collision detector for cross-episode incremental drafting.
3
- *
4
- * When a freshly-parsed fragment declares a new asset (in `## 人物`/`## 场景`/`## 道具`/
5
- * `## 发声源`) whose name already exists in the registry, the detector raises a
6
- * structured report. The orchestrator (episode subcommand) is expected to surface this
7
- * to the agent, which decides among three resolutions:
8
- * - accept_identity: same entity, drop the duplicate declaration (refs use existing id)
9
- * - distinguish_rename: different entity, rename the new one to disambiguate
10
- * - promote_to_state: same entity, different state — add a state to existing asset
11
- *
12
- * Only exact-name collisions are detected. The kind reflects whether the agent
13
- * should treat it as blocking or a warning, based on description similarity:
14
- * - hard: exact name match (description similar enough that it's almost certainly
15
- * the same entity). Blocks commit; expects agent resolution.
16
- * - soft: exact name match but descriptions clearly diverge — could be intentional
17
- * reuse of a generic label (e.g. two unrelated "路人甲"). Warn but don't block.
18
- *
19
- * The detector is a pure function: it reads the fragment and the registry,
20
- * never mutates either.
21
- */
22
- import { lookupActor, lookupLocation, lookupProp, lookupSpeaker, } from "./asset-registry.js";
23
- const DEFAULT_SOFT_THRESHOLD = 0.4;
24
- // ---------------------------------------------------------------------------
25
- // Similarity scoring (lightweight, dependency-free)
26
- // ---------------------------------------------------------------------------
27
- /**
28
- * Character-level Jaccard similarity. Trims and normalises punctuation/whitespace.
29
- * Tuned for short Chinese/English descriptions (≤ 100 chars), which is the typical
30
- * scriptctl asset description length.
31
- *
32
- * Returns 1.0 when both strings are empty (or one is empty — to avoid false soft
33
- * collisions when an existing entry has no description yet).
34
- */
35
- function descriptionSimilarity(a, b) {
36
- const aa = (a ?? "").replace(/[\s\p{P}]+/gu, "");
37
- const bb = (b ?? "").replace(/[\s\p{P}]+/gu, "");
38
- if (aa === "" || bb === "")
39
- return 1.0;
40
- const setA = new Set([...aa]);
41
- const setB = new Set([...bb]);
42
- let intersect = 0;
43
- for (const ch of setA)
44
- if (setB.has(ch))
45
- intersect += 1;
46
- const union = setA.size + setB.size - intersect;
47
- return union === 0 ? 1.0 : intersect / union;
48
- }
49
- function classifyMatch(fragmentDesc, registryDesc, softThreshold) {
50
- const sim = descriptionSimilarity(fragmentDesc, registryDesc);
51
- if (sim < softThreshold)
52
- return { kind: "soft", similarity: sim };
53
- return { kind: "hard", similarity: sim };
54
- }
55
- // ---------------------------------------------------------------------------
56
- // Entry → report builders
57
- // ---------------------------------------------------------------------------
58
- function toRegistryReport(entry) {
59
- const base = {
60
- id: entry.id,
61
- name: entry.name,
62
- description: entry.description,
63
- firstSeenEpisode: entry.firstSeenEpisode,
64
- firstSeenScene: entry.firstSeenScene,
65
- firstSeenAction: entry.firstSeenAction,
66
- lastSeenEpisode: entry.lastSeenEpisode,
67
- appearanceCount: entry.appearanceCount,
68
- states: entry.states.map((s) => ({ id: s.id, name: s.name })),
69
- };
70
- if ("sourceKind" in entry)
71
- base.sourceKind = entry.sourceKind;
72
- return base;
73
- }
74
- function findFragmentFirstSeen(fragment, episode, predicate, actionPicker) {
75
- const scenes = fragment.scenes ?? [];
76
- for (let i = 0; i < scenes.length; i++) {
77
- if (!predicate(i))
78
- continue;
79
- const sceneNum = typeof scenes[i].scene_num === "number" ? scenes[i].scene_num : i + 1;
80
- const sceneId = `ep_${String(episode).padStart(3, "0")}/scn_${String(sceneNum).padStart(3, "0")}`;
81
- return { scene: sceneId, action: actionPicker(i) };
82
- }
83
- return { scene: "", action: "" };
84
- }
85
- function locateActorFirstSeen(fragment, episode, name) {
86
- return findFragmentFirstSeen(fragment, episode, (i) => (fragment.scenes?.[i]?.actor_names ?? []).includes(name), (i) => {
87
- const firstAction = (fragment.scenes?.[i]?.actions ?? []).find((a) => typeof a.content === "string" && a.content.includes(name));
88
- return firstAction ? String(firstAction.content ?? "").trim() : "";
89
- });
90
- }
91
- function locateLocationFirstSeen(fragment, episode, name) {
92
- return findFragmentFirstSeen(fragment, episode, (i) => fragment.scenes?.[i]?.location_name === name, (i) => String(fragment.scenes?.[i]?.actions?.[0]?.content ?? "").trim());
93
- }
94
- function locatePropFirstSeen(fragment, episode, name) {
95
- return findFragmentFirstSeen(fragment, episode, (i) => (fragment.scenes?.[i]?.prop_names ?? []).includes(name), (i) => {
96
- const firstAction = (fragment.scenes?.[i]?.actions ?? []).find((a) => typeof a.content === "string" && a.content.includes(name));
97
- return firstAction ? String(firstAction.content ?? "").trim() : "";
98
- });
99
- }
100
- function locateSpeakerFirstSeen(fragment, episode, name) {
101
- return findFragmentFirstSeen(fragment, episode, (i) => (fragment.scenes?.[i]?.actions ?? []).some((a) => typeof a.speaker === "string" && a.speaker === name), (i) => {
102
- const firstAction = (fragment.scenes?.[i]?.actions ?? []).find((a) => typeof a.speaker === "string" && a.speaker === name);
103
- return firstAction ? String(firstAction.content ?? "").trim() : "";
104
- });
105
- }
106
- function buildReport(kind, assetType, decl, firstSeen, entry, similarity) {
107
- const resolutions = assetType === "actor"
108
- ? ["accept_identity", "distinguish_rename", "promote_to_state"]
109
- : ["accept_identity", "distinguish_rename"];
110
- return {
111
- kind,
112
- assetType,
113
- fragmentDeclaration: {
114
- name: decl.name,
115
- description: decl.description ?? null,
116
- firstSeenScene: firstSeen.scene || undefined,
117
- firstSeenAction: firstSeen.action || undefined,
118
- },
119
- registryEntry: toRegistryReport(entry),
120
- descriptionSimilarity: similarity,
121
- resolutions,
122
- };
123
- }
124
- // ---------------------------------------------------------------------------
125
- // Public API
126
- // ---------------------------------------------------------------------------
127
- /**
128
- * Detect collisions between a freshly-parsed fragment and the existing registry.
129
- *
130
- * Returns a (possibly empty) list of reports. Caller (episode subcommand) treats:
131
- * - `kind: "hard"` as blocking — agent must resolve before commit
132
- * - `kind: "soft"` as warning — agent should consider but may proceed
133
- * - `kind: "hint"` as info — surfaced for awareness only
134
- *
135
- * The episode number is taken from `fragment.episode` if available, otherwise
136
- * passed via `opts.episode` (caller responsibility).
137
- */
138
- export function detectCollisions(fragment, registry, opts = {}) {
139
- const softThreshold = opts.softSimilarityThreshold ?? DEFAULT_SOFT_THRESHOLD;
140
- const episode = opts.episode ?? (typeof fragment.episode === "number" ? fragment.episode : 0);
141
- const reports = [];
142
- for (const decl of fragment.actors ?? []) {
143
- const existing = lookupActor(registry, decl.name);
144
- if (existing) {
145
- const { kind, similarity } = classifyMatch(decl.description, existing.description, softThreshold);
146
- const firstSeen = locateActorFirstSeen(fragment, episode, decl.name);
147
- reports.push(buildReport(kind, "actor", decl, firstSeen, existing, similarity));
148
- }
149
- }
150
- for (const decl of fragment.locations ?? []) {
151
- const existing = lookupLocation(registry, decl.name);
152
- if (existing) {
153
- const { kind, similarity } = classifyMatch(decl.description, existing.description, softThreshold);
154
- const firstSeen = locateLocationFirstSeen(fragment, episode, decl.name);
155
- reports.push(buildReport(kind, "location", decl, firstSeen, existing, similarity));
156
- }
157
- }
158
- for (const decl of fragment.props ?? []) {
159
- const existing = lookupProp(registry, decl.name);
160
- if (existing) {
161
- const { kind, similarity } = classifyMatch(decl.description, existing.description, softThreshold);
162
- const firstSeen = locatePropFirstSeen(fragment, episode, decl.name);
163
- reports.push(buildReport(kind, "prop", decl, firstSeen, existing, similarity));
164
- }
165
- }
166
- for (const decl of fragment.speakers ?? []) {
167
- const existing = lookupSpeaker(registry, decl.name, decl.sourceKind);
168
- if (existing) {
169
- const { kind, similarity } = classifyMatch(decl.description, existing.description, softThreshold);
170
- const firstSeen = locateSpeakerFirstSeen(fragment, episode, decl.name);
171
- reports.push(buildReport(kind, "speaker", decl, firstSeen, existing, similarity));
172
- }
173
- }
174
- return reports;
175
- }
176
- /**
177
- * Whether any report has `kind: "hard"` — convenience for episode.ts commit gating.
178
- */
179
- export function hasHardCollision(reports) {
180
- return reports.some((r) => r.kind === "hard");
181
- }
182
- // ---------------------------------------------------------------------------
183
- // Human-readable summary (for CLI stderr output)
184
- // ---------------------------------------------------------------------------
185
- /**
186
- * Render a list of CollisionReports as a human-readable markdown summary suitable
187
- * for stderr output. The machine-readable JSON is expected to be written to
188
- * `workspace/episodes/ep<n>.collision.json` by the orchestrator.
189
- */
190
- export function summarizeReports(reports) {
191
- if (reports.length === 0)
192
- return "";
193
- const lines = [`检测到 ${reports.length} 个资产冲突:`, ""];
194
- for (let i = 0; i < reports.length; i++) {
195
- const r = reports[i];
196
- lines.push(`### [${i + 1}/${reports.length}] ${r.kind.toUpperCase()} · ${r.assetType} · "${r.fragmentDeclaration.name}"`);
197
- lines.push("");
198
- lines.push("新声明:");
199
- lines.push(` 名字: ${r.fragmentDeclaration.name}`);
200
- if (r.fragmentDeclaration.description) {
201
- lines.push(` 描述: ${r.fragmentDeclaration.description}`);
202
- }
203
- if (r.fragmentDeclaration.firstSeenScene) {
204
- lines.push(` 首次出现: ${r.fragmentDeclaration.firstSeenScene}`);
205
- }
206
- if (r.fragmentDeclaration.firstSeenAction) {
207
- lines.push(` 首次动作: ${r.fragmentDeclaration.firstSeenAction}`);
208
- }
209
- lines.push("");
210
- lines.push(`已存在 (${r.registryEntry.id}):`);
211
- lines.push(` 名字: ${r.registryEntry.name}`);
212
- if (r.registryEntry.description) {
213
- lines.push(` 描述: ${r.registryEntry.description}`);
214
- }
215
- if (r.registryEntry.sourceKind) {
216
- lines.push(` 来源类型: ${r.registryEntry.sourceKind}`);
217
- }
218
- lines.push(` 首次出现: ${r.registryEntry.firstSeenScene} (ep${r.registryEntry.firstSeenEpisode})`);
219
- if (r.registryEntry.firstSeenAction) {
220
- lines.push(` 首次动作: ${r.registryEntry.firstSeenAction}`);
221
- }
222
- lines.push(` 最近出现: ep${r.registryEntry.lastSeenEpisode}`);
223
- lines.push(` 出现总集数: ${r.registryEntry.appearanceCount}`);
224
- if (r.registryEntry.states.length > 0) {
225
- lines.push(` 已注册状态: [${r.registryEntry.states.map((s) => s.name).join(", ")}]`);
226
- }
227
- lines.push(` 描述相似度: ${(r.descriptionSimilarity * 100).toFixed(0)}%`);
228
- lines.push("");
229
- lines.push("可能处置:");
230
- if (r.resolutions.includes("accept_identity")) {
231
- lines.push(` [A] 同一${r.assetType === "actor" ? "人" : "项"},沿用 ${r.registryEntry.id}`);
232
- lines.push(` → 编辑 ep<n>.md,删除资产段中重复声明,refs 不动`);
233
- }
234
- if (r.resolutions.includes("distinguish_rename")) {
235
- lines.push(` [B] 不同${r.assetType === "actor" ? "人" : "项"},需区分`);
236
- lines.push(` → 把这集所有 "${r.fragmentDeclaration.name}" 改名为更具体的名字`);
237
- }
238
- if (r.resolutions.includes("promote_to_state")) {
239
- lines.push(` [C] 同一人不同形态/装扮`);
240
- lines.push(` → scriptctl script state add actor:${r.registryEntry.id} --state-id <id> --name <name> --description <desc>`);
241
- }
242
- lines.push("");
243
- }
244
- lines.push("完整碰撞详情见 workspace/episodes/ep<n>.collision.json");
245
- lines.push("解决后重跑:scriptctl episode draft <n> --resume (跳过 Gemini,仅重跑 parser + collisionCheck)");
246
- return lines.join("\n");
247
- }
248
- //# sourceMappingURL=collision-detector.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"collision-detector.js","sourceRoot":"","sources":["../../src/domain/collision-detector.ts"],"names":[],"mappings":"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"}
@@ -1,31 +0,0 @@
1
- /**
2
- * Default writing prompt bundled with scriptctl.
3
- *
4
- * Used by `scriptctl episode draft` when the caller does not pass
5
- * `--prompt-template <path>`.
6
- *
7
- * The actual prompt lives in the sibling `default-writing-prompt.md` so the
8
- * content stays markdown — readable in editors, diffable, and editable by
9
- * non-engineers (编剧 / 业务). This TS file only:
10
- *
11
- * 1. Loads the .md at module-init time.
12
- * 2. Substitutes the `<!-- MARKDOWN_BATCH_PROMPT_SPEC -->` placeholder with the
13
- * authoritative spec from `common.ts` (single source of truth for the
14
- * parser-acceptable grammar — same spec consumed by `direct init`).
15
- *
16
- * The .md is copied to `dist/infra/` by the package's build script so the
17
- * shipped npm package contains it; vitest reads the source .md directly.
18
- */
19
- export declare const SPEC_PLACEHOLDER = "<!-- MARKDOWN_BATCH_PROMPT_SPEC -->";
20
- /**
21
- * Substitute the `<!-- MARKDOWN_BATCH_PROMPT_SPEC -->` placeholder (if present) in
22
- * any prompt template body with the authoritative parser spec. Returns the body
23
- * unchanged when the placeholder is absent — that's fine for templates that
24
- * hard-code the spec themselves.
25
- *
26
- * Used both by the bundled default prompt loader and by --prompt-template, so
27
- * skill-supplied templates can use the same placeholder mechanism and stay in
28
- * sync with the parser as the spec evolves.
29
- */
30
- export declare function injectSpec(promptBody: string): string;
31
- export declare const DEFAULT_WRITING_PROMPT: string;
@@ -1,50 +0,0 @@
1
- /**
2
- * Default writing prompt bundled with scriptctl.
3
- *
4
- * Used by `scriptctl episode draft` when the caller does not pass
5
- * `--prompt-template <path>`.
6
- *
7
- * The actual prompt lives in the sibling `default-writing-prompt.md` so the
8
- * content stays markdown — readable in editors, diffable, and editable by
9
- * non-engineers (编剧 / 业务). This TS file only:
10
- *
11
- * 1. Loads the .md at module-init time.
12
- * 2. Substitutes the `<!-- MARKDOWN_BATCH_PROMPT_SPEC -->` placeholder with the
13
- * authoritative spec from `common.ts` (single source of truth for the
14
- * parser-acceptable grammar — same spec consumed by `direct init`).
15
- *
16
- * The .md is copied to `dist/infra/` by the package's build script so the
17
- * shipped npm package contains it; vitest reads the source .md directly.
18
- */
19
- import * as fs from "node:fs";
20
- import * as path from "node:path";
21
- import { fileURLToPath } from "node:url";
22
- import { MARKDOWN_BATCH_PROMPT_SPEC } from "../common.js";
23
- export const SPEC_PLACEHOLDER = "<!-- MARKDOWN_BATCH_PROMPT_SPEC -->";
24
- /**
25
- * Substitute the `<!-- MARKDOWN_BATCH_PROMPT_SPEC -->` placeholder (if present) in
26
- * any prompt template body with the authoritative parser spec. Returns the body
27
- * unchanged when the placeholder is absent — that's fine for templates that
28
- * hard-code the spec themselves.
29
- *
30
- * Used both by the bundled default prompt loader and by --prompt-template, so
31
- * skill-supplied templates can use the same placeholder mechanism and stay in
32
- * sync with the parser as the spec evolves.
33
- */
34
- export function injectSpec(promptBody) {
35
- if (!promptBody.includes(SPEC_PLACEHOLDER))
36
- return promptBody;
37
- return promptBody.replace(SPEC_PLACEHOLDER, MARKDOWN_BATCH_PROMPT_SPEC);
38
- }
39
- function loadBundledPrompt() {
40
- const here = path.dirname(fileURLToPath(import.meta.url));
41
- const mdPath = path.join(here, "default-writing-prompt.md");
42
- const raw = fs.readFileSync(mdPath, "utf-8").trim();
43
- if (!raw.includes(SPEC_PLACEHOLDER)) {
44
- throw new Error(`default-writing-prompt.md is missing the "${SPEC_PLACEHOLDER}" placeholder; ` +
45
- `the bundled prompt cannot embed the format spec.`);
46
- }
47
- return injectSpec(raw);
48
- }
49
- export const DEFAULT_WRITING_PROMPT = loadBundledPrompt();
50
- //# sourceMappingURL=default-writing-prompt.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"default-writing-prompt.js","sourceRoot":"","sources":["../../src/infra/default-writing-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAE1D,MAAM,CAAC,MAAM,gBAAgB,GAAG,qCAAqC,CAAC;AAEtE;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,UAAkB;IAC3C,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,gBAAgB,CAAC;QAAE,OAAO,UAAU,CAAC;IAC9D,OAAO,UAAU,CAAC,OAAO,CAAC,gBAAgB,EAAE,0BAA0B,CAAC,CAAC;AAC1E,CAAC;AAED,SAAS,iBAAiB;IACxB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,2BAA2B,CAAC,CAAC;IAC5D,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,6CAA6C,gBAAgB,iBAAiB;YAC5E,kDAAkD,CACrD,CAAC;IACJ,CAAC;IACD,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,iBAAiB,EAAE,CAAC"}
@@ -1,115 +0,0 @@
1
- # Role: 微短剧编剧(scriptctl spec-md 输出版)
2
-
3
- ## Profile
4
- 国内竖屏 90 秒微短剧资深编剧。按集纲逐集写出可被下游 parser 直接消费的结构化 markdown 剧本。强调:**大纲定动作骨架,原文(如有)抓灵魂血肉**;**角色 + 结构双核心驱动**;**冲突层层升级**;**台词去水、动作化、视听可拍**;杜绝模板腔、说明文、心理活动描写。
5
-
6
- ## Goals
7
- 接收【当前集集纲】+【项目上下文(设定 / 改编方案 / 原文 / 上一集尾部)】+【已存在角色清单】,输出该集的 spec-md 剧本片段,让下游 parser 零容忍解析通过。
8
-
9
- ## 输出格式(硬约束,偏离即 parse 失败)
10
-
11
- 整个输出**只包含一份 markdown 文档**,**两个**顶层段:先 `# 剧本`、后 `# 资产`。
12
- **不要**用 ``` 围栏包裹文档,**不要**在 `# 剧本` 之前或 `# 资产` 之后写说明文字。
13
- **不要**输出 `第X集:副标题` 这类首行标题(集号 / 标题由上层 batch_plan 注入)。
14
-
15
- <!-- MARKDOWN_BATCH_PROMPT_SPEC -->
16
-
17
- ## 单集篇幅与节奏(必须暗含,正文不写区块名)
18
-
19
- - 常规集:800-1000 字(约 90 秒);黄金三章(第 1-3 集)可放宽到 1200 字
20
- - 三段式 + 五大模块(**正文不写区块名 / 不写"黄金开场""矛盾拉扯""极限卡点"等提示语**):
21
- - **前 15% 黄金开场**:
22
- - ① 黄金钩子(前 5-10 秒):开场即高潮,强视觉冲击或反常悬念,**不铺垫背景**
23
- - ② 明确目标(10-30 秒):主角用一句台词或动作让观众知道"这集要干什么"
24
- - **中间 70% 矛盾拉扯**:
25
- - ③ 密集冲突:每 10-15 秒至少一次小对抗或转折;信息差驱动反派打压与主角见招拆招交替
26
- - **层层对抗升级**:单集至少 2-3 次"对抗升级点"(筹码加大 / 场域更公开 / 代价更真实 / 规则更硬 / 底牌被迫亮出)。升级形态随剧情选择,**不固定**为"嘴炮→羞辱→肢体"等单一路径
27
- - **后 15% 极限卡点**:
28
- - ④ 反转节点(倒数 15 秒):引爆本集爽点或痛点,用前文伏笔或硬筹码打破预期
29
- - ⑤ 极限悬念(最后 3-5 秒):卡在威胁升级 / 身份揭秘 / 巴掌将落未落的一瞬间戛然而止
30
- - 分场默认 1-4 场(正戏场次);闪回不计入正戏场次,必须极短、只服务信息差 / 名场面,**不得用闪回注水**
31
- - 切场判定基线:**只由时空变化决定**。同一时空内连续动作、连续对峙、连续情绪推进默认保持同一场
32
-
33
- ## 条件性硬约束(按本集标签 / 输入材料启用)
34
-
35
- ### 若属黄金三章(第 1-3 集)
36
-
37
- - 第 1 集:3 秒内完成"代入 + 冲突 + 钩子"闭环;四锚点(我是谁 / 我有什么 / 我要干什么 / 搞砸了会怎样)用 ≤2 句大白话传递;前 10 秒至少 2 条信息通道同步
38
- - 第 2 集:展示 + 认同。缺陷接地气,不要完美应对;通过行动暴露规则,**禁止 OS 大段解释**
39
- - 第 3 集:爽点 + 伏笔。爽点来自主角自身能力,不是纯系统开挂;爽点后必须有三层反馈(环境 + 他人 + 自我)
40
- - 每集开场必须强力呼应上一集结尾钉子
41
-
42
- ### 若属付费卡点集(集纲标了【付费卡点】)
43
-
44
- - 付费集卖点必须是全剧前期最强爽点之一(逆袭 / 身份揭露 / 情感反转 / 决裂 / 终极打脸)
45
- - 结尾钉子必须是全剧前期最强悬念,让观众"不付费睡不着"
46
- - 写作质量与黄金三章同等,不可降低标准
47
-
48
- ### 若提供原著 / 分析报告(骨肉融合法则)
49
-
50
- - 严格按集纲的**微观事件链**推进,不能跑题、不能加支线、不能拖延卡点
51
- - 原著只用于"血肉":提取角色的毒舌金句 / 标志动作 / 特殊道具 / 说话习惯
52
- - 把原著的文学描写改成摄影机可拍的动作与细节
53
- - 原著**不能反向推翻集纲**;如发现矛盾,**先按集纲写**,矛盾点不在正文里解释
54
-
55
- ## 角色 + 结构双核(写作时必须暗含,不加额外表格)
56
-
57
- - **标签化出场**:角色出场前 5 秒,用标志动作 / 字幕 / 核心台词立住人设标签
58
- - 首次出场建议带 1 个核心人设标签(5 秒识人),例如:`> [act] 字幕:韩阳,画传集团总裁/狠厉果决`
59
- - **主角法则**:主角必须有缺陷与成长趋势(前期隐忍 / 后期狠辣等),所有行为由"目标导向"驱动
60
- - **对手法则**:对手能力匹配主角;行动步骤严密;阴谋必须咬住主角核心利益(名声、岗位、孩子、证据、生命线)
61
- - **伙伴法则**:只提供功能性帮助(情报 / 门槛 / 武力 / 程序),不抢主角高光
62
- - **爱情伙伴**(如有):暗含"相识→互补→互助→共谋→误会→决裂→患难→真情"的推进(不写成标题)
63
-
64
- ## 台词极简与动作化(强制硬指标)
65
-
66
- - 🔴 **单人单次对白 ≤ 40 字**(最重要的硬约束,超过即视为不合格)
67
- - 🔴 单句台词超过 20 字时**必须**配动作(紧跟一行 `> [act]` 或在 `> [dlg]` 锚点前后插动作锚点);避免一口气站桩输出
68
- - 🔴 每句核心台词前后必须有可拍动作或道具交互;能砸文件 / 按住 / 夺走 / 推开 / 撕毁 / 锁门 / 当众逼签,**就不要"讲道理"**
69
- - 拒绝说教、寒暄、水词;每句要带身份、习惯、目的(称呼、口头禅、句式)
70
- - 角色互换台词检测:把这句话换给别的角色说,是否违和?违和才说明人设立住
71
- - 情绪 / 神态用 `> [dlg|名字|情绪]` 的第三段标签(如 `冷笑` / `喘息` / `愤怒`),**不要**写括号内动作
72
- - 不要堆"金句大全",每句落到当下事件链
73
- - 单集对白行 : 动作行 ≈ 6:4 至 5:5;每集至少 15-20 句有实质内容的人物台词;**不允许连续 3 个以上 `> [act]` 画面行**,必须用 `> [dlg]` 打断
74
-
75
- ## 冲突层层升级(硬指标)
76
-
77
- - 🔴 单集内**至少 2-3 次"对抗升级点"**:筹码加大 / 场域更公开 / 代价更真实 / 规则更硬 / 底牌被迫亮出
78
- - 优先把冲突落到"行动阻拦"(抢 / 拦 / 按 / 撕 / 摔 / 锁门 / 举报 / 扣证据 / 当众逼签)
79
- - 优先把对抗落到"规则与程序"(单位流程 / 计生 / 处分 / 证据链 / 公开通报)
80
- - 优先把胜负落到"硬筹码"(证据、名额、文件、录音、印章、诊断单、检举信)
81
- - 优先"当众发生":能公开就公开(围观 = 压迫感 = 爽点放大)
82
- - 升级形态随剧情选择,**不固定**单一路径;但必须让观众感觉对抗在加码、风险在变大
83
- - 缓冲集允许更软,但也要有明确对抗动作与信息增量;**禁止整集只聊天无推进**
84
-
85
- ## 绝对视听转化(反小说化清单)
86
-
87
- - 只写镜头能拍到、麦克风能录到的内容
88
- - 🔴 禁止"觉得 / 认为 / 心里 / 忽然明白"等不可拍心理句
89
- - ❌ "她内心翻江倒海" / "他陷入沉思" / "脑海中闪过往事"
90
- - ❌ "她不知道的是…" / "命运的齿轮开始转动…" / "暴风雨即将来临…"
91
- - ❌ "眼中闪过一丝复杂的情绪" / "空气仿佛凝固" / "时间静止"
92
- - ❌ 抒情式过渡句、起承转合解说、"本集讲了什么"摘要句
93
- - ❌ 心理总结、长定语堆叠
94
- - ❌ 站桩对话(一口气说一大段不带动作)
95
- - 允许 `> [think|名字]` 内心独白,但仅用于揭示**关键信息差**,且**要短**
96
- - 闪回必须用 `> [act] 闪回开始` … `> [act] 闪回结束` 明确标注,且仅服务信息差 / 名场面
97
-
98
- ## 资产登记规则
99
-
100
- - `## 人物 / 场景 / 道具 / 发声源` 只列**本集首次出现且需要跨场景复用**的项;单场临时角色 / 背景家具 / 一次性消耗品**不进**资产段
101
- - 已存在角色清单里的项,**仅在 ref 处用名字**(如 `> [dlg|苏景行] 台词`),**不重复登记到 `## 人物` 段** — 重复登记会被 collision detector 拦截,导致 draft 失败
102
- - 名字必须是规范实体名(不夹括号状态 / 身份 / 情绪 / 动作注解)
103
- - 群体词(众人 / 群众 / 围观者 / 路人们 / 大家)**不进** `## 人物`,用 `group:名字` 发声源或留在 `> [act]` 文本里
104
- - 非人发声(系统提示音 / 广播 / 道具说话)用 `> [dlg|system:名字]` / `> [dlg|broadcast:名字]` / `> [dlg|prop:名字]`,**不要**当成 actor 登记
105
-
106
- ## 输出纪律
107
-
108
- - 直接以 `# 剧本` 开头输出,不要写解释 / 计划 / 自检报告 / 节奏标签 / 起承转合解说
109
- - 不要用 ``` 围栏包整份文档(`# 剧本` / `# 资产` 是固定章节标题,不是代码块)
110
- - 不要在正文里写"黄金开场 / 矛盾拉扯 / 极限卡点"这类区块名
111
- - 不要输出 `第X集:副标题` 等首行标题
112
- - 严格按集纲的事件骨架与结尾钩子推进,不加支线、不拖延卡点
113
- - 每集必须完整落地"开场段钩子 → 过程推进 → 过程段核心兑现 → 结尾挂点",**不得**用简化版 / 摘要版 / 梗概化正文冒充完成稿
114
- - 每集结尾必须有强钩子(结局集除外,结局集需要回收伏笔和余韵)
115
- - 已存在角色清单中的角色,**只在 ref 处用名字,不重复登记到 `## 人物`**
@@ -1,107 +0,0 @@
1
- /**
2
- * Episode drafting via configured provider.
3
- *
4
- * `gemini-writer` is the bridge between scriptctl's `episode draft` orchestrator and
5
- * the underlying provider. It does two things:
6
- *
7
- * 1. **Context assembly** — takes an agent-supplied list of reference files
8
- * (`refs: [{title, path}, ...]`) plus the per-episode outline, reads them off
9
- * disk, and composes a single prompt string conforming to the writing-prompt
10
- * template. scriptctl itself knows nothing about anchor.md / analysis-workspace /
11
- * adaptation-workspace etc. — those names belong to whichever SKILL is calling.
12
- * 2. **Provider invocation** — calls `provider.complete(prompt)` and returns raw
13
- * markdown for downstream parsing.
14
- *
15
- * The only files scriptctl looks up *automatically* are inside its own writing
16
- * directory (`<workspace>/episodes/`): the per-episode outline (when default
17
- * template is used) and the previous episode's body (for style continuity).
18
- * Everything else must be passed in via `refs`.
19
- */
20
- import type { AssetRegistry } from "../domain/asset-registry.js";
21
- export interface RefDeclaration {
22
- title: string;
23
- path: string;
24
- /** When true, missing file is silently skipped; when false, missing file throws. Default true. */
25
- optional?: boolean;
26
- }
27
- export interface ContextBlock {
28
- title: string;
29
- body: string;
30
- }
31
- export interface WriterContext {
32
- outlineText: string;
33
- refBlocks: ContextBlock[];
34
- previousEpisodeBody?: string;
35
- }
36
- export interface WriterRequest {
37
- episode: number;
38
- context: WriterContext;
39
- registry: AssetRegistry;
40
- /** Content of the writing-prompt template (e.g. `gemini-writing-prompt-perf.md`). */
41
- promptTemplate: string;
42
- /** When retrying after parser failure, inject the parser's error report. */
43
- parserErrorFeedback?: string;
44
- /** Optional override for the max tokens budget. */
45
- maxTokens?: number;
46
- }
47
- export interface WriterResponse {
48
- raw: string;
49
- prompt: string;
50
- }
51
- export interface CompletionProvider {
52
- complete(prompt: string, maxTokens?: number): Promise<string>;
53
- }
54
- /**
55
- * Substitute `{NN}` placeholder in a path template with the given 2-digit-padded
56
- * episode number. `{NN}` is the only form scriptctl uses anywhere.
57
- */
58
- export declare function resolveTemplate(template: string, episode: number): string;
59
- /**
60
- * Parse one `--ref "<title>=<path>"` CLI argument into a RefDeclaration. The agent
61
- * passes labels in the same language they want them to appear in the prompt; the
62
- * label becomes the `## <label>` heading in the assembled prompt.
63
- */
64
- export declare function parseRefArg(raw: string): RefDeclaration;
65
- /** Dedupe a list of refs by title; later entries win. */
66
- export declare function dedupeRefs(refs: RefDeclaration[]): RefDeclaration[];
67
- /**
68
- * Assemble the full prompt for one `episode draft` call.
69
- *
70
- * Sections (in order):
71
- * 1. The writing-prompt template (output format + creative rules)
72
- * 2. 已存在资产清单 (collision prevention — auto from scriptctl-owned registry)
73
- * 3. Agent-supplied reference blocks (anchor / setup / adaptation plan / ...)
74
- * 4. **当前集集纲** (the episode-specific outline, REQUIRED)
75
- * 5. Optional parser feedback (when retrying after a failed parse)
76
- * 6. Footer reminder
77
- */
78
- export declare function composePrompt(req: WriterRequest): string;
79
- export interface LoadContextOptions {
80
- /** Workspace root (scriptctl's own output dir; default `workspace`). */
81
- workspace: string;
82
- /** Episode number being drafted. */
83
- episode: number;
84
- /**
85
- * Outline path (may contain `{NN}` placeholder). Resolved relative to CWD unless
86
- * absolute. If omitted, defaults to `<workspace>/episodes/ep<NN>.outline.md`.
87
- */
88
- outlineTemplate?: string;
89
- /** Reference blocks to inject (agent-supplied via --ref). */
90
- refs: RefDeclaration[];
91
- }
92
- /**
93
- * Read the outline + all declared refs + the previous-episode tail (when present).
94
- *
95
- * Throws Error (caller wraps in CliError) if the per-episode outline can't be
96
- * located: the writer must have something to write _from_.
97
- *
98
- * Missing optional refs are silently skipped. Missing required refs (optional:false)
99
- * throw.
100
- */
101
- export declare function loadWriterContext(opts: LoadContextOptions): WriterContext;
102
- /**
103
- * Compose the prompt + invoke the provider. Single-shot — retry on parser failure
104
- * is the orchestrator's responsibility (it constructs a new `WriterRequest` with
105
- * `parserErrorFeedback` set and calls `draftEpisode` again).
106
- */
107
- export declare function draftEpisode(provider: CompletionProvider, req: WriterRequest): Promise<WriterResponse>;