@lingjingai/scriptctl 0.63.13 → 0.63.14

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.
@@ -0,0 +1,7 @@
1
+ # 0.63.14
2
+
3
+ ## Added
4
+ - `scriptctl ingest` appends each missing-timestamp response to `ingest-validation.jsonl`, including episode, time, attempt, missing count and locations, and retry exhaustion; history is retained after successful retries and resumed runs.
5
+
6
+ ## Fixed
7
+ - `scriptctl ingest` rejects video episodes with missing or empty Action timestamps, retries the model up to three times, and errors without accepting the episode if timestamps remain missing.
package/dist/bin.js CHANGED
@@ -16088,31 +16088,60 @@ function toSerialOutput(input) {
16088
16088
  }
16089
16089
  return out;
16090
16090
  }
16091
- async function runVideoSerial(port, media, episodeNo, contextText, fps, signal, local) {
16092
- const response = await port.generate({
16093
- media,
16094
- system: VIDEO_SERIAL_SYSTEM,
16095
- user: buildSerialUser(episodeNo, contextText),
16096
- fps,
16097
- responseJsonSchema: VIDEO_SERIAL_SCHEMA,
16098
- // Lets a cross-provider fallback (Doubao) re-upload the same transcode when the
16099
- // Gemini handle can't be reused. The Gemini path ignores these.
16100
- ...local ? { localPath: local.path, localMime: local.mime } : {},
16101
- ...signal ? { signal } : {}
16102
- });
16103
- const raw = response.text.trim();
16104
- if (raw.length < 40) throw serialError(`episode ${episodeNo}: serial output too short (finishReason=${response.finishReason})`, [`length: ${raw.length}`]);
16105
- let parsed;
16106
- try {
16107
- parsed = JSON.parse(raw);
16108
- } catch {
16109
- throw serialError(`episode ${episodeNo}: serial output was not valid JSON (finishReason=${response.finishReason})`, [raw.slice(0, 200)]);
16091
+ async function runVideoSerial(port, media, episodeNo, contextText, fps, signal, local, onTimestampMissing) {
16092
+ let correction = "";
16093
+ for (let attempt = 0; ; attempt++) {
16094
+ const response = await port.generate({
16095
+ media,
16096
+ system: VIDEO_SERIAL_SYSTEM,
16097
+ user: buildSerialUser(episodeNo, contextText + correction),
16098
+ fps,
16099
+ responseJsonSchema: VIDEO_SERIAL_SCHEMA,
16100
+ // Lets a cross-provider fallback (Doubao) re-upload the same transcode when the
16101
+ // Gemini handle can't be reused. The Gemini path ignores these.
16102
+ ...local ? { localPath: local.path, localMime: local.mime } : {},
16103
+ ...signal ? { signal } : {}
16104
+ });
16105
+ const raw = response.text.trim();
16106
+ if (raw.length < 40) throw serialError(`episode ${episodeNo}: serial output too short (finishReason=${response.finishReason})`, [`length: ${raw.length}`]);
16107
+ let parsed;
16108
+ try {
16109
+ parsed = JSON.parse(raw);
16110
+ } catch {
16111
+ throw serialError(`episode ${episodeNo}: serial output was not valid JSON (finishReason=${response.finishReason})`, [raw.slice(0, 200)]);
16112
+ }
16113
+ const clean2 = stripNul(parsed);
16114
+ const output = toSerialOutput(clean2);
16115
+ if (output.scenes.length === 0) throw serialError(`episode ${episodeNo}: serial output produced no scenes`, [raw.slice(0, 200)]);
16116
+ const missing = output.scenes.flatMap((scene, sceneIndex2) => scene.anchors.flatMap(
16117
+ (anchor, anchorIndex) => anchor.ts ? [] : [`scenes[${sceneIndex2}].anchors[${anchorIndex}] (${anchor.kind})`]
16118
+ ));
16119
+ if (missing.length > 0) {
16120
+ onTimestampMissing?.({
16121
+ event: "timestamp_missing",
16122
+ at: (/* @__PURE__ */ new Date()).toISOString(),
16123
+ episode: episodeNo,
16124
+ attempt: attempt + 1,
16125
+ missing_count: missing.length,
16126
+ missing_paths: missing,
16127
+ retries_exhausted: attempt === 3
16128
+ });
16129
+ if (attempt === 3) {
16130
+ throw new CliError(`Episode ${episodeNo}: ${missing.length} anchors still have no timestamp after 3 retries.`, {
16131
+ exitCode: EXIT_RUNTIME,
16132
+ errorCode: "INGEST_VIDEO_TIMESTAMP_MISSING",
16133
+ hint: "Inspect the episode error sidecar and rerun ingest to resume.",
16134
+ data: { actual: missing }
16135
+ });
16136
+ }
16137
+ correction = `
16138
+
16139
+ \u4E0A\u6B21\u8F93\u51FA\u6709 ${missing.length} \u6761\u955C\u5934\u6216\u58F0\u97F3\u7F3A\u5C11 ts\uFF0C\u672A\u88AB\u63A5\u53D7\u3002\u7F3A\u5931\u4F4D\u7F6E\uFF1A${missing.join("\u3001")}\u3002\u8BF7\u91CD\u65B0\u89C2\u770B\u5F53\u524D\u96C6\uFF0C\u4E3A\u6BCF\u4E00\u6761 shots \u548C voices \u586B\u5199\u771F\u5B9E\u8D77\u70B9 ts\uFF08MM:SS\uFF09\uFF0C\u4E0D\u5F97\u7F3A\u5931\u3001\u7559\u7A7A\u6216\u586B null\uFF1B\u8FD4\u56DE\u5B8C\u6574 JSON\uFF0C\u4E0D\u8981\u53EA\u8FD4\u56DE\u4FEE\u8865\u5B57\u6BB5\u3002`;
16140
+ continue;
16141
+ }
16142
+ const md = JSON.stringify(clean2, null, 2);
16143
+ return { md, output };
16110
16144
  }
16111
- const clean2 = stripNul(parsed);
16112
- const output = toSerialOutput(clean2);
16113
- if (output.scenes.length === 0) throw serialError(`episode ${episodeNo}: serial output produced no scenes`, [raw.slice(0, 200)]);
16114
- const md = JSON.stringify(clean2, null, 2);
16115
- return { md, output };
16116
16145
  }
16117
16146
 
16118
16147
  // src/usecases/ingest/failure.ts
@@ -16129,6 +16158,8 @@ function classificationForCliError(error) {
16129
16158
  return { failure_type: "media", failure_code: "media_too_large" };
16130
16159
  case "INGEST_VIDEO_SERIAL_FAILED":
16131
16160
  return { failure_type: "pipeline", failure_code: "serial_parse_failed" };
16161
+ case "INGEST_VIDEO_TIMESTAMP_MISSING":
16162
+ return { failure_type: "validation", failure_code: "script_validation_failed" };
16132
16163
  case "INGEST_SOURCE_MISMATCH":
16133
16164
  return { failure_type: "pipeline", failure_code: "source_mismatch" };
16134
16165
  case "INGEST_PIPELINE_MISMATCH":
@@ -16754,7 +16785,8 @@ async function runVideoIngestUnlocked(opts) {
16754
16785
  generationContext,
16755
16786
  fps,
16756
16787
  sig,
16757
- { path: enc.path, mime: enc.mime }
16788
+ { path: enc.path, mime: enc.mime },
16789
+ (event) => fs27.appendFileSync(store.path("ingest-validation.jsonl"), JSON.stringify(event) + "\n")
16758
16790
  );
16759
16791
  } catch (error) {
16760
16792
  if (!(error instanceof CliError) || error.errorCode !== "INGEST_VIDEO_SERIAL_FAILED" || attempt === VIDEO_VALIDATION_ATTEMPTS) throw error;
@@ -16816,7 +16848,7 @@ async function runVideoIngestUnlocked(opts) {
16816
16848
  const detail = formatFailure(sidecar.failure);
16817
16849
  store.writeJson(`ep/ep_${pad5(e.episode)}.error.json`, sidecar);
16818
16850
  store.writeJson("manifest.json", { ...manifest, updated: sidecar.occurred_at, current_error: sidecar });
16819
- if (err instanceof CliError && (err.errorCode === "INGEST_VIDEO_INLINE_TOO_LARGE" || err.errorCode === "INGEST_VALIDATION_FAILED")) throw err;
16851
+ if (err instanceof CliError && (err.errorCode === "INGEST_VIDEO_INLINE_TOO_LARGE" || err.errorCode === "INGEST_VALIDATION_FAILED" || err.errorCode === "INGEST_VIDEO_TIMESTAMP_MISSING")) throw err;
16820
16852
  throw new CliError(`Episode ${e.episode} failed serial parse; later episodes depend on its context.`, {
16821
16853
  exitCode: EXIT_INPUT,
16822
16854
  errorCode: "INGEST_SERIAL_INCOMPLETE",