videobook 0.3.0 → 0.3.2

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 (61) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/asset-media-projection.js +161 -0
  3. package/dist/command-reference.js +3 -1
  4. package/dist/diagnostics.js +10 -4
  5. package/dist/generation-reference-mentions.js +19 -0
  6. package/dist/goal-plan-v3.js +21 -6
  7. package/dist/goal-plan.js +1 -1
  8. package/dist/goal-store.js +2 -2
  9. package/dist/har-capture.js +36 -3
  10. package/dist/jobs/_image-edit-adapter.js +2 -0
  11. package/dist/jobs/_kie-upload.js +3 -1
  12. package/dist/jobs/assess-media-probes.js +94 -0
  13. package/dist/jobs/complete-provider-task.js +2 -5
  14. package/dist/jobs/generate-media.js +22 -12
  15. package/dist/jobs/image-subjects.js +2 -0
  16. package/dist/jobs/kie-paygate.js +3 -3
  17. package/dist/jobs/register-all.js +2 -0
  18. package/dist/jobs/run-notebook.js +16 -1
  19. package/dist/library-asset-projection.js +141 -0
  20. package/dist/lightning-defaults.js +4 -4
  21. package/dist/media/catalog.js +12 -6
  22. package/dist/media/descriptor.js +16 -7
  23. package/dist/media/frontier-gaps.js +0 -1
  24. package/dist/media/har-endpoint.js +25 -0
  25. package/dist/media/json-schema.js +3 -0
  26. package/dist/media/listing.js +26 -4
  27. package/dist/media/preferred-defaults.js +161 -0
  28. package/dist/media/prompt-dialects.js +9 -1
  29. package/dist/media-library.js +158 -153
  30. package/dist/notebook-sticky-settings.js +60 -0
  31. package/dist/provider-usage.js +24 -9
  32. package/dist/tools/estimate-cost.js +9 -4
  33. package/dist/tools/reconcile-project-work.js +37 -184
  34. package/dist/tools/videobook-domain.js +16 -1
  35. package/dist/usage-providers.js +3 -3
  36. package/package.json +15 -15
  37. package/ui/assets/{alive-captions-Cj8C1tCN.js → alive-captions-BWk_4aX0.js} +1 -1
  38. package/ui/assets/confirm-bodies-CNW9vLsi.js +1 -0
  39. package/ui/assets/{fullscreen-DyEncSdm.js → fullscreen-ND_H86Ih.js} +1 -1
  40. package/ui/assets/{goal-ui-BdFeoSWh.js → goal-ui-CCTD4lxi.js} +1 -1
  41. package/ui/assets/index-CTPstLYq.css +1 -0
  42. package/ui/assets/index-D0ftggO4.js +37 -0
  43. package/ui/assets/inspector-BZnbKsEZ.js +1 -0
  44. package/ui/assets/{jobs-ui-BudI4w0r.js → jobs-ui-BNK0IPSl.js} +1 -1
  45. package/ui/assets/{music-video-CT4C55ok.js → music-video-CoOI0cgW.js} +1 -1
  46. package/ui/assets/{ui-mq0uIS5P.css → ui-BBM3YCY6.css} +1 -1
  47. package/ui/assets/ui-koLWlu0t.js +2 -0
  48. package/ui/assets/vendor-Ca2-b3hB.js +19 -0
  49. package/ui/assets/{vendor-flow-nqTFQmTh.js → vendor-flow-C2NYBYCL.js} +1 -1
  50. package/ui/assets/vendor-icons-D3KrGMJY.js +1 -0
  51. package/ui/assets/{vendor-mcp-D_C3EZPv.js → vendor-mcp-9gUvOX4H.js} +2 -2
  52. package/ui/assets/videobook-B8dYNQrG.js +21 -0
  53. package/ui/index.html +14 -14
  54. package/ui/assets/confirm-bodies-BxWXwDf0.js +0 -1
  55. package/ui/assets/index-DWfR79lA.css +0 -1
  56. package/ui/assets/index-QZ8lbemN.js +0 -37
  57. package/ui/assets/inspector-BC6xWkJU.js +0 -1
  58. package/ui/assets/ui-z5r-p0K9.js +0 -2
  59. package/ui/assets/vendor-D8XcMav4.js +0 -19
  60. package/ui/assets/vendor-icons-CGqhCIXV.js +0 -1
  61. package/ui/assets/videobook-CsW453sb.js +0 -21
