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,161 @@
1
+ const PREFERRED_DURATION_SECONDS = 12;
2
+ const PREFERRED_ASPECT_RATIO = "9:16";
3
+ const PREFERRED_IMAGE_SIZE_PRESET = "portrait_16_9";
4
+ const IMAGE_SIZE_PRESETS = new Set(["square_hd", "square", "portrait_4_3", "portrait_16_9", "landscape_4_3", "landscape_16_9"]);
5
+ const GROUP_FIELDS = {
6
+ duration: ["duration"],
7
+ aspect: ["aspect_ratio", "aspectRatio", "image_size"],
8
+ resolution: ["resolution", "output_resolution", "size", "mode"],
9
+ quality: ["quality"],
10
+ bitrate: ["bitrate_mode"],
11
+ };
12
+ const QUALITY_RANK = ["very_low", "low", "basic", "standard", "medium", "high"];
13
+ const UNTIMED_VIDEO_OPERATIONS = new Set(["edit_video", "lip_sync", "upscale_video", "motion_control"]);
14
+ export function stickySettingGroup(name) {
15
+ const entry = Object.entries(GROUP_FIELDS).find(([, fields]) => fields.includes(name));
16
+ return entry ? entry[0] : null;
17
+ }
18
+ function resolutionRank(value) {
19
+ if (typeof value === "number")
20
+ return value;
21
+ const match = /^(\d+(?:\.\d+)?)\s*(p|k|sq)?$/iu.exec(value.trim());
22
+ if (!match)
23
+ return null;
24
+ const amount = Number(match[1]);
25
+ return match[2]?.toLowerCase() === "k" ? amount * 1000 : amount;
26
+ }
27
+ function isResolutionEnum(values) {
28
+ return values.length > 0 && values.every((value) => resolutionRank(value) !== null);
29
+ }
30
+ function highestBy(values, rank) {
31
+ const ranked = values.flatMap((value) => {
32
+ const score = rank(value);
33
+ return score === null ? [] : [{ value, score }];
34
+ });
35
+ if (ranked.length === 0)
36
+ return undefined;
37
+ return ranked.reduce((best, candidate) => candidate.score > best.score ? candidate : best).value;
38
+ }
39
+ function qualityRank(value) {
40
+ const index = QUALITY_RANK.indexOf(String(value).toLowerCase());
41
+ return index < 0 ? null : index;
42
+ }
43
+ function isNumeric(option) {
44
+ return option.valueType === "number" || option.valueType === "integer" || option.valueType === "json";
45
+ }
46
+ function clampToBounds(option, value) {
47
+ const low = option.minimum ?? Number.NEGATIVE_INFINITY;
48
+ const high = option.maximum ?? Number.POSITIVE_INFINITY;
49
+ return Math.min(Math.max(value, low), high);
50
+ }
51
+ function nearestDurationChoice(values, target) {
52
+ const numeric = values.filter((value) => Number.isFinite(Number(value)));
53
+ if (numeric.length === 0)
54
+ return undefined;
55
+ const atOrBelow = numeric.filter((value) => Number(value) <= target);
56
+ const pool = atOrBelow.length > 0 ? atOrBelow : numeric;
57
+ return pool.reduce((best, candidate) => Math.abs(Number(candidate) - target) < Math.abs(Number(best) - target) ? candidate : best);
58
+ }
59
+ function preferredDuration(option) {
60
+ if (option.enumValues.length > 0)
61
+ return nearestDurationChoice(option.enumValues, PREFERRED_DURATION_SECONDS);
62
+ if (!isNumeric(option))
63
+ return undefined;
64
+ return clampToBounds(option, PREFERRED_DURATION_SECONDS);
65
+ }
66
+ function takesImageSizePreset(entry, option) {
67
+ if (option.name !== "image_size" || option.valueType !== "json" || !entry.id.startsWith("fal:"))
68
+ return false;
69
+ if (!entry.operations.includes("text_to_image"))
70
+ return false;
71
+ return option.defaultValue === undefined || IMAGE_SIZE_PRESETS.has(String(option.defaultValue));
72
+ }
73
+ function preferredAspect(entry, option) {
74
+ if (option.enumValues.includes(PREFERRED_ASPECT_RATIO))
75
+ return PREFERRED_ASPECT_RATIO;
76
+ return takesImageSizePreset(entry, option) ? PREFERRED_IMAGE_SIZE_PRESET : undefined;
77
+ }
78
+ function preferredResolution(option) {
79
+ if (!isResolutionEnum(option.enumValues.filter((value) => value !== "auto")))
80
+ return undefined;
81
+ return highestBy(option.enumValues, resolutionRank);
82
+ }
83
+ function preferredQuality(option) {
84
+ return highestBy(option.enumValues, qualityRank);
85
+ }
86
+ export function takesDuration(entry) {
87
+ return entry.media === "video" && entry.operations.some((operation) => !UNTIMED_VIDEO_OPERATIONS.has(operation));
88
+ }
89
+ function preferredValue(entry, option) {
90
+ if (option.name !== option.path || entry.pinnedDefaults?.includes(option.name))
91
+ return undefined;
92
+ const group = stickySettingGroup(option.name);
93
+ if (group === "duration")
94
+ return takesDuration(entry) ? preferredDuration(option) : undefined;
95
+ if (group === "aspect")
96
+ return preferredAspect(entry, option);
97
+ if (group === "resolution")
98
+ return preferredResolution(option);
99
+ if (group === "quality" || group === "bitrate")
100
+ return preferredQuality(option);
101
+ return undefined;
102
+ }
103
+ export function preferredDefaults(entry, options) {
104
+ if (entry.media !== "video" && entry.media !== "image")
105
+ return {};
106
+ return Object.fromEntries(options.flatMap((option) => {
107
+ const value = preferredValue(entry, option);
108
+ return value === undefined ? [] : [[option.name, value]];
109
+ }));
110
+ }
111
+ function sameResolution(left, right) {
112
+ const a = resolutionRank(left);
113
+ const b = resolutionRank(right);
114
+ return a !== null && a === b && String(left).toLowerCase().replace(/p$/u, "") === String(right).toLowerCase().replace(/p$/u, "");
115
+ }
116
+ function matchingChoice(choices, value) {
117
+ return choices.find((choice) => choice === value)
118
+ ?? choices.find((choice) => String(choice).toLowerCase() === String(value).toLowerCase())
119
+ ?? choices.find((choice) => typeof choice !== "boolean" && typeof value !== "boolean" && sameResolution(choice, value));
120
+ }
121
+ function acceptedStickyValue(control, value) {
122
+ if (control.options.length > 0)
123
+ return matchingChoice(control.options, value);
124
+ if (control.valueType !== "number")
125
+ return undefined;
126
+ const numeric = Number(value);
127
+ if (!Number.isFinite(numeric))
128
+ return undefined;
129
+ if (control.minimum !== undefined && numeric < control.minimum)
130
+ return undefined;
131
+ if (control.maximum !== undefined && numeric > control.maximum)
132
+ return undefined;
133
+ return numeric;
134
+ }
135
+ function isStickyValue(group, value) {
136
+ if (typeof value !== "string" && typeof value !== "number")
137
+ return false;
138
+ return group !== "resolution" || resolutionRank(value) !== null;
139
+ }
140
+ export function stickySettingsFrom(settings) {
141
+ return Object.fromEntries(Object.entries(settings).flatMap(([name, value]) => {
142
+ const group = stickySettingGroup(name);
143
+ if (!group || !isStickyValue(group, value))
144
+ return [];
145
+ return [[group, value]];
146
+ }));
147
+ }
148
+ function isStickySettingGroup(name) {
149
+ return Object.hasOwn(GROUP_FIELDS, name);
150
+ }
151
+ export function parseStickySettings(stored) {
152
+ return Object.fromEntries(Object.entries(stored).flatMap(([group, value]) => isStickySettingGroup(group) && isStickyValue(group, value) ? [[group, value]] : []));
153
+ }
154
+ export function stickySettingsFor(controls, sticky) {
155
+ return Object.fromEntries(controls.flatMap((control) => {
156
+ const group = stickySettingGroup(control.name);
157
+ const remembered = group ? sticky[group] : undefined;
158
+ const value = remembered === undefined ? undefined : acceptedStickyValue(control, remembered);
159
+ return value === undefined ? [] : [[control.name, value]];
160
+ }));
161
+ }
@@ -1,4 +1,4 @@
1
- import { replaceHappyHorseAssetMentions, replaceNaturalAssetMentions, rewriteImageRefMentionsToBrackets, rewriteSeedanceAssetMentions, rewriteViduReferenceMentions, stripRefMentions, } from "../generation-reference-mentions.js";
1
+ import { replaceHappyHorseAssetMentions, replaceNaturalAssetMentions, rewriteImageRefMentionsToBrackets, rewriteKling4FrameMentions, rewriteKling4ReferenceMentions, rewriteSeedanceAssetMentions, rewriteViduReferenceMentions, stripRefMentions, } from "../generation-reference-mentions.js";
2
2
  const SLOT_TOKEN = /@((image|video|audio)(\d+)|firstframe|lastframe)(?![\w-])/giu;
