@lingjingai/scriptctl 0.61.2 → 0.62.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 CHANGED
@@ -117,3 +117,23 @@ pnpm publish # access=public,会自动跑 prepublishOnly
117
117
  ## License
118
118
 
119
119
  MIT
120
+
121
+ ### 指定视频理解服务
122
+
123
+ 默认视频 ingest 沿用 Gemini 主流程,失败后由豆包兜底。显式选择服务时只调用所选服务和模型:
124
+
125
+ ```bash
126
+ # 豆包:默认读取 ARK_API_KEY;模型也可通过 ARK_MODEL 设置
127
+ scriptctl ingest --source-path ./video.mp4 --video-provider doubao --provider-model ep-your-endpoint
128
+
129
+ # 使用自定义凭据来源和服务地址
130
+ scriptctl ingest --source-path ./video.mp4 --video-provider doubao \
131
+ --provider-model ep-your-endpoint --provider-api-key-env MY_ARK_API_KEY \
132
+ --provider-base-url https://ark.cn-beijing.volces.com/api/v3
133
+
134
+ # Gemini:模型省略时使用配置中的视频模型
135
+ scriptctl ingest --source-path ./video.mp4 --video-provider gemini
136
+ ```
137
+
138
+ `--provider-api-key` 与 `--provider-api-key-env` 互斥。豆包仅支持默认的 `--upload-mode file-api`;
139
+ 直接选择豆包必须提供模型,且不需要 Gemini key。这些参数只影响视频理解阶段。
@@ -0,0 +1,4 @@
1
+ # 0.61.3
2
+
3
+ - Refresh the screenplay document indexes after every mutation, including in-memory review sessions. Repeated moves preserve action identity without duplicating actions or deleting unrelated ones; reads and subsequent edits see the current document.
4
+ - Stage scene and episode writes in a detached copy. Rejected writes leave both the document and its indexes unchanged.
@@ -0,0 +1,13 @@
1
+ # 0.61.4
2
+
3
+ ## Added
4
+ - None.
5
+
6
+ ## Changed
7
+ - None.
8
+
9
+ ## Fixed
10
+ - Preserve Google content-filter reason suffixes and report them as non-retryable filtering instead of empty responses.
11
+
12
+ ## Breaking
13
+ - None.
@@ -0,0 +1,5 @@
1
+ # 0.61.5
2
+
3
+ ## Fixed
4
+ - Review frame requests now describe even single timestamps as JSON arrays. Framework argument errors are returned to the agent as text so it can correct the request, instead of crashing while treating the error as image results.
5
+ - Valid frame images keep their existing model output format.
@@ -0,0 +1,13 @@
1
+ # 0.62.0
2
+
3
+ ## Added
4
+ - `scriptctl ingest --video-provider gemini|doubao` supports explicit video provider selection with existing model, API key, and base URL overrides. Direct Doubao requires `--provider-model` or `ARK_MODEL` and file-api upload mode. Explicit selection disables fallback; the default Gemini flow with Doubao fallback is unchanged.
5
+
6
+ ## Changed
7
+ - None.
8
+
9
+ ## Fixed
10
+ - None.
11
+
12
+ ## Breaking
13
+ - None.
package/dist/bin.js CHANGED
@@ -1648,7 +1648,7 @@ var GeminiVideoModel = class {
1648
1648
  return file;
1649
1649
  }