@@ -0,0 +1,94 @@
1
+ import { join } from "node:path";
2
+ import { probeVideo } from "../ffmpeg.js";
3
+ import { logger } from "../logger.js";
4
+ import { queue, registerJobHandler } from "../queue.js";
5
+ import { invalidVideoProxyMetadata, probeStoredVideoFile, videoProxyAssessmentMatches, videoProxyMetadata, videoProxyValidationError, } from "../video-proxy.js";
6
+ const PROXY_FILENAME = "proxy.mp4";
7
+ export function scheduleVideoProxyAssessment(input) {
8
+ const jobId = `assess-video-proxy-${input.bookId}-${input.artifactId}-${input.proxyFile.objectHash}`;
9
+ void queue.enqueue(jobId, {
10
+ type: "assess_video_proxy",
11
+ bookId: input.bookId,
12
+ proxyAssetId: input.artifactId,
13
+ sourceFilename: input.sourceFile.name,
14
+ sourceHash: input.sourceFile.objectHash,
15
+ proxyHash: input.proxyFile.objectHash,
16
+ sourceProfile: input.sourceProfile,
17
+ }).catch((error) => {
18
+ logger.error("proxy-assessment-enqueue-failed", { assetId: input.artifactId, error });
19
+ });
20
+ }
21
+ export function scheduleMediaDurationBackfill(input) {
22
+ const jobId = `backfill-media-duration-${input.bookId}-${input.artifactId}-${input.mediaFile.objectHash}`;
23
+ void queue.enqueue(jobId, {
24
+ type: "backfill_media_duration",
25
+ bookId: input.bookId,
26
+ durationAssetId: input.artifactId,
27
+ mediaFilename: input.mediaFile.name,
28
+ mediaHash: input.mediaFile.objectHash,
29
+ }).catch((error) => {
30
+ logger.error("duration-backfill-enqueue-failed", { assetId: input.artifactId, error });
31
+ });
32
+ }
33
+ async function currentFile(engine, artifactId, name, objectHash) {
34
+ const manifest = await engine.files.manifest(artifactId);
35
+ if (!manifest.ok)
36
+ return null;
37
+ return manifest.value.files.find((file) => file.name === name && file.objectHash === objectHash) ?? null;
38
+ }
39
+ export async function assessStoredVideoProxy(engine, assetId, sourceFile, proxyFile, sourceProfile) {
40
+ let assessment;
41
+ try {
42
+ const proxy = await probeStoredVideoFile(engine, assetId, proxyFile.name);
43
+ const error = videoProxyValidationError(sourceProfile, proxy);
44
+ assessment = error
45
+ ? invalidVideoProxyMetadata(sourceFile, proxyFile, error)
46
+ : videoProxyMetadata(sourceFile, proxyFile, proxy);
47
+ }
48
+ catch (cause) {
49
+ const error = cause instanceof Error ? cause.message : String(cause);
50
+ assessment = invalidVideoProxyMetadata(sourceFile, proxyFile, error);
51
+ }
52
+ await engine.metadata.artifacts.write(assetId, "proxy", assessment);
53
+ return assessment.valid;
54
+ }
55
+ async function runVideoProxyAssessment(engine, payload) {
56
+ const { proxyAssetId } = payload;
57
+ const sourceFile = await currentFile(engine, proxyAssetId, payload.sourceFilename, payload.sourceHash);
58
+ const proxyFile = await currentFile(engine, proxyAssetId, PROXY_FILENAME, payload.proxyHash);
59
+ if (!sourceFile || !proxyFile)
60
+ return { skipped: true };
61
+ const existing = await engine.metadata.artifacts.read(proxyAssetId, "proxy");
62
+ if (existing.ok && videoProxyAssessmentMatches(existing.value, sourceFile, proxyFile)) {
63
+ return { skipped: true };
64
+ }
65
+ return {
66
+ valid: await assessStoredVideoProxy(engine, proxyAssetId, sourceFile, proxyFile, payload.sourceProfile),
67
+ };
68
+ }
69
+ async function runMediaDurationBackfill(engine, payload, signal) {
70
+ const { durationAssetId } = payload;
71
+ const metadata = await engine.metadata.artifacts.read(durationAssetId, "original");
72
+ if (!metadata.ok || typeof metadata.value.duration === "number")
73
+ return { skipped: true };
74
+ if (!await currentFile(engine, durationAssetId, payload.mediaFilename, payload.mediaHash)) {
75
+ return { skipped: true };
76
+ }
77
+ const dir = await engine.workspaces.resolveArtifact(durationAssetId);
78
+ if (!dir.ok)
79
+ throw new Error(`backfill_media_duration: ${dir.error.message}`);
80
+ const probe = await probeVideo(join(dir.value, payload.mediaFilename), { signal });
81
+ if (!Number.isFinite(probe.duration) || probe.duration <= 0)
82
+ return { skipped: true };
83
+ const written = await engine.metadata.artifacts.write(durationAssetId, "original", {
84
+ ...metadata.value,
85
+ duration: probe.duration,
86
+ });
87
+ if (!written.ok)
88
+ throw new Error(written.error.message);
89
+ return { duration: probe.duration };
90
+ }
91
+ export function registerAssessMediaProbeJobs(engine) {
92
+ registerJobHandler("assess_video_proxy", async (payload) => runVideoProxyAssessment(engine, payload));
93
+ registerJobHandler("backfill_media_duration", async (payload, { signal }) => runMediaDurationBackfill(engine, payload, signal));
94
+ }
@@ -1,4 +1,3 @@
1
- import { withHarCapture } from "../har-capture.js";
2
1
  import { join } from "node:path";
3
2
  import { tmpdir } from "node:os";
4
3
  import { writeFile, unlink, readFile as readFsFile } from "node:fs/promises";
@@ -48,9 +47,7 @@ async function reclaimFailedCompletion(engine, task, ctx) {
48
47
  return lease.ownerId;
49
48
  }
50
49
  export function registerCompleteProviderTaskJob(engine) {
51
- registerJobHandler("complete_kie_task", async (payload, ctx) => withHarCapture({
52
- provider: String(payload.taskType).split("_")[0] ?? "unknown", endpoint: "generation.complete", input: payload,
53
- }, async () => {
50
+ registerJobHandler("complete_kie_task", async (payload, ctx) => {
54
51
  const { taskId, taskType, assetId, bookId, meta, resultUrls, resultObject, } = payload;
55
52
  // Pattern C: inherit the provider lease via getOwner, abort if the row
56
53
  // already terminated, and register the lease so the wrapper's catch path
@@ -128,7 +125,7 @@ export function registerCompleteProviderTaskJob(engine) {
128
125
  clearInterval(heartbeat);
129
126
  await engine.jobs.locks.release(assetDir);
130
127
  }
131
- }, globalThis.fetch, { providerTaskId: payload.taskId, model: payload.meta.model, runId: payload.meta.runId, attemptId: payload.meta.attemptId }));
128
+ });
132
129
  }