3
3
  function refsFor(refs, role) {
4
4
  return (refs[role] ?? []).filter((ref) => !/^https?:\/\//u.test(ref));
@@ -58,6 +58,14 @@ function rewrite(dialect, prompt, refs) {
58
58
  return rewriteImageRefMentionsToBrackets(prompt, refsFor(refs, "image"));
59
59
  case "vidu":
60
60
  return rewriteViduReferenceMentions(prompt, refsFor(refs, "image"), refsFor(refs, "audio"));
61
+ case "kling4":
62
+ return rewriteKling4ReferenceMentions(prompt, refsFor(refs, "image"), refsFor(refs, "video"));
63
+ case "kling4_keyframes":
64
+ return rewriteKling4FrameMentions(prompt, {
65
+ first: refsFor(refs, "first_frame"),
66
+ keyframes: refsFor(refs, "image"),
67
+ last: refsFor(refs, "last_frame"),
68
+ });
61
69
  case "happyhorse_edit":
62
70
  return replaceHappyHorseAssetMentions(prompt, refsFor(refs, "video")[0] ?? null, refsFor(refs, "image"));
63
71
  }
@@ -7,9 +7,9 @@ import { markDescendantsStale } from "./notebook-action-catalog.js";
7
7
  import { mutateNotebook } from "./notebook-mutations.js";
8
8
  import { searchReferenceStore } from "./search-reference-store.js";
9
9
  import { comparableHitScore } from "./search-scoring.js";
10
- import { buildAssetList } from "./tools/reconcile-project-work.js";
10
+ import { projectLibraryAsset, readLibraryCandidates, } from "./library-asset-projection.js";
11
11
  import { assetRecency, readAssetUsage, recordAssetUse } from "./asset-usage.js";
12
- import { matchesTagFilter, tagFacetCounts, transferAssetTags, } from "./asset-tags.js";
12
+ import { tagFacetCounts, transferAssetTags, } from "./asset-tags.js";
13
13
  import { addStatusCounts, countBrowseStatuses, emptyStatusCounts, matchesBrowseStatus, pageLibraryAssets, } from "./library-browse.js";
14
14
  function isMediaCellType(value) {
15
15
  return value === "audio" || value === "image" || value === "video";
@@ -101,9 +101,12 @@ export async function setNotebookMediaSlot(engine, input) {
101
101
  revision: result.value.hash,
102
102
  };
103
103
  }
104
- function kindMatches(requested, kind) {
105
- return requested.has(kind)
106
- || (kind === "final" && requested.has("video"));
104
+ function libraryArtifactKinds(requested) {
105
+ return [
106
+ ...(requested.has("audio") ? ["audio"] : []),
107
+ ...(requested.has("image") ? ["image"] : []),
108
+ ...(requested.has("video") ? ["video", "final"] : []),
109
+ ];
107
110
  }
108
111
  function searchRank(asset, query) {
109
112
  if (!query)
@@ -143,175 +146,177 @@ function compareRecentlyUsed(left, right) {
143
146
  ? created
144
147
  : left.artifact_id.localeCompare(right.artifact_id);
145
148
  }
146
- export async function searchLibraryAssets(books, input) {
147
- await books.ready();
148
- const query = input.query?.trim().toLowerCase() ?? "";
149
- const requested = new Set(input.kinds);
150
- const prepared = input.referenceId
151
- ? searchReferenceStore.get(input.referenceId).reference
152
- : undefined;
153
- const searching = Boolean(query || prepared);
154
- const browseStatus = searching
155
- ? "ready"
156
- : (input.browseStatus ?? "ready");
157
- const artifactKinds = [
158
- ...(requested.has("audio") ? ["audio"] : []),
159
- ...(requested.has("image") ? ["image"] : []),
160
- ...(requested.has("video") ? ["video", "final"] : []),
161
- ];
162
- const summaries = books.list({ sort: "newest" });
163
- const results = await Promise.allSettled(summaries.map(async (summary) => {
164
- const book = books.get(summary.bookId);
165
- if (!book.ok)
166
- throw new Error(book.error.message);
167
- const engine = book.value.engine;
168
- const builtAssets = await engine.withLocalMedia(() => buildAssetList(engine, summary.bookId, "newest"));
169
- const usage = readAssetUsage(engine);
170
- const eligible = builtAssets
171
- .filter((asset) => kindMatches(requested, asset.type)
172
- && matchesTagFilter(asset.tags ?? undefined, input.tags))
173
- .map((asset) => ({
174
- ...asset,
175
- owning_book_id: summary.bookId,
176
- owning_book_name: summary.name,
177
- last_used_at: lastUsedAt(usage.get(asset.artifact_id)),
178
- }));
179
- const statusCounts = countBrowseStatuses(eligible);
180
- const readyAssets = eligible.filter((asset) => asset.status === "ready");
181
- const assets = searching
182
- ? readyAssets
183
- : eligible.filter((asset) => matchesBrowseStatus(asset.status, browseStatus));
184
- let hits = [];
185
- let searchError;
186
- let preparationJob;
187
- if (searching) {
188
- registerLocalClipSearchProvider(engine);
189
- const searchQuery = {
190
- ...(query ? { text: query } : {}),
191
- modalities: query
192
- ? ["visual", "metadata"]
193
- : ["visual"],
194
- artifactKinds,
195
- sourceArtifactIds: readyAssets.map((asset) => asset.artifact_id),
196
- limit: 100,
197
- };
198
- const result = prepared
199
- ? await engine.temporalSearch.queryPrepared(searchQuery, prepared, input.referenceRange ? { range: input.referenceRange } : {})
200
- : await engine.temporalSearch.query(searchQuery);
201
- if (result.ok) {
202
- hits = result.value.hits;
203
- }
204
- else if (result.error.code === "NOT_READY" && result.error.details?.requiresIndexPreparation) {
205
- preparationJob = await enqueueTemporalSearchPreparation(summary.bookId);
206
- }
207
- else {
208
- searchError = result.error.message;
209
- }
210
- }
211
- return {
212
- assets,
213
- statusCounts,
214
- hits,
215
- searchError,
216
- preparationJob,
217
- unindexed: missingVisualIndexes(engine, readyAssets).length,
218
- };
149
+ function compareNewest(left, right) {
150
+ const created = Date.parse(right.created_at) - Date.parse(left.created_at);
151
+ return created !== 0
152
+ ? created
153
+ : left.artifact_id.localeCompare(right.artifact_id);
154
+ }
155
+ function entryKey(entry) {
156
+ return `${entry.owning_book_id}:${entry.artifact_id}`;
157
+ }
158
+ async function querySemanticHits(engine, plan, readyEntries) {
159
+ registerLocalClipSearchProvider(engine);
160
+ const searchQuery = {
161
+ ...(plan.query ? { text: plan.query } : {}),
162
+ modalities: plan.query
163
+ ? ["visual", "metadata"]
164
+ : ["visual"],
165
+ artifactKinds: plan.searchKinds,
166
+ sourceArtifactIds: readyEntries.map((entry) => entry.artifact_id),
167
+ limit: 100,
168
+ };
169
+ const result = plan.prepared
170
+ ? await engine.temporalSearch.queryPrepared(searchQuery, plan.prepared, plan.referenceRange ? { range: plan.referenceRange } : {})
171
+ : await engine.temporalSearch.query(searchQuery);
172
+ return result.ok ? { ok: true, value: result.value.hits } : result;
173
+ }
174
+ async function searchBook(books, summary, plan) {
175
+ const book = books.get(summary.bookId);
176
+ if (!book.ok)
177
+ throw new Error(book.error.message);
178
+ const engine = book.value.engine;
179
+ const candidates = await engine.withLocalMedia(() => readLibraryCandidates(engine, {
180
+ kinds: plan.candidateKinds,
181
+ ...(plan.tags ? { tags: plan.tags } : {}),
219
182
  }));
220
- const errors = [];
221
- const assets = [];
222
- let statusCounts = emptyStatusCounts();
223
- const semanticHits = [];
224
- results.forEach((result, index) => {
225
- if (result.status === "fulfilled") {
226
- assets.push(...result.value.assets);
227
- statusCounts = addStatusCounts(statusCounts, result.value.statusCounts);
228
- semanticHits.push(...result.value.hits.map((hit) => ({
229
- key: `${summaries[index]?.bookId ?? "unknown"}:${hit.artifactId}`,
230
- hit,
231
- })));
232
- if (result.value.searchError) {
233
- errors.push({
234
- book_id: summaries[index]?.bookId ?? "unknown",
235
- message: result.value.searchError,
236
- });
237
- }
238
- return;
239
- }
240
- const book = summaries[index];
241
- errors.push({
242
- book_id: book?.bookId ?? "unknown",
243
- message: result.reason instanceof Error
244
- ? result.reason.message
245
- : String(result.reason),
246
- });
247
- });
248
- const metadataRank = new Map(assets
249
- .filter((asset) => !prepared && matchesQuery(asset, query))
250
- .sort((left, right) => {
251
- const rank = searchRank(left, query) - searchRank(right, query);
252
- if (rank !== 0)
253
- return rank;
254
- const created = Date.parse(right.created_at) - Date.parse(left.created_at);
255
- return created !== 0
256
- ? created
257
- : left.artifact_id.localeCompare(right.artifact_id);
258
- })
259
- .map((asset, index) => [
260
- `${asset.owning_book_id}:${asset.artifact_id}`,
261
- index + 1,
262
- ]));
263
- semanticHits.sort((left, right) => comparableHitScore(right.hit) - comparableHitScore(left.hit)
183
+ const usage = readAssetUsage(engine);
184
+ const eligible = candidates.map((candidate) => ({
185
+ ...candidate,
186
+ owning_book_id: summary.bookId,
187
+ owning_book_name: summary.name,
188
+ last_used_at: lastUsedAt(usage.get(candidate.artifact_id)),
189
+ }));
190
+ const readyEntries = eligible.filter((entry) => entry.status === "ready");
191
+ const result = {
192
+ entries: plan.searching
193
+ ? readyEntries
194
+ : eligible.filter((entry) => matchesBrowseStatus(entry.status, plan.browseStatus)),
195
+ statusCounts: countBrowseStatuses(eligible),
196
+ hits: [],
197
+ unindexed: plan.indexCoverage ? missingVisualIndexes(engine, readyEntries).length : 0,
198
+ };
199
+ if (!plan.searching)
200
+ return result;
201
+ const hits = await querySemanticHits(engine, plan, readyEntries);
202
+ if (hits.ok)
203
+ return { ...result, hits: hits.value };
204
+ if (hits.error.code === "NOT_READY" && hits.error.details?.requiresIndexPreparation) {
205
+ return { ...result, preparationJob: await enqueueTemporalSearchPreparation(summary.bookId) };
206
+ }
207
+ return { ...result, searchError: hits.error.message };
208
+ }
209
+ function rankEntries(entries, semanticHits, plan) {
210
+ if (!plan.searching) {
211
+ return [...entries].sort(compareRecentlyUsed).map((entry) => ({ entry, hit: undefined }));
212
+ }
213
+ const metadataRank = new Map(entries
214
+ .filter((entry) => !plan.prepared && matchesQuery(entry, plan.query))
215
+ .sort((left, right) => searchRank(left, plan.query) - searchRank(right, plan.query)
216
+ || compareNewest(left, right))
217
+ .map((entry, index) => [entryKey(entry), index + 1]));
218
+ const sortedHits = [...semanticHits].sort((left, right) => comparableHitScore(right.hit) - comparableHitScore(left.hit)
264
219
  || left.key.localeCompare(right.key));
265
220
  const semanticRank = new Map();
266
221
  const semanticMatch = new Map();
267
- for (const item of semanticHits) {
222
+ for (const item of sortedHits) {
268
223
  if (semanticRank.has(item.key))
269
224
  continue;
270
225
  semanticRank.set(item.key, semanticRank.size + 1);
271
226
  semanticMatch.set(item.key, item.hit);
272
227
  }
273
- const ranked = assets
274
- .flatMap((asset) => {
275
- const key = `${asset.owning_book_id}:${asset.artifact_id}`;
228
+ return entries
229
+ .flatMap((entry) => {
230
+ const key = entryKey(entry);
276
231
  const metadata = metadataRank.get(key);
277
232
  const semantic = semanticRank.get(key);
278
- if ((query || prepared) && metadata === undefined && semantic === undefined) {
233
+ if (metadata === undefined && semantic === undefined)
279
234
  return [];
280
- }
281
- const hit = semanticMatch.get(key);
282
235
  return [{
283
- asset: hit
284
- ? {
285
- ...asset,
286
- search_match: {
287
- score: comparableHitScore(hit),
288
- location: hit.location,
289
- },
290
- }
291
- : asset,
236
+ entry,
237
+ hit: semanticMatch.get(key),
292
238
  score: (metadata === undefined ? 0 : 1 / (60 + metadata))
293
239
  + (semantic === undefined ? 0 : 1 / (60 + semantic)),
294
240
  }];
295
241
  })
296
- .sort((left, right) => {
297
- const rank = right.score - left.score;
298
- if (rank !== 0)
299
- return rank;
300
- const created = Date.parse(right.asset.created_at) - Date.parse(left.asset.created_at);
301
- return created !== 0
302
- ? created
303
- : left.asset.artifact_id.localeCompare(right.asset.artifact_id);
304
- })
305
- .map((item) => item.asset);
306
- if (!query && !prepared)
307
- ranked.sort(compareRecentlyUsed);
308
- const paged = pageLibraryAssets(ranked, {
242
+ .sort((left, right) => right.score - left.score || compareNewest(left.entry, right.entry))
243
+ .map(({ entry, hit }) => ({ entry, hit }));
244
+ }
245
+ async function projectLibraryEntry(books, entry, hit) {
246
+ const book = books.get(entry.owning_book_id);
247
+ if (!book.ok)
248
+ throw new Error(book.error.message);
249
+ const engine = book.value.engine;
250
+ const row = await engine.withLocalMedia(() => projectLibraryAsset(engine, entry.owning_book_id, entry));
251
+ return {
252
+ ...row,
253
+ owning_book_id: entry.owning_book_id,
254
+ owning_book_name: entry.owning_book_name,
255
+ last_used_at: entry.last_used_at,
256
+ ...(hit
257
+ ? { search_match: { score: comparableHitScore(hit), location: hit.location } }
258
+ : {}),
259
+ };
260
+ }
261
+ function librarySearchPlan(input) {
262
+ const query = input.query?.trim().toLowerCase() ?? "";
263
+ const prepared = input.referenceId
264
+ ? searchReferenceStore.get(input.referenceId).reference
265
+ : undefined;
266
+ const searching = Boolean(query || prepared);
267
+ const searchKinds = libraryArtifactKinds(new Set(input.kinds));
268
+ return {
269
+ query,
270
+ prepared,
271
+ referenceRange: input.referenceRange,
272
+ searching,
273
+ browseStatus: searching ? "ready" : (input.browseStatus ?? "ready"),
274
+ candidateKinds: new Set(searchKinds),
275
+ searchKinds,
276
+ tags: input.tags,
277
+ indexCoverage: input.indexCoverage ?? searching,
278
+ };
279
+ }
280
+ export async function searchLibraryAssets(books, input) {
281
+ await books.ready();
282
+ const plan = librarySearchPlan(input);
283
+ const summaries = books.list({ sort: "newest" });
284
+ const results = await Promise.allSettled(summaries.map((summary) => searchBook(books, summary, plan)));
285
+ const errors = [];
286
+ const entries = [];
287
+ let statusCounts = emptyStatusCounts();
288
+ const semanticHits = [];
289
+ results.forEach((result, index) => {
290
+ const bookId = summaries[index]?.bookId ?? "unknown";
291
+ if (result.status === "rejected") {
292
+ errors.push({
293
+ book_id: bookId,
294
+ message: result.reason instanceof Error
295
+ ? result.reason.message
296
+ : String(result.reason),
297
+ });
298
+ return;
299
+ }
300
+ entries.push(...result.value.entries);
301
+ statusCounts = addStatusCounts(statusCounts, result.value.statusCounts);
302
+ semanticHits.push(...result.value.hits.map((hit) => ({
303
+ key: `${bookId}:${hit.artifactId}`,
304
+ hit,
305
+ })));
306
+ if (result.value.searchError) {
307
+ errors.push({ book_id: bookId, message: result.value.searchError });
308
+ }
309
+ });
310
+ const ranked = rankEntries(entries, semanticHits, plan);
311
+ const paged = pageLibraryAssets(ranked.map(({ entry }) => entry), {
309
312
  offset: input.offset ?? 0,
310
313
  limit: input.limit ?? 50,
311
314
  });
315
+ const offset = input.offset ?? 0;
316
+ const pageHits = ranked.slice(offset, offset + paged.pageAssets.length);
312
317
  return {
313
- assets: paged.pageAssets,
314
- tag_facets: tagFacetCounts(ranked),
318
+ assets: await Promise.all(pageHits.map(({ entry, hit }) => projectLibraryEntry(books, entry, hit))),
319
+ tag_facets: tagFacetCounts(paged.matching),
315
320
  unindexed_visual_assets: results.reduce((count, result) => count + (result.status === "fulfilled" ? result.value.unindexed : 0), 0),
316
321
  preparationJobs: results.flatMap((result) => result.status === "fulfilled" && result.value.preparationJob
317
322
  ? [result.value.preparationJob]
@@ -0,0 +1,60 @@
1
+ import { tryResolveModel } from "./media/descriptor.js";
2
+ import { parseStickySettings, stickySettingGroup, stickySettingsFor, stickySettingsFrom, takesDuration, } from "./media/preferred-defaults.js";
3
+ import { controlsForModel } from "./notebook-generation-controls.js";
4
+ function settingsKey(notebookId) {
5
+ return `notebook-generation-settings:${notebookId}`;
6
+ }
7
+ function generationSettingsOf(cell) {
8
+ const settings = cell?.inputs?.generationSettings;
9
+ return settings && typeof settings === "object" && !Array.isArray(settings)
10
+ ? settings
11
+ : {};
12
+ }
13
+ function changedSettings(previous, next) {
14
+ return Object.fromEntries(Object.entries(next).filter(([name, value]) => previous[name] !== value));
15
+ }
16
+ export function readNotebookStickySettings(engine, notebookId) {
17
+ const stored = engine.settings.get(settingsKey(notebookId));
18
+ return stored ? parseStickySettings(stored) : {};
19
+ }
20
+ export function rememberNotebookStickySettings(engine, notebookId, next, previous) {
21
+ const changed = stickySettingsFrom(changedSettings(generationSettingsOf(previous), generationSettingsOf(next)));
22
+ if (Object.keys(changed).length === 0)
23
+ return;
24
+ const written = engine.settings.set(settingsKey(notebookId), {
25
+ ...readNotebookStickySettings(engine, notebookId),
26
+ ...changed,
27
+ });
28
+ if (!written.ok)
29
+ throw new Error(written.error.message);
30
+ }
31
+ function stickyControlsFor(model) {
32
+ const resolved = tryResolveModel(model);
33
+ if (!resolved)
34
+ return [];
35
+ const timed = takesDuration(resolved.entry);
36
+ return controlsForModel(resolved).flatMap((control) => {
37
+ const group = stickySettingGroup(control.name);
38
+ if (!group || (group === "duration" && !timed))
39
+ return [];
40
+ return [{
41
+ name: control.name,
42
+ valueType: control.valueType,
43
+ options: control.options ?? [],
44
+ ...(control.minimum === undefined ? {} : { minimum: control.minimum }),
45
+ ...(control.maximum === undefined ? {} : { maximum: control.maximum }),
46
+ }];
47
+ });
48
+ }
49
+ export function seedNotebookStickySettings(cell, sticky) {
50
+ if (!cell.model || Object.keys(sticky).length === 0)
51
+ return cell;
52
+ const explicit = generationSettingsOf(cell);
53
+ const seeded = stickySettingsFor(stickyControlsFor(cell.model).filter((control) => explicit[control.name] === undefined), sticky);
54
+ if (Object.keys(seeded).length === 0)
55
+ return cell;
56
+ return {
57
+ ...cell,
58
+ inputs: { ...(cell.inputs ?? {}), generationSettings: { ...seeded, ...explicit } },
59
+ };
60
+ }