videobook 0.2.5 → 0.3.1

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.
Files changed (98) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -2
  3. package/dist/archive-b2.js +1 -1
  4. package/dist/asset-media-projection.js +161 -0
  5. package/dist/cli/args.js +42 -3
  6. package/dist/cli/config-command.js +17 -1
  7. package/dist/cli/doctor-command.js +15 -0
  8. package/dist/cli/har-command.js +86 -0
  9. package/dist/cli/main.js +13 -1
  10. package/dist/command-reference.js +3 -1
  11. package/dist/content-store.js +2 -1
  12. package/dist/diagnostics.js +2 -2
  13. package/dist/env-secrets.js +1 -0
  14. package/dist/generation-har.js +106 -0
  15. package/dist/generation-reference-mentions.js +19 -0
  16. package/dist/goal-plan-v3.js +11 -1
  17. package/dist/goal-plan.js +1 -1
  18. package/dist/goal-store.js +2 -2
  19. package/dist/har-capture.js +175 -0
  20. package/dist/har-export.js +238 -0
  21. package/dist/har-index.js +98 -0
  22. package/dist/har-retention.js +73 -0
  23. package/dist/har-settings.js +27 -0
  24. package/dist/jobs/_alibaba-upload.js +2 -4
  25. package/dist/jobs/_fal-upload.js +2 -4
  26. package/dist/jobs/_image-edit-adapter.js +3 -1
  27. package/dist/jobs/_kie-upload.js +2 -4
  28. package/dist/jobs/assess-media-probes.js +94 -0
  29. package/dist/jobs/generate-media.js +10 -1
  30. package/dist/jobs/image-subjects.js +3 -1
  31. package/dist/jobs/kie-paygate.js +10 -3
  32. package/dist/jobs/register-all.js +2 -0
  33. package/dist/jobs/run-notebook.js +20 -2
  34. package/dist/jobs/xai-paygate.js +5 -2
  35. package/dist/library-asset-projection.js +141 -0
  36. package/dist/lightning-defaults.js +4 -4
  37. package/dist/media/adapters/kie.js +2 -1
  38. package/dist/media/catalog.js +8 -2
  39. package/dist/media/cost.js +1 -1
  40. package/dist/media/descriptor.js +7 -4
  41. package/dist/media/frontier-gaps.js +0 -1
  42. package/dist/media/har-endpoint.js +25 -0
  43. package/dist/media/json-schema.js +3 -0
  44. package/dist/media/preferred-defaults.js +161 -0
  45. package/dist/media/prompt-dialects.js +9 -1
  46. package/dist/media-library.js +158 -153
  47. package/dist/notebook-sticky-settings.js +60 -0
  48. package/dist/package-version.js +18 -0
  49. package/dist/posts/dropbox-auth.js +1 -1
  50. package/dist/posts/post-metrics.js +2 -1
  51. package/dist/provider-usage.js +1 -18
  52. package/dist/queue.js +12 -1
  53. package/dist/serve.js +2 -0
  54. package/dist/tools/api-status.js +1 -1
  55. package/dist/tools/generate-media.js +3 -0
  56. package/dist/tools/generation-payload.js +17 -2
  57. package/dist/tools/get-generation-har.js +43 -0
  58. package/dist/tools/index.js +4 -0
  59. package/dist/tools/list-cell-generations.js +19 -0
  60. package/dist/tools/reconcile-project-work.js +52 -193
  61. package/dist/tools/set-har-capture.js +30 -0
  62. package/dist/tools/videobook-domain.js +16 -1
  63. package/dist/upload.js +2 -1
  64. package/dist/usage-providers.js +48 -10
  65. package/dist/workspace-action-groups.js +2 -2
  66. package/dist/x/account.js +1 -1
  67. package/dist/x/authenticated-handle.js +1 -1
  68. package/dist/x/oauth-flow.js +1 -1
  69. package/dist/x/post-to-x.js +2 -1
  70. package/dist/x/x-tokens.js +2 -1
  71. package/package.json +15 -14
  72. package/ui/assets/{alive-captions-duTKsyll.js → alive-captions-CnLK22ED.js} +1 -1
  73. package/ui/assets/confirm-bodies-BaalBxp1.js +1 -0
  74. package/ui/assets/{fullscreen-COrmiHPn.js → fullscreen-DTJQJ6dX.js} +1 -1
  75. package/ui/assets/{goal-ui-D5Xsm2EY.js → goal-ui-CN-kZTEO.js} +2 -2
  76. package/ui/assets/index-CTPstLYq.css +1 -0
  77. package/ui/assets/index-Dp81yChE.js +37 -0
  78. package/ui/assets/inspector-jeyma7HL.js +1 -0
  79. package/ui/assets/{jobs-ui-DdP2Re41.js → jobs-ui-D6S3LJ4j.js} +1 -1
  80. package/ui/assets/{music-video-B709JZqa.js → music-video-CDqr0X6G.js} +1 -1
  81. package/ui/assets/ui-BBM3YCY6.css +1 -0
  82. package/ui/assets/ui-Cqs1-YNg.js +2 -0
  83. package/ui/assets/vendor-BWf-WCsY.js +19 -0
  84. package/ui/assets/{vendor-flow-CKiuzaeM.js → vendor-flow-DPvi8ynA.js} +1 -1
  85. package/ui/assets/vendor-icons-D3KrGMJY.js +1 -0
  86. package/ui/assets/vendor-mcp-BSR9acpG.js +60 -0
  87. package/ui/assets/videobook-b9VODNOA.js +21 -0
  88. package/ui/index.html +14 -14
  89. package/ui/assets/confirm-bodies--2sc9DbH.js +0 -1
  90. package/ui/assets/index-DEKl00St.js +0 -37
  91. package/ui/assets/index-DWfR79lA.css +0 -1
  92. package/ui/assets/inspector-EBp3IhvH.js +0 -1
  93. package/ui/assets/ui-Oirr9VOf.css +0 -1
  94. package/ui/assets/ui-jRPWsNUw.js +0 -2
  95. package/ui/assets/vendor-BFupLz37.js +0 -19
  96. package/ui/assets/vendor-icons-CGqhCIXV.js +0 -1
  97. package/ui/assets/vendor-mcp-DQEeVZH0.js +0 -67
  98. package/ui/assets/videobook-0DOLlhG2.js +0 -20
