@lingjingai/scriptctl 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/cli.js +309 -396
- package/dist/cli.js.map +1 -1
- package/dist/common.d.ts +9 -0
- package/dist/common.js.map +1 -1
- package/dist/domain/asset-registry.d.ts +141 -0
- package/dist/domain/asset-registry.js +318 -0
- package/dist/domain/asset-registry.js.map +1 -0
- package/dist/domain/collision-detector.d.ts +83 -0
- package/dist/domain/collision-detector.js +248 -0
- package/dist/domain/collision-detector.js.map +1 -0
- package/dist/domain/direct-core.d.ts +13 -1
- package/dist/domain/direct-core.js +19 -6
- package/dist/domain/direct-core.js.map +1 -1
- package/dist/domain/script-core.d.ts +11 -0
- package/dist/domain/script-core.js +34 -19
- package/dist/domain/script-core.js.map +1 -1
- package/dist/help-text.js +336 -4
- package/dist/help-text.js.map +1 -1
- package/dist/infra/converters.js +21 -7
- package/dist/infra/converters.js.map +1 -1
- package/dist/infra/default-writing-prompt.d.ts +31 -0
- package/dist/infra/default-writing-prompt.js +50 -0
- package/dist/infra/default-writing-prompt.js.map +1 -0
- package/dist/infra/default-writing-prompt.md +115 -0
- package/dist/infra/gemini-writer.d.ts +107 -0
- package/dist/infra/gemini-writer.js +207 -0
- package/dist/infra/gemini-writer.js.map +1 -0
- package/dist/infra/providers.d.ts +36 -0
- package/dist/infra/providers.js +186 -2
- package/dist/infra/providers.js.map +1 -1
- package/dist/output.js +26 -9
- package/dist/output.js.map +1 -1
- package/dist/usecases/episode.d.ts +48 -0
- package/dist/usecases/episode.js +1209 -0
- package/dist/usecases/episode.js.map +1 -0
- package/dist/usecases/script.d.ts +6 -2
- package/dist/usecases/script.js +49 -5
- package/dist/usecases/script.js.map +1 -1
- package/package.json +9 -5
|
@@ -0,0 +1,115 @@
|
|
|
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 处用名字,不重复登记到 `## 人物`**
|
|
@@ -0,0 +1,107 @@
|
|
|
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>;
|
|
@@ -0,0 +1,207 @@
|
|
|
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 * as fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { DEFAULT_BATCH_MAX_TOKENS, exists, readText } from "../common.js";
|
|
23
|
+
import { summarizeForPrompt } from "../domain/asset-registry.js";
|
|
24
|
+
/** Tail of the previous episode's body to inject for style/hook continuity. */
|
|
25
|
+
const PREV_EPISODE_TAIL_CHARS = 12_000;
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Template resolution
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
/**
|
|
30
|
+
* Substitute `{NN}` placeholder in a path template with the given 2-digit-padded
|
|
31
|
+
* episode number. `{NN}` is the only form scriptctl uses anywhere.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveTemplate(template, episode) {
|
|
34
|
+
return template.replace(/\{NN\}/g, String(episode).padStart(2, "0"));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse one `--ref "<title>=<path>"` CLI argument into a RefDeclaration. The agent
|
|
38
|
+
* passes labels in the same language they want them to appear in the prompt; the
|
|
39
|
+
* label becomes the `## <label>` heading in the assembled prompt.
|
|
40
|
+
*/
|
|
41
|
+
export function parseRefArg(raw) {
|
|
42
|
+
const idx = raw.indexOf("=");
|
|
43
|
+
if (idx <= 0 || idx === raw.length - 1) {
|
|
44
|
+
throw new Error(`invalid --ref format: "${raw}". Expected "<title>=<path>", e.g. --ref "全局设定=analysis-workspace/全局设定.md".`);
|
|
45
|
+
}
|
|
46
|
+
const title = raw.slice(0, idx).trim();
|
|
47
|
+
const path = raw.slice(idx + 1).trim();
|
|
48
|
+
if (!title || !path) {
|
|
49
|
+
throw new Error(`invalid --ref format: "${raw}". Both title and path must be non-empty.`);
|
|
50
|
+
}
|
|
51
|
+
return { title, path };
|
|
52
|
+
}
|
|
53
|
+
/** Dedupe a list of refs by title; later entries win. */
|
|
54
|
+
export function dedupeRefs(refs) {
|
|
55
|
+
const seen = new Map();
|
|
56
|
+
for (const ref of refs)
|
|
57
|
+
seen.set(ref.title, ref);
|
|
58
|
+
return [...seen.values()];
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Prompt composition
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
/**
|
|
64
|
+
* Strip control characters / heading-level markers from agent-supplied titles so
|
|
65
|
+
* a malformed `--ref "Title\n# Something=path"` can't escape the prompt structure.
|
|
66
|
+
*/
|
|
67
|
+
function sanitizeTitle(raw) {
|
|
68
|
+
return raw.replace(/[\r\n]/g, " ").replace(/[`]/g, "").replace(/^#+\s*/, "").trim() || "未命名段";
|
|
69
|
+
}
|
|
70
|
+
function renderBlocks(blocks) {
|
|
71
|
+
if (blocks.length === 0)
|
|
72
|
+
return "(暂无参考资料)";
|
|
73
|
+
return blocks.map((b) => `## ${sanitizeTitle(b.title)}\n\n${b.body.trim()}\n`).join("\n");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Assemble the full prompt for one `episode draft` call.
|
|
77
|
+
*
|
|
78
|
+
* Sections (in order):
|
|
79
|
+
* 1. The writing-prompt template (output format + creative rules)
|
|
80
|
+
* 2. 已存在资产清单 (collision prevention — auto from scriptctl-owned registry)
|
|
81
|
+
* 3. Agent-supplied reference blocks (anchor / setup / adaptation plan / ...)
|
|
82
|
+
* 4. **当前集集纲** (the episode-specific outline, REQUIRED)
|
|
83
|
+
* 5. Optional parser feedback (when retrying after a failed parse)
|
|
84
|
+
* 6. Footer reminder
|
|
85
|
+
*/
|
|
86
|
+
export function composePrompt(req) {
|
|
87
|
+
const sections = [];
|
|
88
|
+
sections.push(req.promptTemplate.trim());
|
|
89
|
+
sections.push("");
|
|
90
|
+
sections.push("---");
|
|
91
|
+
sections.push("");
|
|
92
|
+
const registrySummary = summarizeForPrompt(req.registry);
|
|
93
|
+
if (registrySummary) {
|
|
94
|
+
sections.push(registrySummary);
|
|
95
|
+
sections.push("");
|
|
96
|
+
sections.push("---");
|
|
97
|
+
sections.push("");
|
|
98
|
+
}
|
|
99
|
+
sections.push("# 项目上下文");
|
|
100
|
+
sections.push("");
|
|
101
|
+
const blocks = [...req.context.refBlocks];
|
|
102
|
+
if (req.context.previousEpisodeBody) {
|
|
103
|
+
blocks.push({
|
|
104
|
+
title: "上一集已生成正文(用于风格 / 钩子衔接)",
|
|
105
|
+
body: req.context.previousEpisodeBody,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
sections.push(renderBlocks(blocks));
|
|
109
|
+
sections.push("");
|
|
110
|
+
sections.push("---");
|
|
111
|
+
sections.push("");
|
|
112
|
+
sections.push(`# 当前集集纲(第 ${req.episode} 集)`);
|
|
113
|
+
sections.push("");
|
|
114
|
+
sections.push(req.context.outlineText.trim());
|
|
115
|
+
sections.push("");
|
|
116
|
+
sections.push("---");
|
|
117
|
+
sections.push("");
|
|
118
|
+
if (req.parserErrorFeedback) {
|
|
119
|
+
sections.push("# 上一次输出失败的原因(请这次修复后重新输出,避免重复错误)");
|
|
120
|
+
sections.push("");
|
|
121
|
+
sections.push("```");
|
|
122
|
+
sections.push(req.parserErrorFeedback.trim());
|
|
123
|
+
sections.push("```");
|
|
124
|
+
sections.push("");
|
|
125
|
+
sections.push("---");
|
|
126
|
+
sections.push("");
|
|
127
|
+
}
|
|
128
|
+
sections.push(`# 任务:按上面"输出格式(scriptctl spec markdown,硬约束)"的规则,输出第 ${req.episode} 集的剧本片段。`);
|
|
129
|
+
sections.push("");
|
|
130
|
+
sections.push("再次提醒:");
|
|
131
|
+
sections.push("- 直接以 `# 剧本` 开头,**不要**用 ``` 围栏包裹");
|
|
132
|
+
sections.push("- 不要写 `第X集:副标题` 等首行标题");
|
|
133
|
+
sections.push("- 资产段只列**本集首次出现且需要跨场景复用**的人物 / 场景 / 道具 / 发声源");
|
|
134
|
+
sections.push("- 已存在资产清单里的角色,**只在 ref 处用名字,不重复登记到 `## 人物`**");
|
|
135
|
+
return sections.join("\n");
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Context loading
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
function tryRead(filePath) {
|
|
141
|
+
if (!exists(filePath))
|
|
142
|
+
return undefined;
|
|
143
|
+
try {
|
|
144
|
+
const text = readText(filePath).trim();
|
|
145
|
+
return text || undefined;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function findPreviousEpisodeBody(episodesDir, episode) {
|
|
152
|
+
if (!fs.existsSync(episodesDir))
|
|
153
|
+
return undefined;
|
|
154
|
+
for (let n = episode - 1; n >= 1; n--) {
|
|
155
|
+
const candidate = path.join(episodesDir, `ep${String(n).padStart(2, "0")}.md`);
|
|
156
|
+
if (fs.existsSync(candidate)) {
|
|
157
|
+
const text = readText(candidate).trim();
|
|
158
|
+
if (text)
|
|
159
|
+
return text.slice(-PREV_EPISODE_TAIL_CHARS);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Read the outline + all declared refs + the previous-episode tail (when present).
|
|
166
|
+
*
|
|
167
|
+
* Throws Error (caller wraps in CliError) if the per-episode outline can't be
|
|
168
|
+
* located: the writer must have something to write _from_.
|
|
169
|
+
*
|
|
170
|
+
* Missing optional refs are silently skipped. Missing required refs (optional:false)
|
|
171
|
+
* throw.
|
|
172
|
+
*/
|
|
173
|
+
export function loadWriterContext(opts) {
|
|
174
|
+
const defaultOutlineTemplate = path.join(opts.workspace, "episodes", "ep{NN}.outline.md");
|
|
175
|
+
const outlinePath = resolveTemplate(opts.outlineTemplate ?? defaultOutlineTemplate, opts.episode);
|
|
176
|
+
const outlineText = tryRead(outlinePath);
|
|
177
|
+
if (!outlineText) {
|
|
178
|
+
throw new Error(`episode outline not found at ${outlinePath}. Pass --outline <path> (literal or template with {NN}), ` +
|
|
179
|
+
`or place the outline at ${defaultOutlineTemplate.replace(/\{NN\}/g, String(opts.episode).padStart(2, "0"))}.`);
|
|
180
|
+
}
|
|
181
|
+
const refBlocks = [];
|
|
182
|
+
for (const ref of opts.refs) {
|
|
183
|
+
const body = tryRead(ref.path);
|
|
184
|
+
if (body) {
|
|
185
|
+
refBlocks.push({ title: ref.title, body });
|
|
186
|
+
}
|
|
187
|
+
else if (ref.optional === false) {
|
|
188
|
+
throw new Error(`required ref "${ref.title}" not found at ${ref.path}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const previousEpisodeBody = findPreviousEpisodeBody(path.join(opts.workspace, "episodes"), opts.episode);
|
|
192
|
+
return { outlineText, refBlocks, previousEpisodeBody };
|
|
193
|
+
}
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Driver
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
/**
|
|
198
|
+
* Compose the prompt + invoke the provider. Single-shot — retry on parser failure
|
|
199
|
+
* is the orchestrator's responsibility (it constructs a new `WriterRequest` with
|
|
200
|
+
* `parserErrorFeedback` set and calls `draftEpisode` again).
|
|
201
|
+
*/
|
|
202
|
+
export async function draftEpisode(provider, req) {
|
|
203
|
+
const prompt = composePrompt(req);
|
|
204
|
+
const raw = await provider.complete(prompt, req.maxTokens ?? DEFAULT_BATCH_MAX_TOKENS);
|
|
205
|
+
return { raw, prompt };
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=gemini-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini-writer.js","sourceRoot":"","sources":["../../src/infra/gemini-writer.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,11 @@ import { type ProviderLike } from "../domain/direct-core.js";
|
|
|
2
2
|
type Dict = Record<string, unknown>;
|
|
3
3
|
export declare class MockProvider implements ProviderLike {
|
|
4
4
|
readonly name = "mock";
|
|
5
|
+
/**
|
|
6
|
+
* Mock completion for episode draft tests. Returns a small valid spec-md fragment.
|
|
7
|
+
* Real callers must never use mock provider for delivery.
|
|
8
|
+
*/
|
|
9
|
+
complete(_prompt: string, _maxTokens?: number): Promise<string>;
|
|
5
10
|
extractEpisode(sourceText: string, episodePlan: Dict): Dict;
|
|
6
11
|
extractBatch(sourceText: string, batchPlan: Dict): Dict;
|
|
7
12
|
extractMetadata(_sourceText: string, script: Dict): Dict;
|
|
@@ -15,11 +20,42 @@ export declare class AnthropicProvider implements ProviderLike {
|
|
|
15
20
|
constructor(model: string);
|
|
16
21
|
private messageRequest;
|
|
17
22
|
private collectResponseText;
|
|
23
|
+
/**
|
|
24
|
+
* Generic completion entry point. Used by episode subcommand's gemini-writer to draft
|
|
25
|
+
* one episode's spec markdown from the assembled prompt. Throws CliError when the
|
|
26
|
+
* provider truncates output (so the caller can surface a deterministic error rather
|
|
27
|
+
* than committing a half-baked episode).
|
|
28
|
+
*/
|
|
29
|
+
complete(prompt: string, maxTokens?: number): Promise<string>;
|
|
18
30
|
extractEpisode(sourceText: string, episodePlan: Dict): Promise<Dict>;
|
|
19
31
|
extractBatch(sourceText: string, batchPlan: Dict): Promise<Dict>;
|
|
20
32
|
extractEpisodeTitles(sourceText: string, episodePlan: Dict): Promise<Dict>;
|
|
21
33
|
extractAssetCuration(_sourceText: string, script: Dict): Promise<Dict>;
|
|
22
34
|
extractMetadata(_sourceText: string, script: Dict): Promise<Dict>;
|
|
23
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Google Gemini provider via REST API. No SDK dependency (uses fetch).
|
|
38
|
+
*
|
|
39
|
+
* Used as the default writer for `scriptctl episode draft`. The Anthropic provider
|
|
40
|
+
* stays available for other internal extraction tasks (metadata / title generation /
|
|
41
|
+
* direct-init batch extraction) where Claude tends to outperform Gemini on JSON
|
|
42
|
+
* schema adherence.
|
|
43
|
+
*
|
|
44
|
+
* Env:
|
|
45
|
+
* - `SCRIPTCTL_GEMINI_API_KEY` (or fallback `GEMINI_API_KEY`) required
|
|
46
|
+
* - `SCRIPTCTL_GEMINI_BASE_URL` optional override
|
|
47
|
+
* - `SCRIPTCTL_GEMINI_MODEL` optional default-model override
|
|
48
|
+
*/
|
|
49
|
+
export declare class GeminiProvider implements ProviderLike {
|
|
50
|
+
readonly name = "gemini";
|
|
51
|
+
private readonly apiKey;
|
|
52
|
+
private readonly baseUrl;
|
|
53
|
+
private readonly model;
|
|
54
|
+
constructor(model?: string);
|
|
55
|
+
/**
|
|
56
|
+
* Plain-text completion. Used by `episode draft` to write a spec-md episode body.
|
|
57
|
+
*/
|
|
58
|
+
complete(prompt: string, maxTokens?: number): Promise<string>;
|
|
59
|
+
}
|
|
24
60
|
export declare function makeProvider(name: string, model: string): ProviderLike;
|
|
25
61
|
export {};
|