133
130
  async function mediaOutputs(taskType, taskId, urls) {
134
131
  if (taskType === MEDIA_TASK_TYPES.machgen)
@@ -1,4 +1,5 @@
1
- import { withHarCapture } from "../har-capture.js";
1
+ import { withHarLocalFailure } from "../har-capture.js";
2
+ import { generationHarEndpoint } from "../media/har-endpoint.js";
2
3
  import { withBookContext } from "../book-context.js";
3
4
  import { isSeedanceEdit } from "../media/seedance-edit.js";
4
5
  import { registerJobHandler } from "../queue.js";
@@ -21,24 +22,33 @@ async function replaceAudioMeta(engine, ref) {
21
22
  const input = await resolveRefInput(engine, "audio", ref);
22
23
  return { replaceAudioWith: { assetId: input.assetId, filename: input.filename } };
23
24
  }
25
+ async function sourceAudioSeconds(engine, assetIds) {
26
+ for (const assetId of assetIds) {
27
+ const metadata = await engine.metadata.artifacts.read(assetId, "original");
28
+ const seconds = metadata.ok ? metadata.value.duration : undefined;
29
+ if (typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0)
30
+ return seconds;
31
+ }
32
+ return undefined;
33
+ }
24
34
  export async function generationSourceCostHints(engine, refs, job) {
25
35
  const draftHints = await seedanceDraftCostHints(engine, job.model, job.options ?? {});
26
36
  if (Object.keys(draftHints).length > 0)
27
37
  return draftHints;
38
+ const model = resolveModel(job.model);
39
+ if (model.entry.outputLength === "audio") {
40
+ const seconds = await sourceAudioSeconds(engine, refs.sourceAudioIds);
41
+ return seconds === undefined ? {} : { durationSeconds: seconds };
42
+ }
28
43
  const videoSeconds = await referenceVideoDuration(engine, refs.sourceVideoIds);
29
44
  if (videoSeconds !== undefined) {
30
- return { inputDurationSeconds: videoSeconds, ...(isSeedanceEdit(job.model, job.mode) ? { durationSeconds: videoSeconds } : {}) };
45
+ const followsVideo = model.entry.outputLength === "video" || isSeedanceEdit(job.model, job.mode);
46
+ return { inputDurationSeconds: videoSeconds, ...(followsVideo ? { durationSeconds: videoSeconds } : {}) };
31
47
  }
32
- if (resolveModel(job.model).provider !== "fal")
48
+ if (model.provider !== "fal")
33
49
  return {};
34
- for (const assetId of refs.sourceAudioIds) {
35
- const metadata = await engine.metadata.artifacts.read(assetId, "original");
36
- const seconds = metadata.ok ? metadata.value.duration : undefined;
37
- if (typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0) {
38
- return { inputDurationSeconds: seconds };
39
- }
40
- }
41
- return {};
50
+ const seconds = await sourceAudioSeconds(engine, refs.sourceAudioIds);
51
+ return seconds === undefined ? {} : { inputDurationSeconds: seconds };
42
52
  }
43
53
  function generationMeta(job, model, refs) {
44
54
  return {
@@ -87,7 +97,7 @@ async function generateMedia(engine, job, ctx) {
87
97
  }
88
98
  export async function runGenerateMedia(engine, job, ctx) {
89
99
  return withBookContext({ bookId: job.bookId, engine, operation: "generate_media", artifactId: job.assetId,
90
- attemptId: job.generationId ?? job.assetId, runId: job.runId, model: job.model }, () => withHarCapture({ provider: resolveModel(job.model).provider, endpoint: "generation.submit", input: job }, () => generateMedia(engine, job, ctx)));
100
+ attemptId: job.generationId ?? job.assetId, runId: job.runId, model: job.model }, () => withHarLocalFailure({ provider: resolveModel(job.model).provider, endpoint: generationHarEndpoint(resolveModel(job.model)), input: job }, () => generateMedia(engine, job, ctx)));
91
101
  }
92
102
  export function registerGenerateMediaJob(engine) {
93
103
  registerJobHandler("generate_media", (payload, ctx) => runGenerateMedia(engine, payload, ctx));
@@ -4,6 +4,7 @@ import sharp from "sharp";
4
4
  import { KIE_RESULT_TIMEOUT_MS, kieProvider, parseResultUrls, waitForKieResult, } from "./_kie-poll.js";
5
5
  import { GROK_EXACT_MASK_ADAPTER_ID, imageEditAdapter, IMAGE_EDIT_ADAPTERS, } from "./_image-edit-adapter.js";
6
6
  import { DEFAULT_IMAGE_EDIT_MODEL_ID } from "../image-edit-models.js";
7
+ import { requireApiKey } from "../api-key.js";
7
8
  import { compositeImageRegionEdit, imageRegionEditPrompt } from "../image-region-edit.js";
8
9
  import { IMAGE_SUBJECT_MAP_VERSION, applyGrokSegmentsToSubjects, composeImageSubjectRemoval, exactEditMaskIndexes, grokMaskIndex, parseGrokSegmentMapResult, persistSeedreamSubjectLayers, readImageSubjectMapRecord, resolveImageSubjectSource, writeCurrentImageSubjectAnalysisRecord, } from "../image-subject-map.js";
9
10
  import { enqueueSearchIndex } from "./enqueue-search-index.js";
@@ -534,6 +535,7 @@ export async function previewImageSubjectEditRequest(engine, payload, imageUrls)
534
535
  : adapter.buildSemanticRequest(input);
535
536
  }
536
537
  async function runAdapterImageSubjectEdit(engine, payload, adapter, route, prepared, sourceFilename, signal) {
538
+ requireApiKey(adapter.envKey);
537
539
  const stored = await readStoredImageSubjectEditTask(engine, payload.imageAssetId);
538
540
  const storedMatch = stored?.adapterId === adapter.id ? stored : null;
539
541
  let request = storedMatch?.request ?? null;
@@ -1,4 +1,4 @@
1
- import { withHarCapture } from "../har-capture.js";
1
+ import { withHarLocalFailure } from "../har-capture.js";
2
2
  import { createKie } from "../usage-providers.js";
3
3
  import { submitMediaJob } from "../usage-providers.js";
4
4
  import { mintOtp } from "@apicity/cost";
@@ -177,8 +177,8 @@ async function submitQueuedKieTaskInner(engine, task) {
177
177
  return writeResult.value.task;
178
178
  }
179
179
  export async function submitKieTask(engine, ctx, args) {
180
- return withHarCapture({ provider: "kie", endpoint: "generation.paygate", input: args.request }, () => submitKieTaskInner(engine, ctx, args));
180
+ return withHarLocalFailure({ provider: "kie", endpoint: "api.v1.jobs.createTask", input: args.request }, () => submitKieTaskInner(engine, ctx, args));
181
181
  }
182
182
  export async function submitQueuedKieTask(engine, task) {
183
- return withHarCapture({ provider: "kie", endpoint: "generation.queued", input: task }, () => submitQueuedKieTaskInner(engine, task));
183
+ return withHarLocalFailure({ provider: "kie", endpoint: "api.v1.jobs.createTask", input: task }, () => submitQueuedKieTaskInner(engine, task));
184
184
  }
@@ -38,6 +38,7 @@ import { registerLabelMusicSectionsJob } from "./label-music-sections.js";
38
38
  import { registerEditingMomentsJob } from "./analyze-editing-moments.js";
39
39
  import { registerExtractWaveformJob } from "./extract-waveform.js";
40
40
  import { registerExtractVideoPosterJob } from "./extract-video-poster.js";
41
+ import { registerAssessMediaProbeJobs } from "./assess-media-probes.js";
41
42
  import { registerReplaceVideoAudioJob } from "./replace-video-audio.js";
42
43
  import { registerSpliceVideoJob } from "./splice-video.js";
43
44
  import { registerSmoothSceneConcatJob } from "./smooth-scene-concat.js";
@@ -109,6 +110,7 @@ export function registerAllJobHandlers(engine, books = bookManager) {
109
110
  registerExtractAudioJob(engine);
110
111
  registerExtractWaveformJob(engine);
111
112
  registerExtractVideoPosterJob(engine);
113
+ registerAssessMediaProbeJobs(engine);
112
114
  registerAnalyzeMusicJob(engine);
113
115
  registerPlanGoalJob(engine);
114
116
  registerAssembleGoalJob(engine);
@@ -2609,6 +2609,21 @@ async function finishAwaitingNotebookCell(engine, input) {
2609
2609
  throw new Error(refreshed.error.message);
2610
2610
  return { outputArtifactId, document: refreshed.value };
2611
2611
  }
2612
+ function withFailedGenerationPlan(document, cellId, runId, error, updatedAt) {
2613
+ return {
2614
+ ...document,
2615
+ generationPlans: (document.generationPlans ?? []).map((record) => record.cellId === cellId
2616
+ && (record.status === "queued" || record.status === "awaiting-output")
2617
+ && generationPlanClaimRunId(record.plan) === runId
2618
+ ? {
2619
+ ...record,
2620
+ status: "failed",
2621
+ updatedAt,
2622
+ error: truncateGenerationError(error),
2623
+ }
2624
+ : record),
2625
+ };
2626
+ }
2612
2627
  async function failAwaitingNotebookCell(engine, input, message) {
2613
2628
  const updatedAt = new Date().toISOString();
2614
2629
  const result = await mutateNotebook(engine, input.notebookId, "fail_notebook_provider_output", (document) => {
@@ -2628,7 +2643,7 @@ async function failAwaitingNotebookCell(engine, input, message) {
2628
2643
  : undefined) ?? cell.id;
2629
2644
  const current = executionState(document)[failTargetId]
2630
2645
  ?? executionState(document)[cell.id];
2631
- const withFailure = withExecution(document, failTargetId, {
2646
+ const withFailure = withExecution(withFailedGenerationPlan(document, cell.id, input.runId, message, updatedAt), failTargetId, {
2632
2647
  fingerprint: current?.fingerprint
2633
2648
  ?? notebookCellFingerprint(document, cell),
2634
2649
  status: "error",
@@ -0,0 +1,141 @@
1
+ import { computeArtifactStatus } from "videobook-engine";
2
+ import { EMPTY_TAG_SUMMARY, matchesTagFilter, readAssetTagSummaries, } from "./asset-tags.js";
3
+ import { assetDuration, assetMediaUrls, assetStreamProfile, findAssetMediaFile, groupByArtifactId, indexByArtifactId, PARTIAL_DOWNLOAD_RE, scheduleMissingVideoPoster, servedVideoProxyFile, } from "./asset-media-projection.js";
4
+ function metadataNumber(meta, key) {
5
+ const value = meta?.[key];
6
+ return typeof value === "number" ? value : null;
7
+ }
8
+ function metadataString(meta, key) {
9
+ const value = meta?.[key];
10
+ return typeof value === "string" ? value : null;
11
+ }
12
+ function readJobIndexes(engine) {
13
+ const pending = engine.jobs.pending.findAll();
14
+ const failures = engine.jobs.failures.findAll();
15
+ const work = engine.jobs.artifactWork.list();
16
+ const streams = engine.streams.list();
17
+ return {
18
+ pending: indexByArtifactId(pending.ok ? pending.value : []),
19
+ failures: indexByArtifactId(failures.ok ? failures.value : []),
20
+ work: indexByArtifactId(work.ok ? work.value : []),
21
+ streams: groupByArtifactId(streams.ok ? streams.value : []),
22
+ };
23
+ }
24
+ async function readCandidate(engine, artifact, jobs, tags) {
25
+ const [manifest, metadata] = await Promise.all([
26
+ engine.files.manifest(artifact.artifactId, { includeDotfiles: true }),
27
+ engine.metadata.artifacts.read(artifact.artifactId, "original"),
28
+ ]);
29
+ const files = manifest.ok ? manifest.value.files : [];
30
+ const meta = metadata.ok ? metadata.value : null;
31
+ const mediaFile = findAssetMediaFile(files, artifact);
32
+ const work = jobs.work.get(artifact.artifactId) ?? null;
33
+ const status = computeArtifactStatus({
34
+ kind: artifact.kind,
35
+ fileNames: new Set(files.map((file) => file.name)),
36
+ primaryMediaName: mediaFile?.name ?? null,
37
+ hasOriginalMetadata: metadata.ok,
38
+ hasPartFile: files.some((file) => PARTIAL_DOWNLOAD_RE.test(file.name)),
39
+ lockData: engine.jobs.locks.get(artifact.path),
40
+ pendingTask: jobs.pending.get(artifact.artifactId) ?? null,
41
+ generationError: jobs.failures.get(artifact.artifactId) ?? null,
42
+ artifactRow: work
43
+ ? { status: work.status, meta: work.meta, deadlineAt: work.deadlineAt }
44
+ : null,
45
+ });
46
+ return {
47
+ id: artifact.artifactId,
48
+ artifact_id: artifact.artifactId,
49
+ ...(artifact.label ? { label: artifact.label } : {}),
50
+ type: artifact.kind,
51
+ created_at: new Date(artifact.createdAt).toISOString(),
52
+ status,
53
+ prompt: metadataString(meta, "prompt"),
54
+ object_hash: mediaFile?.objectHash ?? null,
55
+ tags,
56
+ source: {
57
+ artifact,
58
+ files,
59
+ meta,
60
+ mediaFile,
61
+ work,
62
+ streams: jobs.streams.get(artifact.artifactId) ?? [],
63
+ },
64
+ };
65
+ }
66
+ export async function readLibraryCandidates(engine, input) {
67
+ const artifacts = engine.artifacts.list({ sort: "newest" })
68
+ .filter((artifact) => input.kinds.has(artifact.kind));
69
+ const tagsByArtifact = readAssetTagSummaries(engine, artifacts.map((artifact) => artifact.artifactId));
70
+ const tagged = artifacts.filter((artifact) => matchesTagFilter(tagsByArtifact.get(artifact.artifactId), input.tags));
71
+ if (tagged.length === 0)
72
+ return [];
73
+ const jobs = readJobIndexes(engine);
74
+ return Promise.all(tagged.map((artifact) => readCandidate(engine, artifact, jobs, tagsByArtifact.get(artifact.artifactId) ?? EMPTY_TAG_SUMMARY)));
75
+ }
76
+ function libraryAssetWork(work) {
77
+ return work
78
+ ? {
79
+ status: work.status,
80
+ meta: work.meta,
81
+ ownerKind: work.ownerKind,
82
+ deadlineAt: work.deadlineAt,
83
+ }
84
+ : null;
85
+ }
86
+ function framesPerSecond(meta, frameRate) {
87
+ const metadataFps = Number(meta?.fps);
88
+ if (Number.isFinite(metadataFps) && metadataFps > 0)
89
+ return metadataFps;
90
+ if (!frameRate || frameRate.denominator <= 0)
91
+ return null;
92
+ const streamFps = frameRate.numerator / frameRate.denominator;
93
+ return Number.isFinite(streamFps) && streamFps > 0 ? streamFps : null;
94
+ }
95
+ export async function projectLibraryAsset(engine, bookId, candidate) {
96
+ const { artifact, files, meta, mediaFile, work, streams } = candidate.source;
97
+ const profile = assetStreamProfile(streams, meta);
98
+ const proxyMetadata = artifact.kind === "video" && files.some((file) => file.name === "proxy.mp4")
99
+ ? await engine.metadata.artifacts.read(artifact.artifactId, "proxy")
100
+ : { ok: false, error: { code: "NOT_FOUND", message: "no proxy" } };
101
+ const proxyFile = servedVideoProxyFile({
102
+ bookId,
103
+ artifact,
104
+ files,
105
+ mediaFile,
106
+ proxyMetadata,
107
+ work,
108
+ sourceProfile: profile.sourceProfile,
109
+ });
110
+ await scheduleMissingVideoPoster(engine, {
111
+ bookId,
112
+ artifact,
113
+ files,
114
+ mediaFile,
115
+ status: candidate.status,
116
+ });
117
+ return {
118
+ id: candidate.id,
119
+ artifact_id: candidate.artifact_id,
120
+ ...(candidate.label ? { label: candidate.label } : {}),
121
+ type: candidate.type,
122
+ created_at: candidate.created_at,
123
+ ...assetMediaUrls({ bookId, artifact, files, mediaFile, proxyFile }),
124
+ object_hash: candidate.object_hash,
125
+ status: candidate.status,
126
+ assetWork: libraryAssetWork(work),
127
+ width: metadataNumber(meta, "width"),
128
+ height: metadataNumber(meta, "height"),
129
+ origin: metadataString(meta, "origin"),
130
+ prompt: candidate.prompt,
131
+ duration: assetDuration({
132
+ bookId,
133
+ artifactId: artifact.artifactId,
134
+ meta,
135
+ mediaFile,
136
+ profile,
137
+ }),
138
+ fps: framesPerSecond(meta, profile.frameRate),
139
+ tags: candidate.tags,
140
+ };
141
+ }
@@ -1,12 +1,12 @@
1
1
  export const LIGHTNING_SCENE_MODEL = "kie:nano-banana-2-1";
2
- export const LIGHTNING_SCENE_ASPECT_RATIO = "16:9";
3
- export const LIGHTNING_SCENE_RESOLUTION = "2K";
2
+ export const LIGHTNING_SCENE_ASPECT_RATIO = "9:16";
3
+ export const LIGHTNING_SCENE_RESOLUTION = "4K";
4
4
  export const LIGHTNING_SCENE_OUTPUT_FORMAT = "jpg";
5
5
  export const LIGHTNING_SCENE_PROMPT_WORDS = 25;
6
6
  export const LIGHTNING_VIDEO_MODEL = "kie:bytedance/seedance-2-5";
7
- export const LIGHTNING_VIDEO_ASPECT_RATIO = "16:9";
7
+ export const LIGHTNING_VIDEO_ASPECT_RATIO = "9:16";
8
8
  export const LIGHTNING_VIDEO_RESOLUTION = "1080p";
9
- export const LIGHTNING_VIDEO_DURATION_SECONDS = 13;
9
+ export const LIGHTNING_VIDEO_DURATION_SECONDS = 12;
10
10
  export const LIGHTNING_VIDEO_OUTPUT_FORMAT = "mp4";
11
11
  export const LIGHTNING_VIDEO_GENERATE_AUDIO = true;
12
12
  export const LIGHTNING_VIDEO_PROMPT_WORDS = 60;
@@ -22,7 +22,7 @@ const SEEDANCE_US_REFERENCE_MODES = [
22
22
  ];
23
23
  const XAI_MODES = [
24
24
  { id: "i2v", label: "Image to video", roles: ["first_frame"] },
25
- { id: "r2v", label: "References", roles: ["image"] },
25
+ { id: "r2v", label: "References", roles: ["image"], defaults: { duration: 10 } },
26
26
  { id: "v2v", label: "Edit video", roles: ["video"], requireAny: ["video"] },
27
27
  ];
28
28
  const MACHGEN_FRAMES = { id: "i2v", label: "Frames", roles: ["first_frame", "last_frame"], requireAny: ["first_frame"] };
@@ -157,10 +157,11 @@ export const MODEL_ENTRIES = [
157
157
  },
158
158
  { id: "kie:grok-imagine/image-to-video", media: "video", operations: ["image_to_video"], guide: "grok_imagine" },
159
159
  { id: "kie:grok-imagine/text-to-video", media: "video", operations: ["text_to_video"], guide: "grok_imagine", defaults: { duration: 6, resolution: "480p" } },
160
+ { id: "kie:grok-imagine-video-1-5-preview", media: "video", operations: ["image_to_video"], title: "Grok Imagine Video 1.5 Preview (Kie)", guide: "grok_imagine", dialect: "natural_strip_frames" },
160
161
  { id: "kie:grok-imagine/extend", media: "video", operations: ["extend_video"], hidden: true },
161
162
  { id: "kie:grok-imagine/upscale", media: "video", operations: ["upscale_video"], hidden: true },
162
163
  { id: "kie:kling-3.0-omni/text-to-video", media: "video", operations: ["text_to_video"], guide: "kling30", dialect: "natural", defaults: { customize_multi_shots: false } },
163
- { id: "kie:kling-3.0-omni/image-to-video", media: "video", operations: ["image_to_video"], guide: "kling30", dialect: "natural" },
164
+ { id: "kie:kling-3.0-omni/image-to-video", media: "video", operations: ["image_to_video"], guide: "kling30", dialect: "natural", defaults: { aspect_ratio: "auto" }, pinnedDefaults: ["aspect_ratio"] },
164
165
  { id: "kie:kling-3.0-omni/reference-to-video", media: "video", operations: ["reference_to_video"], guide: "kling30", dialect: "natural", defaults: { audio: false } },
165
166
  {
166
167
  id: "kie:kling-3.0-omni/transformation",
@@ -173,21 +174,26 @@ export const MODEL_ENTRIES = [
173
174
  { id: "v2v_refs", label: "Edit with references", roles: ["video", "image"], requireAny: ["video"] },
174
175
  ],
175
176
  },
176
- { id: "kie:kling-3.0/motion-control", media: "video", operations: ["motion_control"], guide: "kling30", dialect: "natural", defaults: { mode: "720p" } },
177
+ { id: "kie:kling-3.0/motion-control", media: "video", operations: ["motion_control"], guide: "kling30", dialect: "natural", defaults: { mode: "720p" }, outputLength: "video" },
178
+ { id: "kie:kling-4.0/reference-to-video", media: "video", operations: ["reference_to_video"], dialect: "kling4" },
179
+ { id: "kie:kling-4.0/keyframe-to-video", media: "video", operations: ["image_to_video"], dialect: "kling4_keyframes", refs: { image: "keyframe_urls" } },
177
180
  { id: "kie:gemini-omni-video", media: "video", operations: ["image_to_video", "edit_video"], dialect: "natural_source_video", defaults: { duration: "8" }, modes: [{ id: "i2v", label: "Image to video", roles: ["image"] }, { id: "v2v", label: "Edit video", roles: ["video", "image"], requireAny: ["video"] }], refItem: { video: { start: 0, ends: 8 } }, aliases: { clip_start: "video_list.0.start", clip_end: "video_list.0.ends" } },
178
181
  { id: "kie:wan/3-0-video", media: "video", operations: ["text_to_video", "image_to_video", "reference_to_video"], dialect: "seedance", modes: SEEDANCE_MODES },
179
182
  { id: "kie:wan/3-0-video-prime", media: "video", operations: ["text_to_video", "image_to_video", "reference_to_video"], dialect: "seedance", modes: SEEDANCE_MODES },
180
183
  { id: "kie:happyhorse-1-1/text-to-video", media: "video", operations: ["text_to_video"] },
181
184
  { id: "kie:happyhorse-1-1/image-to-video", media: "video", operations: ["image_to_video"], dialect: "strip_frames" },
182
185
  { id: "kie:happyhorse-1-1/reference-to-video", media: "video", operations: ["reference_to_video"], dialect: "brackets" },
183
- { id: "kie:happyhorse/video-edit", media: "video", operations: ["edit_video"], dialect: "happyhorse_edit" },
186
+ { id: "kie:happyhorse/video-edit", media: "video", operations: ["edit_video"], dialect: "happyhorse_edit", outputLength: "video" },
184
187
  { id: "kie:minimax-h3/text-to-video", media: "video", operations: ["text_to_video"] },
185
188
  { id: "kie:minimax-h3/image-to-video", media: "video", operations: ["image_to_video"], dialect: "strip_frames" },
186
189
  { id: "kie:minimax-h3/reference-to-video", media: "video", operations: ["reference_to_video"], dialect: "natural", rules: { refDurationMs: MiniMaxH3ReferenceClipDurationMs } },
187
- { id: "kie:volcengine/video-to-video-lip-sync", media: "video", operations: ["lip_sync"] },
188
- { id: "kie:omnihuman-1-5", media: "video", operations: ["image_to_video", "lip_sync"], dialect: "natural" },
190
+ { id: "kie:volcengine/video-to-video-lip-sync", media: "video", operations: ["lip_sync"], outputLength: "audio" },
191
+ { id: "kie:omnihuman-1-5", media: "video", operations: ["image_to_video", "lip_sync"], dialect: "natural", outputLength: "audio" },
192
+ { id: "kie:omnihuman-2-0", media: "video", operations: ["image_to_video", "lip_sync"], dialect: "natural", defaults: { enable_watermark: false }, outputLength: "audio" },
189
193
  { id: "kie:vidu-q4/image-to-video", media: "video", operations: ["image_to_video"], dialect: "natural_kinds" },
190
194
  { id: "kie:vidu-q4/reference-to-video", media: "video", operations: ["reference_to_video"], dialect: "natural_kinds" },
195
+ { id: "kie:pixverse-v6-max/text-to-video", media: "video", operations: ["text_to_video"], defaults: { quality: "1080p" } },
196
+ { id: "kie:pixverse-v6-max/image-to-video", media: "video", operations: ["image_to_video"], dialect: "strip_frames", defaults: { quality: "1080p" } },
191
197
  { id: "kie:veo3", media: "video", operations: ["text_to_video", "image_to_video"], title: "Veo 3 (Kie)" },
192
198
  { id: "kie:veo3_fast", media: "video", operations: ["text_to_video", "image_to_video"], title: "Veo 3 Fast (Kie)" },
193
199
  { id: "xai:grok-imagine-video", media: "video", operations: ["text_to_video", "image_to_video", "reference_to_video", "edit_video"], guide: "grok_imagine", title: "Grok Imagine Video (xAI)", dialect: "natural", refs: { first_frame: "image", audio: null }, modes: XAI_MODES },
@@ -1,9 +1,10 @@
1
1
  import { MODEL_DISPLAY } from "@apicity/cost";
2
2
  import { modelEntry } from "./catalog.js";
3
3
  import { parseModelId } from "./model-id.js";
4
+ import { preferredDefaults } from "./preferred-defaults.js";
4
5
  import { claimedFields, resolveSlot } from "./ref-fields.js";
5
6
  import { wireSpec } from "./wire.js";
6
- import { enumValues, numberBounds, propertyAt, schemaTypes, stringMaxLength, toJsonSchema, } from "./json-schema.js";
7
+ import { acceptsNoValue, enumValues, numberBounds, propertyAt, schemaTypes, stringMaxLength, toJsonSchema, } from "./json-schema.js";
7
8
  import { REF_ROLES, } from "./types.js";
8
9
  const PROMPT_CANDIDATES = ["prompt", "text", "instruction", "description"];
9
10
  const cache = new Map();
@@ -74,7 +75,7 @@ function resolveOptions(entry, input, excluded, fixed) {
74
75
  const defaults = entry.defaults ?? {};
75
76
  const options = [];
76
77
  for (const [name, schema] of Object.entries(input.properties ?? {})) {
77
- if (excluded.has(name) || name in fixed)
78
+ if (excluded.has(name) || name in fixed || acceptsNoValue(schema))
78
79
  continue;
79
80
  options.push(optionSpec(name, name, schema, required.has(name), defaults));
80
81
  }
@@ -121,8 +122,10 @@ function build(entry) {
121
122
  excluded.add(top);
122
123
  if (promptTop)
123
124
  excluded.add(promptTop);
125
+ const preferred = preferredDefaults(entry, resolveOptions(entry, input, excluded, wire.fixed));
126
+ const tuned = { ...entry, defaults: { ...entry.defaults, ...preferred } };
124
127
  return {
125
- entry,
128
+ entry: tuned,
126
129
  id: entry.id,
127
130
  provider: parsed.provider,
128
131
  apiId: parsed.apiId,
@@ -132,10 +135,18 @@ function build(entry) {
132
135
  promptField,
133
136
  promptRequired: promptTop !== undefined && promptField === promptTop && (input.required ?? []).includes(promptTop),
134
137
  slots,
135
- options: resolveOptions(entry, input, excluded, wire.fixed),
138
+ options: resolveOptions(tuned, input, excluded, wire.fixed),
136
139
  modes: entry.modes ? [...entry.modes] : defaultModes(slots),
137
140
  };
138
141
  }
142
+ export function resolveEntry(entry) {
143
+ const cached = cache.get(entry.id);
144
+ if (cached)
145
+ return cached;
146
+ const resolved = build(entry);
147
+ cache.set(entry.id, resolved);
148
+ return resolved;
149
+ }
139
150
  export function resolveModel(model) {
140
151
  const cached = cache.get(model);
141
152
  if (cached)
@@ -143,9 +154,7 @@ export function resolveModel(model) {
143
154
  const entry = modelEntry(model);
144
155
  if (!entry)
145
156
  throw new UnknownModelError(model);
146
- const resolved = build(entry);
147
- cache.set(model, resolved);
148
- return resolved;
157
+ return resolveEntry(entry);
149
158
  }
150
159
  export function tryResolveModel(model) {
151
160
  try {
@@ -31,7 +31,6 @@ export const FRONTIER_CATALOG_GAPS = {
31
31
  "fal:bytedance.seedance2p0.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
32
32
  "fal:blackforestlabs.flux3.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
33
33
  "fal:xai.grokImagineVideo.v1p5.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
34
- "kie:grok-imagine-video-1-5-preview": "This exact endpoint is not in the Videobook catalog.",
35
34
  "xai:v1.videos.generations.imageToVideo": "The catalog uses grok-imagine-video, not the ranked 1.5 model id.",
36
35
  "fal:alibaba.happyHorse.v1p1.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
37
36
  "fal:bytedance.seedance2p5.referenceToVideo": "This exact endpoint is not in the Videobook catalog.",
@@ -0,0 +1,25 @@
1
+ import { isKieVeoModel } from "./wire.js";
2
+ export function generationHarEndpoint(model) {
3
+ switch (model.provider) {
4
+ case "kie":
5
+ if (isKieVeoModel(model.apiId))
6
+ return "api.v1.veo.generate";
7
+ if (model.apiId === "suno/generate")
8
+ return "api.v1.generate";
9
+ if (model.apiId === "suno/sounds-generate")
10
+ return "api.v1.generate.sounds";
11
+ return "api.v1.jobs.createTask";
12
+ case "fal": return "v1.queue.submit";
13
+ case "xai": return "v1.videos.generations";
14
+ case "alibaba": return "api.v1.services.aigc.multimodalGeneration.generation";
15
+ case "machgen": return "api.v0.generate";
16
+ case "elevenlabs":
17
+ switch (model.apiId) {
18
+ case "sound-generation": return "v1.soundGeneration";
19
+ case "music": return "v1.music";
20
+ case "video-to-music": return "v1.music.videoToMusic";
21
+ case "text-to-speech": return "v1.textToSpeech";
22
+ default: throw new Error(`Unsupported ElevenLabs model ${model.apiId}`);
23
+ }
24
+ }
25
+ }
@@ -43,6 +43,9 @@ export function isKeyframeItem(schema) {
43
43
  && branch.properties?.image_url !== undefined
44
44
  && branch.properties?.frame_index !== undefined);
45
45
  }
46
+ export function acceptsNoValue(schema) {
47
+ return schema.not !== undefined && Object.keys(schema.not).length === 0;
48
+ }
46
49
  export function isArraySchema(schema) {
47
50
  return branches(schema).some((branch) => branch.type === "array");
48
51
  }
@@ -1,7 +1,9 @@
1
1
  import { modelFrontierRankings } from "./frontier.js";
2
2
  import { PROVIDER_ENV_KEYS } from "./adapters/index.js";
3
3
  import { MODEL_ENTRIES } from "./catalog.js";
4
- import { resolveModel } from "./descriptor.js";
4
+ import { resolveEntry } from "./descriptor.js";
5
+ import { registerDiagnosticName } from "../diagnostics.js";
6
+ import { logger } from "../logger.js";
5
7
  export function modelIsHidden(entry) {
6
8
  return entry.hidden === true && modelFrontierRankings(entry.id).length === 0;
7
9
  }
@@ -22,10 +24,30 @@ export function modelListing(model) {
22
24
  frontier: modelFrontierRankings(model.id),
23
25
  };
24
26
  }
27
+ for (const entry of MODEL_ENTRIES)
28
+ registerDiagnosticName("model", entry.id);
29
+ const unresolvableReported = new Set();
30
+ function reportUnresolvable(entry, error) {
31
+ if (unresolvableReported.has(entry.id))
32
+ return;
33
+ unresolvableReported.add(entry.id);
34
+ logger.warn("model-unresolvable", { model: entry.id, error });
35
+ }
36
+ function resolveOrSkip(entry) {
37
+ try {
38
+ return [resolveEntry(entry)];
39
+ }
40
+ catch (error) {
41
+ reportUnresolvable(entry, error);
42
+ return [];
43
+ }
44
+ }
45
+ export function resolvableModels(entries) {
46
+ return entries.flatMap(resolveOrSkip);
47
+ }
25
48
  export function listModels(filter = {}) {
26
- return MODEL_ENTRIES
49
+ return resolvableModels(MODEL_ENTRIES
27
50
  .filter((entry) => filter.includeHidden || !modelIsHidden(entry))
28
51
  .filter((entry) => !filter.media || entry.media === filter.media)
29
- .filter((entry) => !filter.operation || entry.operations.includes(filter.operation))
30
- .map((entry) => resolveModel(entry.id));
52
+ .filter((entry) => !filter.operation || entry.operations.includes(filter.operation)));
31
53
  }