@lingjingai/scriptctl 0.63.3 → 0.63.5
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 +4 -2
- package/changes/0.63.4.md +5 -0
- package/changes/0.63.5.md +6 -0
- package/dist/bin.js +75 -28
- package/dist/bin.js.map +2 -2
- package/dist/llm/router.js +77 -32
- package/dist/llm/router.js.map +1 -1
- package/dist/review-agent.js +383 -226
- package/dist/review-agent.js.map +4 -4
- package/dist/usecases/script/review.js +2 -1
- package/dist/usecases/script/review.js.map +1 -1
- package/package.json +2 -1
- package/skills/scriptctl/SKILL.md +6 -2
- package/skills/scriptctl/references/ingest-workflow.md +2 -2
package/README.md
CHANGED
|
@@ -55,7 +55,9 @@ Gemini 接收左侧带时间戳、行锚点的亚洲格式剧本正文,以及
|
|
|
55
55
|
- **漏台词**:记录 ASR 原话、时间范围、邻近位置和理由,不直接插入 ASR 文本。随后对应集 Agent 收到清单和切好的看片窗口,逐项 `watch` 核对原片后修复并记录 finding。每处线索按窗口数增加看片预算,长句拆成不超过 15 秒的窗口。
|
|
56
56
|
- **无法可靠对应**:保留原样,把具体说明交给 Agent。服务请求失败、无效响应或非法修正目标记为本集失败,继续后续集;失败集不应用对比方案、不启动本集 Agent。已完成集的修改保留,失败清单在 `episode` 事件及最终统计中体现。
|
|
57
57
|
|
|
58
|
-
剧本旁 `.review/asr/episode-N/` 保存按音频 SHA-256 复用的成功 ASR 响应,以及每次对比的 `script.txt`、`asr.txt`、`prompt.txt
|
|
58
|
+
剧本旁 `.review/asr/episode-N/` 保存按音频 SHA-256 复用的成功 ASR 响应,以及每次对比的 `script.txt`、`asr.txt`、`prompt.txt`、逐模型逐次响应和 `report.json`(含前后时间、台词、缺漏线索)。当前剧本每次重新对比,不缓存对比结论。
|
|
59
|
+
|
|
60
|
+
审核 Agent、ASR 文本比对和原片视频核对均使用所选 Gemini → 其余 3.8 / 3.7 / 3.6 Flash → 豆包的恢复顺序。Gemini 沿用 provider 的 `maxAttempts`,豆包最多尝试三次;明确内容过滤进入下一模型。Mastra 在当前模型调用内切换,已执行成功的工具不重放。豆包使用 `ARK_API_KEY`,端点可通过 `ARK_MODEL` 指定。
|
|
59
61
|
|
|
60
62
|
`asr-started` / `asr-transcribed` / `asr-compared` NDJSON 按集报告进度与证据;`asr-compared` 不代表修改已落盘,带 `episode_number` 的 `asr-applied` 表示本集校时已经保存。之后的 `episode` 报告本集完成或失败,已有修改已保存;`done.statistics.fixed` 包含自动校时条数,`usage_total` 汇总对比与审核 Gemini 用量。
|
|
61
63
|
|
|
@@ -136,7 +138,7 @@ MIT
|
|
|
136
138
|
|
|
137
139
|
### 指定视频理解服务
|
|
138
140
|
|
|
139
|
-
Gemini
|
|
141
|
+
Gemini 文本、结构化工具和视频请求先使用配置的主模型,再按 `gemini-3.8-flash → gemini-3.7-flash → gemini-3.6-flash` 尝试尚未调用的模型,全部失败后使用豆包。每个 Gemini 模型按 provider 的 `maxAttempts` 重试暂时性错误,豆包最多尝试 3 次;审核拒绝等不可重试错误直接进入下一模型,用户取消立即停止。切换 Gemini 模型保留服务地址、凭据和同一份视频;公共 Files API 上传重试耗尽时,由豆包读取本地视频。豆包使用 `ARK_API_KEY`,`ARK_MODEL` 可指定已开通的视频理解端点,默认 `ep-20260721150136-98dj9`。完整模型链耗尽后才汇总报错,调用和用量逐模型记录。直接选择豆包时只调用豆包:
|
|
140
142
|
|
|
141
143
|
```bash
|
|
142
144
|
# 豆包:默认读取 ARK_API_KEY;模型也可通过 ARK_MODEL 设置
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# 0.63.5
|
|
2
|
+
|
|
3
|
+
- `review` 的逐集与全局 Agent、ASR 文本比对、原视频核对统一执行所选 Gemini → 其余 3.8 / 3.7 / 3.6 Flash → 豆包模型链,各模型使用有限重试。
|
|
4
|
+
- Gemini 以 HTTP 200 返回内容过滤时也进入下一模型,避免将空响应当作审核完成。
|
|
5
|
+
- Agent 在当前模型调用内切换,保留已成功的工具执行;视频核对复用同一段原片,ASR 比对保存逐模型响应及用量。
|
|
6
|
+
- 审核沿用所选 provider 的地址、凭据与重试预算;豆包使用 `ARK_API_KEY` 和 `ARK_MODEL`,最多尝试三次。
|
package/dist/bin.js
CHANGED
|
@@ -7802,6 +7802,7 @@ var DOUBAO_FALLBACK = {
|
|
|
7802
7802
|
timeoutMs: 6e5,
|
|
7803
7803
|
maxAttempts: 3
|
|
7804
7804
|
};
|
|
7805
|
+
var GEMINI_FLASH_MODELS = ["gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash"];
|
|
7805
7806
|
function normalizeBaseUrl(value) {
|
|
7806
7807
|
return value?.trim().replace(/\/+$/, "") ?? "anthropic-sdk-default";
|
|
7807
7808
|
}
|
|
@@ -7842,9 +7843,9 @@ function routedFailure(error, route) {
|
|
|
7842
7843
|
message: error.message
|
|
7843
7844
|
});
|
|
7844
7845
|
}
|
|
7845
|
-
function
|
|
7846
|
-
|
|
7847
|
-
const aggregate = aggregateFailures(failures, "
|
|
7846
|
+
function exhaustedFailure(error, failures) {
|
|
7847
|
+
if (failures.length === 1) return error;
|
|
7848
|
+
const aggregate = aggregateFailures(failures, "All configured model routes failed.");
|
|
7848
7849
|
return new LlmTransportError(error.code, aggregate.message, {
|
|
7849
7850
|
retryable: aggregate.retryable,
|
|
7850
7851
|
classification: classificationOf(aggregate),
|
|
@@ -7856,6 +7857,19 @@ function fallbackFailure(primary, error, route) {
|
|
|
7856
7857
|
function isGeminiRoute(route) {
|
|
7857
7858
|
return route.protocol === "google-gemini" || /(^|\/)gemini-/i.test(route.model);
|
|
7858
7859
|
}
|
|
7860
|
+
function geminiRoutes(primary) {
|
|
7861
|
+
const prefix = primary.info.model.slice(0, primary.info.model.lastIndexOf("/") + 1);
|
|
7862
|
+
const models = /* @__PURE__ */ new Set([primary.info.model, ...GEMINI_FLASH_MODELS.map((model) => prefix + model)]);
|
|
7863
|
+
return [...models].map((model) => {
|
|
7864
|
+
if (model === primary.info.model) return primary;
|
|
7865
|
+
const modelConfig = { ...primary.modelConfig, model };
|
|
7866
|
+
return {
|
|
7867
|
+
...primary,
|
|
7868
|
+
modelConfig,
|
|
7869
|
+
info: { ...primary.info, model, fingerprint: routeFingerprint(primary.info.task, primary.providerConfig, modelConfig) }
|
|
7870
|
+
};
|
|
7871
|
+
});
|
|
7872
|
+
}
|
|
7859
7873
|
function canFallback(error, signal) {
|
|
7860
7874
|
return !signal?.aborted && error instanceof LlmTransportError && error.code !== "cancelled";
|
|
7861
7875
|
}
|
|
@@ -8162,25 +8176,43 @@ var ModelRouter = class {
|
|
|
8162
8176
|
generate: async (request2) => {
|
|
8163
8177
|
request2.signal?.throwIfAborted();
|
|
8164
8178
|
if (request2.media.kind === "local-fallback") {
|
|
8165
|
-
return this.generateDoubaoVideo(task, request2, request2.media.primaryFailure);
|
|
8179
|
+
return this.generateDoubaoVideo(task, request2, [request2.media.primaryFailure]);
|
|
8166
8180
|
}
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8181
|
+
const routes = geminiRoutes(route);
|
|
8182
|
+
const failures = [];
|
|
8183
|
+
for (const [index, candidate] of routes.entries()) {
|
|
8184
|
+
request2.signal?.throwIfAborted();
|
|
8185
|
+
try {
|
|
8186
|
+
return await this.tracked(candidate.info, () => this.videoAdapter(candidate).generate({
|
|
8187
|
+
...request2,
|
|
8188
|
+
maxOutputTokens: spec2.maxOutputTokens
|
|
8189
|
+
}));
|
|
8190
|
+
} catch (error) {
|
|
8191
|
+
if (!canFallback(error, request2.signal)) throw error;
|
|
8192
|
+
const failure = routedFailure(error, candidate.info);
|
|
8193
|
+
failures.push(failure);
|
|
8194
|
+
const next = routes[index + 1];
|
|
8195
|
+
if (next) {
|
|
8196
|
+
process.stderr.write(`\u21AA\uFE0F ${candidate.info.model} \u8BF7\u6C42\u5931\u8D25\uFF0C\u5207\u6362\u5230 ${next.info.model} (${formatFailure(failure)})
|
|
8197
|
+
`);
|
|
8198
|
+
continue;
|
|
8199
|
+
}
|
|
8200
|
+
if (!this.doubaoAdapter() || !request2.localPath) throw exhaustedFailure(error, failures);
|
|
8201
|
+
return this.generateDoubaoVideo(task, request2, failures);
|
|
8202
|
+
}
|
|
8172
8203
|
}
|
|
8204
|
+
throw new Error("Gemini model routes cannot be empty.");
|
|
8173
8205
|
}
|
|
8174
8206
|
};
|
|
8175
8207
|
}
|
|
8176
|
-
async generateDoubaoVideo(task, request2,
|
|
8208
|
+
async generateDoubaoVideo(task, request2, failures) {
|
|
8177
8209
|
const adapter = this.doubaoAdapter();
|
|
8178
8210
|
if (!adapter) throw credentialError("doubao");
|
|
8179
8211
|
const localPath = request2.media.kind === "local-fallback" ? request2.media.path : request2.localPath;
|
|
8180
8212
|
if (!localPath) throw configRuntimeError("Doubao video fallback requires the local video.", ["localPath"], ["localPath: missing"]);
|
|
8181
8213
|
const localMime = request2.localMime ?? (request2.media.kind === "file-api" ? request2.media.file.mimeType : request2.media.mimeType);
|
|
8182
8214
|
const route = this.doubaoRoute(task);
|
|
8183
|
-
process.stderr.write(`\u21AA\uFE0F
|
|
8215
|
+
process.stderr.write(`\u21AA\uFE0F Gemini \u8DEF\u7531\u5DF2\u8017\u5C3D\uFF0C\u56DE\u9000\u5230\u8C46\u5305 ${route.model}
|
|
8184
8216
|
`);
|
|
8185
8217
|
try {
|
|
8186
8218
|
return await this.tracked(route, () => adapter.run({
|
|
@@ -8195,7 +8227,7 @@ var ModelRouter = class {
|
|
|
8195
8227
|
}));
|
|
8196
8228
|
} catch (error) {
|
|
8197
8229
|
if (!canFallback(error, request2.signal)) throw error;
|
|
8198
|
-
throw
|
|
8230
|
+
throw exhaustedFailure(error, [...failures, routedFailure(error, route)]);
|
|
8199
8231
|
}
|
|
8200
8232
|
}
|
|
8201
8233
|
// Resolve the default fallback or explicitly selected Doubao video.
|
|
@@ -8224,24 +8256,38 @@ var ModelRouter = class {
|
|
|
8224
8256
|
}
|
|
8225
8257
|
async withTextFallback(task, signal, generate) {
|
|
8226
8258
|
const route = this.resolve(task);
|
|
8227
|
-
const
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
const
|
|
8233
|
-
if (!doubao) throw error;
|
|
8234
|
-
const primary = routedFailure(error, route.info);
|
|
8235
|
-
const fallback = this.doubaoRoute(task);
|
|
8236
|
-
process.stderr.write(`\u21AA\uFE0F ${route.info.model} \u8BF7\u6C42\u5931\u8D25\uFF0C\u56DE\u9000\u5230\u8C46\u5305 ${fallback.model} (${formatFailure(primary)})
|
|
8237
|
-
`);
|
|
8259
|
+
const gemini = isGeminiRoute(route.info);
|
|
8260
|
+
const routes = gemini ? geminiRoutes(route) : [route];
|
|
8261
|
+
const failures = [];
|
|
8262
|
+
for (const [index, candidate] of routes.entries()) {
|
|
8263
|
+
signal?.throwIfAborted();
|
|
8264
|
+
const adapter = this.adapter(candidate);
|
|
8238
8265
|
try {
|
|
8239
|
-
return await this.tracked(
|
|
8240
|
-
} catch (
|
|
8241
|
-
if (!canFallback(
|
|
8242
|
-
|
|
8266
|
+
return await this.tracked(candidate.info, () => generate(adapter));
|
|
8267
|
+
} catch (error) {
|
|
8268
|
+
if (!canFallback(error, signal) || !gemini && error.code !== "content_filtered") throw error;
|
|
8269
|
+
const failure = routedFailure(error, candidate.info);
|
|
8270
|
+
failures.push(failure);
|
|
8271
|
+
const next = routes[index + 1];
|
|
8272
|
+
if (next) {
|
|
8273
|
+
process.stderr.write(`\u21AA\uFE0F ${candidate.info.model} \u8BF7\u6C42\u5931\u8D25\uFF0C\u5207\u6362\u5230 ${next.info.model} (${formatFailure(failure)})
|
|
8274
|
+
`);
|
|
8275
|
+
continue;
|
|
8276
|
+
}
|
|
8277
|
+
const doubao = this.doubaoTextAdapter();
|
|
8278
|
+
if (!doubao) throw exhaustedFailure(error, failures);
|
|
8279
|
+
const fallback = this.doubaoRoute(task);
|
|
8280
|
+
process.stderr.write(`\u21AA\uFE0F \u4E3B\u6A21\u578B\u8DEF\u7531\u5DF2\u8017\u5C3D\uFF0C\u56DE\u9000\u5230\u8C46\u5305 ${fallback.model}
|
|
8281
|
+
`);
|
|
8282
|
+
try {
|
|
8283
|
+
return await this.tracked(fallback, () => generate(doubao));
|
|
8284
|
+
} catch (fallbackError) {
|
|
8285
|
+
if (!canFallback(fallbackError, signal)) throw fallbackError;
|
|
8286
|
+
throw exhaustedFailure(fallbackError, [...failures, routedFailure(fallbackError, fallback)]);
|
|
8287
|
+
}
|
|
8243
8288
|
}
|
|
8244
8289
|
}
|
|
8290
|
+
throw new Error("Text model routes cannot be empty.");
|
|
8245
8291
|
}
|
|
8246
8292
|
async generateText(task, request2) {
|
|
8247
8293
|
const spec2 = LLM_TASK_REGISTRY[task];
|
|
@@ -8594,7 +8640,7 @@ function resolveProfile(llm2, override) {
|
|
|
8594
8640
|
data: { expected: [provider.apiKeyEnv], actual: ["<empty>"] }
|
|
8595
8641
|
});
|
|
8596
8642
|
}
|
|
8597
|
-
return { provider: model.provider, model: model.model, baseUrl: provider.baseUrl, apiKey };
|
|
8643
|
+
return { provider: model.provider, model: model.model, baseUrl: provider.baseUrl, apiKey, maxAttempts: provider.maxAttempts };
|
|
8598
8644
|
}
|
|
8599
8645
|
async function loadAgent() {
|
|
8600
8646
|
const here = path13.dirname(fileURLToPath3(import.meta.url));
|
|
@@ -8696,6 +8742,7 @@ async function commandReview(opts) {
|
|
|
8696
8742
|
converge: opts["converge"] !== false,
|
|
8697
8743
|
convergeOnly: opts["converge_only"] === true,
|
|
8698
8744
|
apiKey: profile.apiKey,
|
|
8745
|
+
maxAttempts: profile.maxAttempts,
|
|
8699
8746
|
model: profile.model,
|
|
8700
8747
|
mediaModel: profile.model,
|
|
8701
8748
|
...profile.baseUrl ? { baseUrl: profile.baseUrl } : {},
|