1650
1650
  async generate(request2) {
1651
- if (request2.media.kind === "local-fallback") {
1651
+ if (request2.media.kind === "local-fallback" || request2.media.kind === "local") {
1652
1652
  throw new LlmTransportError("invalid_response", "A local fallback video cannot be sent to Gemini.");
1653
1653
  }
1654
1654
  const config = {
@@ -1761,7 +1761,7 @@ var GeminiVideoModel = class {
1761
1761
  return { text: value.text, finishReason: value.finishReason, attempts, usage: addTokenUsage(failedUsage, value.usage), raw: value.raw };
1762
1762
  }
1763
1763
  async release(media, signal) {
1764
- if (media.kind === "base64" || media.kind === "local-fallback") return;
1764
+ if (media.kind === "base64" || media.kind === "local-fallback" || media.kind === "local") return;
1765
1765
  try {
1766
1766
  await this.client.files.delete({ name: media.file.name });
1767
1767
  } catch (error) {
@@ -1930,7 +1930,7 @@ var OpenAiCompatibleModel = class {
1930
1930
  const choice = firstChoice(payload);
1931
1931
  const reason = finishReason(choice);
1932
1932
  if (reason === "length") throw new LlmTransportError("truncated", "Provider output was truncated.");
1933
- if (reason === "content_filter") throw new LlmTransportError("content_filtered", "Provider filtered the response.");
1933
+ if (reason === "content_filter" || reason.startsWith("content_filter:")) throw new LlmTransportError("content_filtered", `Provider filtered the response (${reason}).`);
1934
1934
  const text2 = messageText(choice);
1935
1935
  if (!text2) throw new LlmTransportError("empty_response", "Provider returned an empty text response.");
1936
1936
  return { text: text2, finishReason: reason, attempts, usage, raw: payload };
@@ -1957,7 +1957,7 @@ var OpenAiCompatibleModel = class {
1957
1957
  const choice = firstChoice(payload);
1958
1958
  const reason = finishReason(choice);
1959
1959
  if (reason === "length") throw new LlmTransportError("truncated", "Provider tool output was truncated.");
1960
- if (reason === "content_filter") throw new LlmTransportError("content_filtered", "Provider filtered the response.");
1960
+ if (reason === "content_filter" || reason.startsWith("content_filter:")) throw new LlmTransportError("content_filtered", `Provider filtered the response (${reason}).`);
1961
1961
  const calls = message(choice)?.["tool_calls"];
1962
1962
  if (!Array.isArray(calls)) throw new LlmTransportError("invalid_response", "Provider did not return a tool call.");
1963
1963
  for (const call of calls) {
@@ -2065,6 +2065,11 @@ var ModelRouter = class {
2065
2065
  const cached2 = this.resolvedRoutes.get(task);
2066
2066
  if (cached2) return cached2;
2067
2067
  const spec2 = LLM_TASK_REGISTRY[task];
2068
+ if (spec2.mode === "video" && this.options.videoProvider?.provider === "doubao") {
2069
+ const resolved2 = this.resolveDoubao(task, true);
2070
+ this.resolvedRoutes.set(task, resolved2);
2071
+ return resolved2;
2072
+ }
2068
2073
  const profile = this.config.tasks[task] ?? spec2.defaultProfile;
2069
2074
  const modelConfig = this.config.models[profile];
2070
2075
  if (!modelConfig) throw configRuntimeError("The task route references a missing model profile.", [profile], [`task: ${task}`]);
@@ -2257,25 +2262,37 @@ var ModelRouter = class {
2257
2262
  };
2258
2263
  });
2259
2264
  }
2260
- doubaoRoute(task) {
2261
- const provider = {
2265
+ resolveDoubao(task, explicit) {
2266
+ const overrides = explicit ? this.options.videoProvider : void 0;
2267
+ const modelName = explicit ? (overrides?.model ?? process.env["ARK_MODEL"] ?? "").trim() : this.doubaoModel();
2268
+ if (!modelName) {
2269
+ throw configRuntimeError("Doubao requires --provider-model or ARK_MODEL.", ["--provider-model <id> or ARK_MODEL"], ["model: empty"]);
2270
+ }
2271
+ const providerConfig = {
2262
2272
  protocol: "openai-compatible",
2263
- baseUrl: DOUBAO_FALLBACK.baseUrl,
2273
+ baseUrl: overrides?.baseUrl ? normalizeProviderBaseUrl(overrides.baseUrl, "--provider-base-url") : DOUBAO_FALLBACK.baseUrl,
2264
2274
  apiKeyEnv: DOUBAO_FALLBACK.apiKeyEnv,
2265
2275
  timeoutMs: DOUBAO_FALLBACK.timeoutMs,
2266
2276
  maxAttempts: DOUBAO_FALLBACK.maxAttempts
2267
2277
  };
2268
- const model = { provider: "doubao", model: this.doubaoModel() };
2278
+ const modelConfig = { provider: "doubao", model: modelName };
2269
2279
  return {
2270
- task,
2271
- profile: "doubao-fallback",
2272
- provider: "doubao",
2273
- protocol: provider.protocol,
2274
- baseUrl: provider.baseUrl,
2275
- model: model.model,
2276
- fingerprint: routeFingerprint(task, provider, model)
2280
+ info: {
2281
+ task,
2282
+ profile: explicit ? "doubao" : "doubao-fallback",
2283
+ provider: "doubao",
2284
+ protocol: providerConfig.protocol,
2285
+ baseUrl: providerConfig.baseUrl,
2286
+ model: modelName,
2287
+ fingerprint: routeFingerprint(task, providerConfig, modelConfig)
2288
+ },
2289
+ providerConfig,
2290
+ modelConfig
2277
2291
  };
2278
2292
  }
2293
+ doubaoRoute(task) {
2294
+ return this.resolveDoubao(task, false).info;
2295
+ }
2279
2296
  adapter(route) {
2280
2297
  const key = `${route.info.provider}\0${route.info.model}\0${route.modelConfig.thinkingBudgetTokens ?? ""}`;
2281
2298
  const cached2 = this.adapters.get(key);
@@ -2317,6 +2334,38 @@ var ModelRouter = class {
2317
2334
  const spec2 = LLM_TASK_REGISTRY[task];
2318
2335
  if (spec2.mode !== "video") throw configRuntimeError("The task does not support video generation.", ["video task"], [`task: ${task}`]);
2319
2336
  const route = this.resolve(task);
2337
+ if (this.options.videoProvider?.provider === "doubao") {
2338
+ const adapter = this.doubaoAdapter(route);
2339
+ if (!adapter) throw credentialError("doubao");
2340
+ return {
2341
+ prepare: async (localPath, mimeType, mode, signal) => {
2342
+ if (mode !== "file-api") {
2343
+ throw configRuntimeError("Doubao requires file-api upload mode.", ["file-api"], [mode]);
2344
+ }
2345
+ signal?.throwIfAborted();
2346
+ return { kind: "local", path: localPath, mimeType };
2347
+ },
2348
+ generate: (request2) => {
2349
+ if (request2.media.kind !== "local") {
2350
+ throw configRuntimeError("Doubao requires locally prepared video.", ["local media"], [request2.media.kind]);
2351
+ }
2352
+ request2.signal?.throwIfAborted();
2353
+ const { path: localPath, mimeType: localMime } = request2.media;
2354
+ return this.tracked(route.info, () => adapter.run({
2355
+ localPath,
2356
+ localMime,
2357
+ user: request2.user,
2358
+ fps: request2.fps,
2359
+ maxOutputTokens: spec2.maxOutputTokens,
2360
+ ...request2.system ? { system: request2.system } : {},
2361
+ ...request2.responseJsonSchema ? { responseJsonSchema: request2.responseJsonSchema } : {},
2362
+ ...request2.signal ? { signal: request2.signal } : {}
2363
+ }));
2364
+ },
2365
+ release: async () => {
2366
+ }
2367
+ };
2368
+ }
2320
2369
  if (route.providerConfig.protocol !== "google-gemini") {
2321
2370
  throw configRuntimeError(
2322
2371
  "Video tasks require a google-gemini provider.",
@@ -2405,16 +2454,17 @@ var ModelRouter = class {
2405
2454
  }
2406
2455
  };
2407
2456
  }
2408
- // The Doubao fallback adapter, or null when ARK_API_KEY is unset. Cached per key.
2457
+ // Resolve an explicit Doubao route or the default fallback; absent fallback credentials skip it.
2409
2458
  // Overridable in tests to inject a fake Ark client.
2410
- doubaoAdapter() {
2411
- const apiKey = (process.env[DOUBAO_FALLBACK.apiKeyEnv] ?? "").trim();
2459
+ doubaoAdapter(route) {
2460
+ const apiKey = route ? this.videoCredential(route) : (process.env[DOUBAO_FALLBACK.apiKeyEnv] ?? "").trim();
2412
2461
  if (!apiKey) return null;
2413
- const model = this.doubaoModel();
2414
- const key = `doubao ${model}`;
2462
+ const model = route?.info.model ?? this.doubaoModel();
2463
+ const baseUrl = route?.providerConfig.baseUrl ?? DOUBAO_FALLBACK.baseUrl;
2464
+ const key = `doubao ${baseUrl} ${model}`;
2415
2465
  const cached2 = this.doubaoAdapters.get(key);
2416
2466
  if (cached2) return cached2;
2417
- const adapter = new DoubaoVideoModel(model, DOUBAO_FALLBACK.timeoutMs, DOUBAO_FALLBACK.maxAttempts, apiKey, DOUBAO_FALLBACK.baseUrl);
2467
+ const adapter = new DoubaoVideoModel(model, DOUBAO_FALLBACK.timeoutMs, DOUBAO_FALLBACK.maxAttempts, apiKey, baseUrl);
2418
2468
  this.doubaoAdapters.set(key, adapter);
2419
2469
  return adapter;
2420
2470
  }
@@ -4207,6 +4257,7 @@ function newId(prefix, taken) {
4207
4257
  return id;
4208
4258
  }
4209
4259
  function save(doc2) {
4260
+ Object.assign(doc2, parse(doc2.raw, doc2.path));
4210
4261
  if (!doc2.path) {
4211
4262
  step("save", "skipped: document is not bound to a file");
4212
4263
  return;
@@ -4940,6 +4991,13 @@ function preview(raw) {
4940
4991
  return clip(String(raw.content ?? ""), 40);
4941
4992
  }
4942
4993
  function write(doc2, addr, text2, opts = {}) {
4994
+ const draft = parse(structuredClone(doc2.raw));
4995
+ const result = writeDraft(draft, addr, text2, opts);
4996
+ doc2.raw = draft.raw;
4997
+ save(doc2);
4998
+ return result;
4999
+ }
5000
+ function writeDraft(doc2, addr, text2, opts) {
4943
5001
  if (addr === "+" || Number(addr) === doc2.episodes.length + 1) {
4944
5002
  const raw = { scenes: [] };
4945
5003
  if (doc2.version === 4)
@@ -12089,6 +12147,7 @@ var intOf = (v, fallback) => {
12089
12147
  return Number.isInteger(n) && n > 0 ? n : fallback;
12090
12148
  };
12091
12149
  var VIDEO_OPTION_KEYS = [
12150
+ "video_provider",
12092
12151
  "upload_mode",
12093
12152
  "provider_base_url",
12094
12153
  "provider_api_key",
@@ -12121,6 +12180,10 @@ function uploadModeOf(opts) {
12121
12180
  return mode;
12122
12181
  }
12123
12182
  function providerOverridesOf(opts) {
12183
+ const provider = requiredOptionValue(opts, "video_provider", "--video-provider");
12184
+ if (provider !== void 0 && provider !== "gemini" && provider !== "doubao") {
12185
+ throw invalidVideoOption("Video provider must be gemini or doubao.", ["--video-provider gemini|doubao"]);
12186
+ }
12124
12187
  const literalKeyProvided = optionProvided(opts, "provider_api_key");
12125
12188
  const keyEnvProvided = optionProvided(opts, "provider_api_key_env");
12126
12189
  if (literalKeyProvided && keyEnvProvided) {
@@ -12139,8 +12202,9 @@ function providerOverridesOf(opts) {
12139
12202
  let apiKey;
12140
12203
  if (literalKey !== void 0) apiKey = { kind: "literal", value: literalKey };
12141
12204
  else if (keyEnv !== void 0) apiKey = { kind: "env", name: keyEnv };
12142
- if (!baseUrl && !apiKey && !model) return void 0;
12205
+ if (!provider && !baseUrl && !apiKey && !model) return void 0;
12143
12206
  return {
12207
+ ...provider ? { provider } : {},
12144
12208
  ...baseUrl ? { baseUrl } : {},
12145
12209
  ...apiKey ? { apiKey } : {},
12146
12210
  ...model ? { model } : {}
@@ -12168,6 +12232,9 @@ async function commandIngest(opts, composition = PRODUCTION_LLM_COMPOSITION) {
12168
12232
  if (resolved.kind !== "text") {
12169
12233
  const uploadMode = uploadModeOf(opts);
12170
12234
  const videoProvider = providerOverridesOf(opts);
12235
+ if (videoProvider?.provider === "doubao" && uploadMode !== "file-api") {
12236
+ throw invalidVideoOption("Doubao requires --upload-mode file-api.", ["--upload-mode file-api"]);
12237
+ }
12171
12238
  const executorOptions = videoProvider ? { videoProvider } : void 0;
12172
12239
  const executor2 = composition.createExecutor(strOf3(opts["llm_config"]).trim() || void 0, executorOptions);
12173
12240
  const configuredRoute = executor2.route("ingest.video-serial");
@@ -15954,7 +16021,7 @@ function registerWorkflowCommands(program, state2, llm2) {
15954
16021
  "after",
15955
16022
  "\nExample:\n scriptctl migrate -p ./old-script.json -o ./script-v4.json --to 4\n"
15956
16023
  ).action(dispatch(commandMigrate, state2));
15957
- const ingest = program.command("ingest").description("Convert source files into a Script v4 workspace").option("-s, --source-path <path>", "Source file or directory").option("-w, --workspace-path <path>", "Workspace directory", "workspace").option("--llm-config <path>", "LLM configuration file").option("--concurrency <n>", "Maximum concurrent model calls").option("--fps <n>", "Video sampling rate").option("--upload-mode <mode>", "Video transport: file-api (default) or base64").option("--provider-base-url <url>", "Gemini-compatible provider service root").option("--provider-api-key <key>", "Provider API key (visible in process arguments; prefer --provider-api-key-env)").option("--provider-api-key-env <name>", "Environment variable containing the provider API key").option("--provider-model <model>", "Gemini-compatible provider model").option("--step", "Process only the next unfinished video episode").option("--force", "Rebuild completed stages").addHelpText("after", "\nExample:\n scriptctl ingest --source-path ./draft.md --workspace-path ./workspace\n").action(dispatch((options) => commandIngest(options, llm2), state2));
16024
+ const ingest = program.command("ingest").description("Convert source files into a Script v4 workspace").option("-s, --source-path <path>", "Source file or directory").option("-w, --workspace-path <path>", "Workspace directory", "workspace").option("--llm-config <path>", "LLM configuration file").option("--concurrency <n>", "Maximum concurrent model calls").option("--fps <n>", "Video sampling rate").option("--upload-mode <mode>", "Video transport: file-api (default) or base64").option("--video-provider <provider>", "Video provider: gemini or doubao (explicit selection disables fallback)").option("--provider-base-url <url>", "Selected video provider service root").option("--provider-api-key <key>", "Provider API key (visible in process arguments; prefer --provider-api-key-env)").option("--provider-api-key-env <name>", "Environment variable containing the provider API key").option("--provider-model <model>", "Selected video provider model (Doubao defaults to ARK_MODEL)").option("--step", "Process only the next unfinished video episode").option("--force", "Rebuild completed stages").addHelpText("after", "\nExample:\n scriptctl ingest --source-path ./draft.md --workspace-path ./workspace\n").action(dispatch((options) => commandIngest(options, llm2), state2));
15958
16025
  ingest.command("status").description("Show ingest workspace progress").option("-w, --workspace-path <path>", "Workspace directory").action(dispatch(commandIngestStatus, state2));
15959
16026
  ingest.command("skip").description("Manually accept the next failed video episode as empty").option("-w, --workspace-path <path>", "Workspace directory").requiredOption("--episode <n>", "Failed episode number").requiredOption("--reason <text>", "Why this episode is being skipped").addHelpText("after", '\nExample:\n scriptctl ingest skip -w ./workspace --episode 7 --reason "provider policy block"\n').action(dispatch(commandSkipVideoEpisode, state2));
15960
16027
  program.command("evidence").description("Generate local original-video reference frames from final Script v4 timestamps").option("-p, --script-path <path>", "Final Script v4 JSON file").requiredOption("-s, --source-path <path>", "Linked source-video file or episode directory").requiredOption("-w, --workspace-path <path>", "Project workspace directory").option("--force", "Re-extract frames even when deterministic output files already exist").addHelpText("after", "\nExample:\n scriptctl evidence -p ./script.json -s /Volumes/NAS/videos -w ./workspace --force\n").action(dispatch(commandGenerateVideoStateEvidence, state2));