@@ -1,3 +1,4 @@
1
+ import { setHarCaptureDef } from "./set-har-capture.js";
1
2
  import { coldOpenDefs } from "./cold-open.js";
2
3
  import { withBookContext } from "../book-context.js";
3
4
  import { bookManager } from "../engine.js";
@@ -78,6 +79,7 @@ import { listAssetsDef } from "./list-assets.js";
78
79
  import { listCellGenerationsDef } from "./list-cell-generations.js";
79
80
  import { waitForAssetReadyDef } from "./wait-for-asset-ready.js";
80
81
  import { generationPayloadDef } from "./generation-payload.js";
82
+ import { getGenerationHarDef } from "./get-generation-har.js";
81
83
  import { concatImagesDef } from "./concat-images.js";
82
84
  import { chatDef } from "./chat.js";
83
85
  import { chainDef } from "./chain.js";
@@ -250,6 +252,7 @@ export function getAllToolDefs(engine, books = bookManager) {
250
252
  listChatModelsDef(),
251
253
  providerKeyStatusDef(),
252
254
  setEnvSecretDef(),
255
+ setHarCaptureDef(),
253
256
  listElevenLabsVoicesDef(),
254
257
  estimateCostDef(engine, books),
255
258
  bookUsageDef(engine),
@@ -285,6 +288,7 @@ export function getAllToolDefs(engine, books = bookManager) {
285
288
  listCellGenerationsDef(engine),
286
289
  waitForAssetReadyDef(engine),
287
290
  generationPayloadDef(engine),
291
+ getGenerationHarDef(engine),
288
292
  concatImagesDef(engine),
289
293
  chatDef(engine),
290
294
  chainDef(engine),
@@ -2,6 +2,20 @@ import { z } from "zod";
2
2
  import { findGenerateAudioOutput, findGenerateImageOutput, findGenerateVideoOutput, listGenerateImageOutputs, listGenerateVideoOutputs, } from "../notebook-cell-outputs.js";
3
3
  import { buildAssetList } from "./reconcile-project-work.js";
4
4
  import { readProviderTaskStatus } from "../provider-task-status.js";
5
+ import { captureKeys, generationHarStatus } from "../generation-har.js";
6
+ import { readHarIndex } from "../har-index.js";
7
+ import { readHarSettings } from "../har-settings.js";
8
+ import { logger } from "../logger.js";
9
+ async function attemptHarLookup(engine) {
10
+ try {
11
+ const [{ mode }, rows] = await Promise.all([readHarSettings(), readHarIndex()]);
12
+ return { mode, status: async (ids) => generationHarStatus(await captureKeys(engine, ids), mode, rows) };
13
+ }
14
+ catch (error) {
15
+ logger.warn("har-capture-failed", { error });
16
+ return null;
17
+ }
18
+ }
5
19
  function generationArtifactIds(generations) {
6
20
  return [...new Set(generations.flatMap((row) => [
7
21
  ...(row.outputArtifactId ? [row.outputArtifactId] : []),
@@ -132,6 +146,7 @@ export function listCellGenerationsDef(engine) {
132
146
  ? generationsResult.value.filter((row) => !row.outputCellId || row.outputCellId === requestedResult.id)
133
147
  : generationsResult.value;
134
148
  const transitions = generationTransitions(engine, rows);
149
+ const harLookup = await attemptHarLookup(engine);
135
150
  const assets = await buildAssetList(engine, book_id);
136
151
  const assetById = new Map(assets.map((asset) => [asset.artifact_id, asset]));
137
152
  const attempts = [];
@@ -180,6 +195,9 @@ export function listCellGenerationsDef(engine) {
180
195
  ...(include_payloads ? { payload } : {}),
181
196
  commandPlan,
182
197
  transitions: transitions.get(row.generationId) ?? [],
198
+ har: harLookup
199
+ ? await harLookup.status([row.generationId, row.providerArtifactId, row.outputArtifactId])
200
+ : null,
183
201
  createdAt: row.createdAt,
184
202
  updatedAt: row.updatedAt,
185
203
  });
@@ -205,6 +223,7 @@ export function listCellGenerationsDef(engine) {
205
223
  latestStatus: rows[0]?.status ?? null,
206
224
  artifactIds: generationArtifactIds(rows),
207
225
  },
226
+ harMode: harLookup?.mode ?? null,
208
227
  };
209
228
  return {
210
229
  content: [{
@@ -7,7 +7,6 @@ import { legacyMediaForTaskType } from "../media/legacy-task-types.js";
7
7
  import { parseModelId } from "../media/model-id.js";
8
8
  import { computeArtifactStatus, QUEUED_TASK_ID } from "videobook-engine";
9
9
  import { z } from "zod";
10
- import { createHash } from "node:crypto";
11
10
  import { join } from "node:path";
12
11
  import { KieError } from "@apicity/kie";
13
12
  import { XaiError } from "@apicity/xai";
@@ -18,20 +17,19 @@ import { EMPTY_TAG_SUMMARY, readAssetTagSummaries, } from "../asset-tags.js";
18
17
  import { logger } from "../logger.js";
19
18
  import { queue } from "../queue.js";
20
19
  import { getApiKey } from "../api-key.js";
21
- import { findVideoFile, findAudioFile, findPrimaryMediaFile, RENDER_ORIENTATIONS as MEDIA_RENDER_ORIENTATIONS, } from "videobook-engine";
20
+ import { findVideoFile, findAudioFile, RENDER_ORIENTATIONS as MEDIA_RENDER_ORIENTATIONS, } from "videobook-engine";
22
21
  import { readAndAssessDialog } from "../dialog-quality.js";
23
22
  import { hasSpokenAudio, PLAIN_TRANSCRIPT_FILE, readAssetTranscriptSources, TIMED_TRANSCRIPT_FILE, } from "../transcript-projection.js";
24
23
  import { isAnalyzableVideo, readVideoAnalysisSummary, VIDEO_ANALYSIS_FILE, } from "../video-analysis-projection.js";
25
24
  import { failPendingTaskAndLog } from "../jobs/task-failure.js";
26
25
  import { findUnprocessedUploads, processExternalUpload } from "../jobs/process-upload.js";
27
26
  import { planVideoTranscriptJobs } from "../jobs/process-video.js";
28
- import { probeVideo } from "../ffmpeg.js";
29
27
  import { providerEndpoints } from "../media/endpoints.js";
30
28
  import { machgenProvider } from "../media/adapters/machgen.js";
31
29
  import { MEDIA_TASK_TYPES } from "../media/adapters/types.js";
32
30
  import { submitQueuedKieTask } from "../jobs/kie-paygate.js";
33
31
  import { DEFAULT_KEYWORD_GRAPHICS_LINGER_SECONDS, DEFAULT_KEYWORD_GRAPHICS_MAX_COUNT, } from "../remotion/keyword-graphics.js";
34
- import { invalidVideoProxyMetadata, probeStoredVideoFile, videoProxyAssessmentMatches, videoProxyMetadata, videoProxyMetadataMatches, videoProxyValidationError, } from "../video-proxy.js";
32
+ import { assetDuration, assetMediaUrls, assetStreamProfile, findAssetMediaFile, groupByArtifactId, indexByArtifactId, PARTIAL_DOWNLOAD_RE, scheduleMissingVideoPoster, servedVideoProxyFile, } from "../asset-media-projection.js";
35
33
  /** Upper bound on how long a pending task may sit without reaching success/fail
36
34
  * before reconciliation ages it out. Guards against providers leaving jobs in an
37
35
  * intermediate "waiting" state indefinitely. */
@@ -153,29 +151,6 @@ async function analysisOutputProjection(engine, artifactId) {
153
151
  return null;
154
152
  }
155
153
  }
156
- function findAssetMediaFile(files, asset) {
157
- return asset.kind === "audio"
158
- ? findAudioFile(files, asset.kind)
159
- : asset.kind === "video"
160
- ? findVideoFile(files.filter((file) => file.name !== "proxy.mp4"))
161
- : findPrimaryMediaFile(files, asset.kind);
162
- }
163
- async function assessStoredVideoProxy(engine, assetId, sourceFile, proxyFile, sourceProfile) {
164
- let assessment;
165
- try {
166
- const proxy = await probeStoredVideoFile(engine, assetId, proxyFile.name);
167
- const error = videoProxyValidationError(sourceProfile, proxy);
168
- assessment = error
169
- ? invalidVideoProxyMetadata(sourceFile, proxyFile, error)
170
- : videoProxyMetadata(sourceFile, proxyFile, proxy);
171
- }
172
- catch (cause) {
173
- const error = cause instanceof Error ? cause.message : String(cause);
174
- assessment = invalidVideoProxyMetadata(sourceFile, proxyFile, error);
175
- }
176
- await engine.metadata.artifacts.write(assetId, "proxy", assessment);
177
- return assessment.valid;
178
- }
179
154
  /** Read the OSG overlay summary from the asset's render metadata. OSG is stored
180
155
  * identically across orientations, so the first orientation that carries an
181
156
  * osgAssetId wins. Returns null when no overlay is configured. */
@@ -277,9 +252,7 @@ function scheduleWaveformExtraction(bookId, assetId, videoFilename) {
277
252
  logger.error("waveform-enqueue-failed", { assetId, error: err });
278
253
  });
279
254
  }
280
- const PARTIAL_DOWNLOAD_RE = /\.(mp4|mov)\.part$/i;
281
255
  /** Media extensions whose duration can be recovered via ffprobe when metadata lacks it. */
282
- const DURATION_PROBE_EXT_RE = /\.(mp4|mov|webm|mkv|mp3|wav|m4a|aac|ogg|flac)$/i;
283
256
  export const RENDER_ORIENTATIONS = MEDIA_RENDER_ORIENTATIONS;
284
257
  export async function readRenderSettings(engine, assetId, orientations) {
285
258
  const result = {};
@@ -377,30 +350,6 @@ async function readAnalysisRecommendations(engine, assetId) {
377
350
  return {};
378
351
  }
379
352
  }
380
- // Content fingerprint covering every file in the asset directory. Changes
381
- // whenever any file is added, removed, resized, or rewritten, so every URL
382
- // derived from an asset's filePath (including alternate-orientation renders
383
- // like portrait.mp4 / landscape.mp4) invalidates together. Cheap: no file
384
- // reads — just hashes the immutable manifest tuples. Partial downloads are
385
- // excluded: their size grows on every poll while no served URL points at
386
- // them, and including them made every media URL churn (and the browser
387
- // refetch) once per poll tick for the whole duration of a download.
388
- function computeAssetFingerprint(files) {
389
- const settled = files.filter((f) => !PARTIAL_DOWNLOAD_RE.test(f.name));
390
- if (settled.length === 0)
391
- return "";
392
- const sorted = [...settled].sort((a, b) => a.name.localeCompare(b.name));
393
- const hash = createHash("sha1");
394
- for (const f of sorted) {
395
- hash.update(f.name);
396
- hash.update("\0");
397
- hash.update(String(f.sizeBytes ?? 0));
398
- hash.update("\0");
399
- hash.update(f.objectHash);
400
- hash.update("\0");
401
- }
402
- return hash.digest("hex").slice(0, 12);
403
- }
404
353
  function renderedOrientationsFor(files, kind) {
405
354
  const fileSet = new Set(files.map((f) => f.name));
406
355
  if (kind === "final") {
@@ -408,23 +357,6 @@ function renderedOrientationsFor(files, kind) {
408
357
  }
409
358
  return RENDER_ORIENTATIONS.filter((o) => fileSet.has(`${o}.mp4`));
410
359
  }
411
- function indexByArtifactId(rows) {
412
- const map = new Map();
413
- for (const row of rows)
414
- map.set(row.artifactId, row);
415
- return map;
416
- }
417
- function groupByArtifactId(rows) {
418
- const map = new Map();
419
- for (const row of rows) {
420
- const list = map.get(row.artifactId);
421
- if (list)
422
- list.push(row);
423
- else
424
- map.set(row.artifactId, [row]);
425
- }
426
- return map;
427
- }
428
360
  export async function buildAssetList(engine, book_id, sort) {
429
361
  const assets = engine.artifacts.list({ ...(sort ? { sort } : {}) });
430
362
  const pendingAll = engine.jobs.pending.findAll();
@@ -468,30 +400,9 @@ export async function buildAssetList(engine, book_id, sort) {
468
400
  const mediaFile = findAssetMediaFile(files, a);
469
401
  const meta = metadata.ok ? metadata.value : null;
470
402
  const streams = streamsByArtifact.get(a.artifactId) ?? [];
471
- const videoStream = streams.find((stream) => stream.kind === "video" && stream.video);
472
- const audioStream = streams.find((stream) => stream.kind === "audio" && stream.audio);
473
- const frameRate = videoStream?.video?.averageFrameRate
474
- ?? videoStream?.video?.nominalFrameRate;
475
- const streamDuration = videoStream
476
- ? videoStream.durationTicks
477
- * videoStream.timeBase.numerator
478
- / videoStream.timeBase.denominator
479
- : 0;
480
- const metadataDuration = Number(meta?.duration);
481
- const sourceDuration = streamDuration > 0
482
- ? streamDuration
483
- : Number.isFinite(metadataDuration) ? metadataDuration : 0;
484
- const sourceProfile = sourceDuration > 0
485
- ? {
486
- videoDuration: sourceDuration,
487
- avgFps: frameRate
488
- ? frameRate.numerator / frameRate.denominator
489
- : null,
490
- hasAudio: audioStream !== undefined || meta?.hasAudio === true,
491
- }
492
- : null;
403
+ const profile = assetStreamProfile(streams, meta);
404
+ const { audioStream, frameRate } = profile;
493
405
  const fileNames = new Set(files.map((f) => f.name));
494
- const hasThumbnail = fileNames.has("original.jpg");
495
406
  const renders = renderedOrientationsFor(files, a.kind);
496
407
  const hasPartFile = files.some((f) => PARTIAL_DOWNLOAD_RE.test(f.name));
497
408
  const frameDirs = buildFrameDirs(directories, book_id, a.artifactId);
@@ -509,64 +420,22 @@ export async function buildAssetList(engine, book_id, sort) {
509
420
  : Promise.resolve(null),
510
421
  ]);
511
422
  const waveform = await readWaveformOrSchedule(engine, a.artifactId, book_id, mediaFile?.name ?? null);
512
- // Cache-bust media URLs so browsers refetch after trim / re-encode / re-render.
513
- // For video and image assets, use a content fingerprint covering the whole
514
- // asset directory: orientation re-renders (portrait/landscape/square.mp4)
515
- // touch alternate files but not original.mp4, so a primary-mtime cache
516
- // key would leave alternate-orientation URLs cached and stale.
517
- const useFingerprint = a.kind === "video" || a.kind === "image";
518
- let cacheBust = '';
519
- if (useFingerprint) {
520
- const fp = computeAssetFingerprint(files);
521
- if (fp)
522
- cacheBust = `?v=${fp}`;
523
- }
524
- else {
525
- const mediaHash = mediaFile?.objectHash;
526
- if (mediaHash)
527
- cacheBust = `?v=${mediaHash.slice(0, 12)}`;
528
- }
529
- // Character assets: free-form folders have no original.* media. Project
530
- // the first image in the folder so sidebar thumbs and drag-drop keep working.
531
- let charImage = null;
532
- if (a.kind === "character") {
533
- charImage = files.find((f) => /\.(jpg|jpeg|png|webp|gif)$/i.test(f.name)) ?? null;
534
- }
535
- const proxyCandidate = a.kind === "video"
536
- ? files.find((file) => file.name === "proxy.mp4")
537
- : null;
538
- let proxyFile = null;
539
- if (proxyCandidate && mediaFile) {
540
- if (proxyMetadata.ok
541
- && videoProxyMetadataMatches(proxyMetadata.value, mediaFile, proxyCandidate)) {
542
- proxyFile = proxyCandidate;
543
- }
544
- else {
545
- const assessmentMatches = proxyMetadata.ok
546
- && videoProxyAssessmentMatches(proxyMetadata.value, mediaFile, proxyCandidate);
547
- const active = assetWorkRow.ok
548
- && assetWorkRow.value !== null
549
- && (assetWorkRow.value.status === "pending" || assetWorkRow.value.status === "working");
550
- if (!assessmentMatches && !active && sourceProfile) {
551
- const valid = await assessStoredVideoProxy(engine, a.artifactId, mediaFile, proxyCandidate, sourceProfile);
552
- if (valid)
553
- proxyFile = proxyCandidate;
554
- }
555
- }
556
- }
557
- const media_url = charImage
558
- ? `/files/${book_id}/${a.artifactId}/${charImage.name}${cacheBust}`
559
- : proxyFile
560
- ? `/files/${book_id}/${a.artifactId}/${proxyFile.name}${cacheBust}`
561
- : mediaFile
562
- ? `/files/${book_id}/${a.artifactId}/${mediaFile.name}${cacheBust}`
563
- : null;
564
- const isImage = a.kind === "image";
565
- const thumbnail_url = charImage
566
- ? `/files/${book_id}/${a.artifactId}/${charImage.name}${cacheBust}`
567
- : hasThumbnail
568
- ? `/files/${book_id}/${a.artifactId}/original.jpg${cacheBust}`
569
- : isImage ? media_url : null;
423
+ const proxyFile = servedVideoProxyFile({
424
+ bookId: book_id,
425
+ artifact: a,
426
+ files,
427
+ mediaFile,
428
+ proxyMetadata,
429
+ work: assetWorkRow.value,
430
+ sourceProfile: profile.sourceProfile,
431
+ });
432
+ const { media_url, thumbnail_url } = assetMediaUrls({
433
+ bookId: book_id,
434
+ artifact: a,
435
+ files,
436
+ mediaFile,
437
+ proxyFile,
438
+ });
570
439
  // Status derivation lives in videobook-engine (`computeArtifactStatus`). We pass
571
440
  // the asset-type-aware primary media name we already computed so the
572
441
  // engine doesn't have to redo media-type heuristics.
@@ -587,20 +456,13 @@ export async function buildAssetList(engine, book_id, sort) {
587
456
  }
588
457
  : null,
589
458
  });
590
- if (a.kind === "video" && status === "ready" && !hasThumbnail && mediaFile) {
591
- const recovery = await engine.metadata.artifacts.read(a.artifactId, "poster-recovery");
592
- if (!recovery.ok || !recovery.value.failed || recovery.value.sourceHash !== mediaFile.objectHash) {
593
- void queue.enqueue(`extract-video-poster-${book_id}-${a.artifactId}-${mediaFile.objectHash}`, {
594
- type: "extract_video_poster",
595
- bookId: book_id,
596
- posterArtifactId: a.artifactId,
597
- sourceFilename: mediaFile.name,
598
- sourceHash: mediaFile.objectHash,
599
- }).catch((error) => {
600
- logger.error("poster-enqueue-failed", { assetId: a.artifactId, error });
601
- });
602
- }
603
- }
459
+ await scheduleMissingVideoPoster(engine, {
460
+ bookId: book_id,
461
+ artifact: a,
462
+ files,
463
+ mediaFile,
464
+ status,
465
+ });
604
466
  const archiveMeta = archiveMetadata.ok ? archiveMetadata.value : null;
605
467
  const analysisInfo = analysisInfoFromMetadata(meta);
606
468
  if (analysisInfo && fileNames.has("analysis.json")) {
@@ -612,22 +474,13 @@ export async function buildAssetList(engine, book_id, sort) {
612
474
  analysisInfo.llm_exchanges = output.llmExchanges;
613
475
  }
614
476
  }
615
- // Backfill duration if missing from metadata (video + audio; probeVideo
616
- // reads format.duration via ffprobe and works for both).
617
- let duration = meta?.duration ?? null;
618
- if (duration === null && mediaFile && DURATION_PROBE_EXT_RE.test(mediaFile.name)) {
619
- try {
620
- const probe = await probeVideo(join(a.path, mediaFile.name));
621
- if (Number.isFinite(probe.duration) && probe.duration > 0) {
622
- duration = probe.duration;
623
- // Persist so we don't re-probe next time
624
- if (metadata.ok) {
625
- await engine.metadata.artifacts.write(a.artifactId, "original", { ...metadata.value, duration });
626
- }
627
- }
628
- }
629
- catch { /* non-fatal */ }
630
- }
477
+ const duration = assetDuration({
478
+ bookId: book_id,
479
+ artifactId: a.artifactId,
480
+ meta,
481
+ mediaFile,
482
+ profile,
483
+ });
631
484
  const audio_info = audioStream?.audio
632
485
  ? {
633
486
  codec: audioStream.codec,
@@ -791,12 +644,18 @@ async function processMachGenTaskStatus(engine, task, status, result, bookId) {
791
644
  });
792
645
  result.completed.push({ assetId: task.artifactId, taskType: task.taskType, jobId });
793
646
  }
647
+ function withProviderTaskContext(engine, bookId, task, operation) {
648
+ return withBookContext({ bookId, engine, operation: "poll_pending_provider_tasks", artifactId: task.artifactId,
649
+ providerTaskId: task.taskId, attemptId: typeof task.meta.attemptId === "string" ? task.meta.attemptId : task.artifactId,
650
+ runId: typeof task.meta.runId === "string" ? task.meta.runId : undefined,
651
+ model: typeof task.meta.model === "string" ? task.meta.model : undefined }, operation);
652
+ }
794
653
  async function pollMachGenTasks(engine, tasks, result, bookId, baseURL) {
795
654
  const machgenTasks = tasks.filter((task) => task.taskType === MEDIA_TASK_TYPES.machgen);
796
655
  if (!getApiKey("MACHGEN_API_KEY") || machgenTasks.length === 0)
797
656
  return;
798
657
  const provider = machgenProvider(30_000, baseURL);
799
- await Promise.all(machgenTasks.map(async (task) => {
658
+ await Promise.all(machgenTasks.map(async (task) => withProviderTaskContext(engine, bookId, task, async () => {
800
659
  try {
801
660
  await processMachGenTaskStatus(engine, task, await provider.api.v0.tasks(task.taskId), result, bookId);
802
661
  }
@@ -807,7 +666,7 @@ async function pollMachGenTasks(engine, tasks, result, bookId, baseURL) {
807
666
  if (machgenPollFailureIsFinal(error))
808
667
  await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
809
668
  }
810
- }));
669
+ })));
811
670
  }
812
671
  /**
813
672
  * Poll provider-pending tasks: submit queued KIE rows, age out stale tasks,
@@ -887,7 +746,7 @@ export async function pollPendingProviderTasks(engine, book_id, options = {}) {
887
746
  timeout: 30_000,
888
747
  ...(kieBaseURL ? { baseURL: kieBaseURL } : {}),
889
748
  });
890
- await Promise.all(kieTasks.map(async (task) => {
749
+ await Promise.all(kieTasks.map(async (task) => withProviderTaskContext(engine, book_id, task, async () => {
891
750
  try {
892
751
  const taskInfo = await provider.get.api.v1.jobs.recordInfo(task.taskId);
893
752
  await processKieTaskResult(engine, task, taskInfo, result, book_id);
@@ -904,7 +763,7 @@ export async function pollPendingProviderTasks(engine, book_id, options = {}) {
904
763
  }
905
764
  }
906
765
  }
907
- }));
766
+ })));
908
767
  }
909
768
  // Poll KIE Veo tasks (separate namespace: kie.veo.get.api.v1.veo.recordInfo)
910
769
  const veoTasks = pollableTasks.filter(isVeoPendingTask);
@@ -914,7 +773,7 @@ export async function pollPendingProviderTasks(engine, book_id, options = {}) {
914
773
  timeout: 30_000,
915
774
  ...(kieBaseURL ? { baseURL: kieBaseURL } : {}),
916
775
  });
917
- await Promise.all(veoTasks.map(async (task) => {
776
+ await Promise.all(veoTasks.map(async (task) => withProviderTaskContext(engine, book_id, task, async () => {
918
777
  const prompt = metaPrompt(task.meta);
919
778
  try {
920
779
  const recordInfo = await provider.veo.get.api.v1.veo.recordInfo(task.taskId);
@@ -973,13 +832,13 @@ export async function pollPendingProviderTasks(engine, book_id, options = {}) {
973
832
  }
974
833
  }
975
834
  }
976
- }));
835
+ })));
977
836
  }
978
837
  // Poll xAI tasks (only if API key is set)
979
838
  const xaiTasks = pollableTasks.filter((t) => t.taskType.startsWith("xai_"));
980
839
  if (process.env.XAI_API_KEY && xaiTasks.length > 0) {
981
840
  const provider = createXai({ apiKey: process.env.XAI_API_KEY, timeout: 30_000 });
982
- await Promise.all(xaiTasks.map(async (task) => {
841
+ await Promise.all(xaiTasks.map(async (task) => withProviderTaskContext(engine, book_id, task, async () => {
983
842
  const prompt = metaPrompt(task.meta);
984
843
  try {
985
844
  const videoResult = await provider.get.v1.videos(task.taskId);
@@ -1029,13 +888,13 @@ export async function pollPendingProviderTasks(engine, book_id, options = {}) {
1029
888
  }
1030
889
  }
1031
890
  }
1032
- }));
891
+ })));
1033
892
  }
1034
893
  // Poll Alibaba tasks (only if API key is set)
1035
894
  const alibabaTasks = pollableTasks.filter((t) => t.taskType.startsWith("alibaba_"));
1036
895
  if (process.env.DASHSCOPE_API_KEY && alibabaTasks.length > 0) {
1037
896
  const provider = createAlibaba({ apiKey: process.env.DASHSCOPE_API_KEY, timeout: 30_000 });
1038
- await Promise.all(alibabaTasks.map(async (task) => {
897
+ await Promise.all(alibabaTasks.map(async (task) => withProviderTaskContext(engine, book_id, task, async () => {
1039
898
  const prompt = metaPrompt(task.meta);
1040
899
  try {
1041
900
  const taskStatus = await provider.get.api.v1.tasks(task.taskId);
@@ -1105,7 +964,7 @@ export async function pollPendingProviderTasks(engine, book_id, options = {}) {
1105
964
  }
1106
965
  }
1107
966
  }
1108
- }));
967
+ })));
1109
968
  }
1110
969
  // Poll FAL tasks (only if API key is set)
1111
970
  const falTasks = pollableTasks.filter((t) => t.taskType.startsWith("fal_"));
@@ -1116,7 +975,7 @@ export async function pollPendingProviderTasks(engine, book_id, options = {}) {
1116
975
  // — e.g. `fal-ai/elevenlabs/speech-to-text/scribe-v2` — 405s. Strip back
1117
976
  // to `fal-ai/elevenlabs` for polling.
1118
977
  const falNamespace = (endpointId) => endpointId.split("/").slice(0, 2).join("/");
1119
- await Promise.all(falTasks.map(async (task) => {
978
+ await Promise.all(falTasks.map(async (task) => withProviderTaskContext(engine, book_id, task, async () => {
1120
979
  const prompt = metaPrompt(task.meta);
1121
980
  const endpointId = typeof task.meta?.falEndpointId === "string" ? task.meta.falEndpointId : null;
1122
981
  if (!endpointId) {
@@ -1179,7 +1038,7 @@ export async function pollPendingProviderTasks(engine, book_id, options = {}) {
1179
1038
  }
1180
1039
  }
1181
1040
  }
1182
- }));
1041
+ })));
1183
1042
  }
1184
1043
  await pollMachGenTasks(engine, pollableTasks, result, book_id, options.machgenBaseURL ?? providerEndpoints().machgenBaseURL);
1185
1044
  for (const task of expiredTasks) {
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ import { harSettingsSchema, readHarSettings, writeHarSettings, harDirectory } from "../har-settings.js";
3
+ import { appState } from "../app-state.js";
4
+ export function setHarCaptureDef(store = appState) {
5
+ return {
6
+ name: "set_har_capture",
7
+ description: "Read or change opt-in local provider HAR diagnostics. Captures retain prompts, media URLs, parameters, responses and errors; secrets, cookies and oversized media bytes are scrubbed. Omit mode to read settings. Environment override wins.",
8
+ schema: {
9
+ mode: harSettingsSchema.shape.mode.removeDefault().optional(),
10
+ maxAgeDays: harSettingsSchema.shape.maxAgeDays.removeDefault().optional(),
11
+ maxTotalMB: harSettingsSchema.shape.maxTotalMB.removeDefault().optional(),
12
+ },
13
+ handler: async (args) => {
14
+ try {
15
+ const changes = z.object({
16
+ mode: harSettingsSchema.shape.mode.removeDefault().optional(),
17
+ maxAgeDays: harSettingsSchema.shape.maxAgeDays.removeDefault().optional(),
18
+ maxTotalMB: harSettingsSchema.shape.maxTotalMB.removeDefault().optional(),
19
+ }).parse(args);
20
+ const settings = Object.values(changes).some((value) => value !== undefined)
21
+ ? await writeHarSettings({ ...await readHarSettings(store, {}), ...changes }, store)
22
+ : await readHarSettings(store);
23
+ return { content: [{ type: "text", text: JSON.stringify({ ...settings, dir: harDirectory(), overridden: Boolean(process.env.VIDEOBOOK_HAR_CAPTURE?.trim()) }) }] };
24
+ }
25
+ catch (error) {
26
+ return { isError: true, content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }] };
27
+ }
28
+ },
29
+ };
30
+ }
@@ -17,6 +17,7 @@ import { listNotebookActions, markDescendantsStale, normalizeCellActionFields, n
17
17
  import { hydrateNotebookProviderFailures } from "../notebook-provider-failure.js";
18
18
  import { buildSevenStageScaffold } from "../notebook-workflow.js";
19
19
  import { previewGeneration } from "../media/preview.js";
20
+ import { readNotebookStickySettings, rememberNotebookStickySettings, seedNotebookStickySettings, } from "../notebook-sticky-settings.js";
20
21
  import { generationInputMentions, remapGenerationInputPrompt } from "../generation-input-mentions.js";
21
22
  import { applyPreviewPlaceholderUrls } from "../preview-payload.js";
22
23
  import { jobProviderTaskStatus } from "../provider-task-status.js";
@@ -559,6 +560,7 @@ function notebookPayload(engine, notebook) {
559
560
  cells: normalized.cells.map(cellPayload),
560
561
  posts: notebookPosts(engine, normalized),
561
562
  edges: normalized.edges,
563
+ generation_settings_memory: readNotebookStickySettings(engine, normalized.id),
562
564
  };
563
565
  }
564
566
  const entityKindSchema = z.enum(["prompt", "scene", "character"]);
@@ -1158,6 +1160,7 @@ export function videobookDomainDefs(engine, books) {
1158
1160
  if (args.cell.type === "goal")
1159
1161
  return failure(GOAL_TILE_CREATE_ERROR);
1160
1162
  let cell = engine.notebooks.createCell(cellFromInput(args.cell));
1163
+ let requestedCell = cell;
1161
1164
  let outputCell = null;
1162
1165
  let outputEdge = null;
1163
1166
  try {
@@ -1167,6 +1170,8 @@ export function videobookDomainDefs(engine, books) {
1167
1170
  throw new Error(`Stale notebook revision: expected ${args.expected_revision}`);
1168
1171
  }
1169
1172
  cell = normalizeCellActionFields(cell, notebook);
1173
+ requestedCell = cell;
1174
+ cell = seedNotebookStickySettings(cell, readNotebookStickySettings(engine, String(args.notebook_id)));
1170
1175
  const withCell = seedInitialGenerationPrompt({
1171
1176
  ...notebook,
1172
1177
  cells: [...notebook.cells, cell],
@@ -1209,6 +1214,8 @@ export function videobookDomainDefs(engine, books) {
1209
1214
  ],
1210
1215
  };
1211
1216
  }, { cellId: cell.id });
1217
+ if (result.ok)
1218
+ rememberNotebookStickySettings(engine, String(args.notebook_id), requestedCell);
1212
1219
  return result.ok
1213
1220
  ? json({
1214
1221
  ...cellPayload(cell),
@@ -1239,6 +1246,7 @@ export function videobookDomainDefs(engine, books) {
1239
1246
  return failure(`Stale notebook revision: expected ${args.expected_revision}`);
1240
1247
  }
1241
1248
  const patch = args.cell;
1249
+ const edited = {};
1242
1250
  try {
1243
1251
  const result = await mutateNotebook(engine, String(args.notebook_id), "update_notebook_cell", (notebook) => {
1244
1252
  if (typeof args.expected_revision === "string"
@@ -1298,10 +1306,15 @@ export function videobookDomainDefs(engine, books) {
1298
1306
  ? {}
1299
1307
  : { pinnedResults: patch.pinned_results.map(pinnedResultFromInput) }),
1300
1308
  }, document);
1301
- const next = reconcileInitialGenerationPrompt(cell, normalized);
1309
+ const reconciled = reconcileInitialGenerationPrompt(cell, normalized);
1310
+ const next = reconciled.model !== cell.model
1311
+ ? seedNotebookStickySettings(reconciled, readNotebookStickySettings(engine, String(args.notebook_id)))
1312
+ : reconciled;
1302
1313
  const validation = validateCellConfiguration(next, document);
1303
1314
  if (!validation.ok)
1304
1315
  throw new Error(validation.message);
1316
+ edited.previous = cell;
1317
+ edited.next = reconciled;
1305
1318
  return next;
1306
1319
  });
1307
1320
  if (!found)
@@ -1312,6 +1325,8 @@ export function videobookDomainDefs(engine, books) {
1312
1325
  });
1313
1326
  return markDescendantsStale(updated, String(args.cell_id));
1314
1327
  }, { cellId: args.cell_id });
1328
+ if (result.ok && edited.next)
1329
+ rememberNotebookStickySettings(engine, String(args.notebook_id), edited.next, edited.previous);
1315
1330
  return result.ok
1316
1331
  ? json({ cell_id: args.cell_id, revision: result.value.hash })
1317
1332
  : failure(result.error.message);
package/dist/upload.js CHANGED
@@ -1,4 +1,5 @@
1
- import { createFreeMediaUpload, uploadToAnyHost } from "@apicity/free-media-upload";
1
+ import { createFreeMediaUpload } from "./usage-providers.js";
2
+ import { uploadToAnyHost } from "@apicity/free-media-upload";
2
3
  import { createHash } from "node:crypto";
3
4
  import { maybeReencodeImage, maybeUpscaleSmallImage } from "./image-compress.js";
4
5
  export function contentHash(data) {