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.
- package/CHANGELOG.md +135 -0
- package/dist/asset-media-projection.js +161 -0
- package/dist/command-reference.js +3 -1
- package/dist/diagnostics.js +10 -4
- package/dist/generation-reference-mentions.js +19 -0
- package/dist/goal-plan-v3.js +21 -6
- package/dist/goal-plan.js +1 -1
- package/dist/goal-store.js +2 -2
- package/dist/har-capture.js +36 -3
- package/dist/jobs/_image-edit-adapter.js +2 -0
- package/dist/jobs/_kie-upload.js +3 -1
- package/dist/jobs/assess-media-probes.js +94 -0
- package/dist/jobs/complete-provider-task.js +2 -5
- package/dist/jobs/generate-media.js +22 -12
- package/dist/jobs/image-subjects.js +2 -0
- package/dist/jobs/kie-paygate.js +3 -3
- package/dist/jobs/register-all.js +2 -0
- package/dist/jobs/run-notebook.js +16 -1
- package/dist/library-asset-projection.js +141 -0
- package/dist/lightning-defaults.js +4 -4
- package/dist/media/catalog.js +12 -6
- package/dist/media/descriptor.js +16 -7
- package/dist/media/frontier-gaps.js +0 -1
- package/dist/media/har-endpoint.js +25 -0
- package/dist/media/json-schema.js +3 -0
- package/dist/media/listing.js +26 -4
- package/dist/media/preferred-defaults.js +161 -0
- package/dist/media/prompt-dialects.js +9 -1
- package/dist/media-library.js +158 -153
- package/dist/notebook-sticky-settings.js +60 -0
- package/dist/provider-usage.js +24 -9
- package/dist/tools/estimate-cost.js +9 -4
- package/dist/tools/reconcile-project-work.js +37 -184
- package/dist/tools/videobook-domain.js +16 -1
- package/dist/usage-providers.js +3 -3
- package/package.json +15 -15
- package/ui/assets/{alive-captions-Cj8C1tCN.js → alive-captions-BWk_4aX0.js} +1 -1
- package/ui/assets/confirm-bodies-CNW9vLsi.js +1 -0
- package/ui/assets/{fullscreen-DyEncSdm.js → fullscreen-ND_H86Ih.js} +1 -1
- package/ui/assets/{goal-ui-BdFeoSWh.js → goal-ui-CCTD4lxi.js} +1 -1
- package/ui/assets/index-CTPstLYq.css +1 -0
- package/ui/assets/index-D0ftggO4.js +37 -0
- package/ui/assets/inspector-BZnbKsEZ.js +1 -0
- package/ui/assets/{jobs-ui-BudI4w0r.js → jobs-ui-BNK0IPSl.js} +1 -1
- package/ui/assets/{music-video-CT4C55ok.js → music-video-CoOI0cgW.js} +1 -1
- package/ui/assets/{ui-mq0uIS5P.css → ui-BBM3YCY6.css} +1 -1
- package/ui/assets/ui-koLWlu0t.js +2 -0
- package/ui/assets/vendor-Ca2-b3hB.js +19 -0
- package/ui/assets/{vendor-flow-nqTFQmTh.js → vendor-flow-C2NYBYCL.js} +1 -1
- package/ui/assets/vendor-icons-D3KrGMJY.js +1 -0
- package/ui/assets/{vendor-mcp-D_C3EZPv.js → vendor-mcp-9gUvOX4H.js} +2 -2
- package/ui/assets/videobook-B8dYNQrG.js +21 -0
- package/ui/index.html +14 -14
- package/ui/assets/confirm-bodies-BxWXwDf0.js +0 -1
- package/ui/assets/index-DWfR79lA.css +0 -1
- package/ui/assets/index-QZ8lbemN.js +0 -37
- package/ui/assets/inspector-BC6xWkJU.js +0 -1
- package/ui/assets/ui-z5r-p0K9.js +0 -2
- package/ui/assets/vendor-D8XcMav4.js +0 -19
- package/ui/assets/vendor-icons-CGqhCIXV.js +0 -1
- package/ui/assets/videobook-CsW453sb.js +0 -21
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,141 @@ and the package uses [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.2] - 2026-10-26
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- The KIE Grok Imagine Video 1.5 preview's 1080p tier, priced at $0.04 per
|
|
14
|
+
second. At 1080p it takes exactly one image (`image_urls`); 480p and 720p
|
|
15
|
+
still take more.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Every `@apicity` package is at 0.19.0.
|
|
20
|
+
- The KIE Grok Imagine Video 1.5 preview defaults to 1080p, its highest
|
|
21
|
+
resolution, as `kie:grok-imagine/image-to-video` already does. A request
|
|
22
|
+
with more than one image needs 480p or 720p and is refused before
|
|
23
|
+
anything is spent.
|
|
24
|
+
- The KIE Grok Imagine Video 1.5 preview sends `nsfw_checker: false` by
|
|
25
|
+
default, the default KIE documents and every other KIE video model with
|
|
26
|
+
the setting already sends. 0.3.1 sent `true`; pass `nsfw_checker: true` to
|
|
27
|
+
keep KIE's content filter on.
|
|
28
|
+
- KIE Seedance 2.5 keeps generating audio by default. apicity 0.19.0 now
|
|
29
|
+
defaults `generate_audio` to `false`, but Videobook still sends `true`
|
|
30
|
+
unless a request turns audio off.
|
|
31
|
+
- MachGen estimates follow the 0.19.0 pricebook. A MiniMax H3 run with a
|
|
32
|
+
first and a last frame is priced at the image-to-video rate ($0.035 per
|
|
33
|
+
second at 480p instead of $0.05), and Wan 3.0 references that include a
|
|
34
|
+
video add the reference video's seconds to the output's.
|
|
35
|
+
- KIE OmniHuman 2.0 generations turn KIE's watermark off by default
|
|
36
|
+
(`enable_watermark: false`); pass `enable_watermark: true` to keep it.
|
|
37
|
+
OmniHuman 2.0 videos generated with 0.3.1 carry the watermark. OmniHuman
|
|
38
|
+
1.5 has no watermark option.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- A catalog model the installed apicity packages cannot resolve no longer
|
|
43
|
+
fails the whole model listing, which left every book looking empty on
|
|
44
|
+
load. The listing skips that model and logs a `model-unresolvable`
|
|
45
|
+
warning naming it.
|
|
46
|
+
- Queued KIE OmniHuman 1.5 and 2.0 generations pass their driving audio's
|
|
47
|
+
length to the cost estimate, so the KIE paygate estimate is bounded and the
|
|
48
|
+
usage row is priced.
|
|
49
|
+
- Queued KIE HappyHorse video edit and Kling 3.0 motion control generations
|
|
50
|
+
pass their source video's length, and Volcengine lip sync passes its audio's
|
|
51
|
+
length, so their paygate estimates and usage rows are priced too.
|
|
52
|
+
`estimate_cost` now prices Volcengine lip sync from the audio, whose length
|
|
53
|
+
the output follows, instead of the source video.
|
|
54
|
+
- The usage counter groups each catalog generation under its catalog media.
|
|
55
|
+
34 KIE and fal models, among them OmniHuman, the HeyGen digital twins, the
|
|
56
|
+
Gemini and ElevenLabs speech models and the Bria image models, were counted
|
|
57
|
+
as analysis, and fal Wan 2.7 image edit as video.
|
|
58
|
+
- KIE Suno music and sound generations now reach the usage counter as audio,
|
|
59
|
+
priced at KIE's flat rate per generation: $0.06 for music and $0.0125 for
|
|
60
|
+
sounds. Their status polls settle the row: a lyrics-only result stays
|
|
61
|
+
pending, and a failed generation is not counted as spend.
|
|
62
|
+
- KIE generations stay pending in the usage counter until their task
|
|
63
|
+
settles. A queuing or generating KIE task, or a Veo task that is still
|
|
64
|
+
generating, was counted as settled spend at its first status poll, and a
|
|
65
|
+
Veo generation that failed while generating (`successFlag` 3) was counted
|
|
66
|
+
as completed spend.
|
|
67
|
+
- A Veo generation that fails while generating now fails with KIE's error
|
|
68
|
+
message at the next status poll. It was treated as still processing and
|
|
69
|
+
polled until the pending-task timeout.
|
|
70
|
+
|
|
71
|
+
## [0.3.1] - 2026-10-25
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- KIE Kling 4.0 reference-to-video and keyframe-to-video, 3 to 30 seconds at
|
|
76
|
+
720p or 1080p. Reference-to-video takes up to ten images and five videos,
|
|
77
|
+
and generates from the prompt alone when none are connected. Keyframe-to-video
|
|
78
|
+
takes a first frame, up to eight middle keyframes and a last frame, as many
|
|
79
|
+
as the duration allows. Prompts name connected media the way Kling 4.0 does:
|
|
80
|
+
`@image_1`, `@video_1`, `@first_frame`, `@keyframe_1` and `@last_frame`.
|
|
81
|
+
- KIE PixVerse V6 Max text-to-video and image-to-video, 1 to 15 seconds at
|
|
82
|
+
360p to 1080p, with optional generated audio and a seed. Image-to-video
|
|
83
|
+
follows the image's aspect ratio.
|
|
84
|
+
- KIE OmniHuman 2.0, which animates one image to a driving audio track at 720p
|
|
85
|
+
for as long as the audio runs, with an optional prompt of up to 300
|
|
86
|
+
characters. KIE's watermark stays on by default (`enable_watermark`).
|
|
87
|
+
- The KIE Grok Imagine Video 1.5 preview, the KIE Grok Imagine endpoint that
|
|
88
|
+
takes first and last frames, a loop image (`loop_url`) and up to four timed
|
|
89
|
+
`keyframes`. It shares the Grok Imagine Video 1.5 image-to-video frontier
|
|
90
|
+
ranking. Kling 4.0, PixVerse V6 Max and OmniHuman 2.0 are on no top-10 board
|
|
91
|
+
and appear in the unranked group.
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
- Upgrade all `@apicity/*` dependencies to 0.18.1, which prices the new KIE
|
|
96
|
+
models. Its endpoint catalog now lists fal `v1.queue.status` and
|
|
97
|
+
`v1.queue.result`, with the other fal SDK methods written as method
|
|
98
|
+
shorthand, so `apicity har report` from `@apicity/cli` 0.18.1 can resolve the
|
|
99
|
+
fal polling captures Videobook writes. The requests sent to fal are
|
|
100
|
+
unchanged.
|
|
101
|
+
- Image and video models default to a 12-second duration (or the nearest
|
|
102
|
+
allowed length), a 9:16 aspect ratio and their highest resolution, quality
|
|
103
|
+
and bitrate. Each notebook remembers the last duration, aspect ratio,
|
|
104
|
+
resolution and quality a cell used, and applies them to new cells and model
|
|
105
|
+
switches. Goals saved before this change keep their 2K scene; new goals use
|
|
106
|
+
4K.
|
|
107
|
+
- On desktop, Settings shows AI providers in one column and accounts, storage
|
|
108
|
+
and diagnostics in another; on phones the columns stack behind section jump
|
|
109
|
+
buttons. The chat input stays a plain bar on Settings.
|
|
110
|
+
- The Library opens from an Assets button in the notebook toolbar instead of
|
|
111
|
+
the header, and Arrange is renamed Clean.
|
|
112
|
+
- Library browsing pages assets before building their rows and no longer runs
|
|
113
|
+
ffprobe while listing; proxy checks and duration backfills run as background
|
|
114
|
+
jobs.
|
|
115
|
+
- A Post can take its published link without being saved to iPhone or Dropbox
|
|
116
|
+
first.
|
|
117
|
+
|
|
118
|
+
### Fixed
|
|
119
|
+
|
|
120
|
+
- HAR captures of KIE's nested Veo and Suno SDK calls are named by their
|
|
121
|
+
apicity catalog dot paths, such as `api.v1.veo.generate` and
|
|
122
|
+
`api.v1.generate.sounds`, instead of their SDK paths, so `apicity har report`
|
|
123
|
+
resolves them. Sub-provider prefixes that are URL segments stay:
|
|
124
|
+
`kie.claude.post.v1.messages` is named `claude.v1.messages`.
|
|
125
|
+
- A generation that fails before reaching its provider, such as a request the
|
|
126
|
+
schema refuses, is captured under the provider's catalog endpoint, and fal,
|
|
127
|
+
Alibaba and KIE file uploads are captured as `storage.upload.initiate`,
|
|
128
|
+
`api.v1.uploads` and `api.fileStreamUpload`, so `apicity har report` resolves
|
|
129
|
+
them.
|
|
130
|
+
- A notebook generation whose provider fails after accepting the request now
|
|
131
|
+
marks its plan failed with the provider error. `list_notebook_generation_plans`
|
|
132
|
+
and the Jobs screen kept reporting such plans as awaiting output.
|
|
133
|
+
- v3 goals saved with a 2K scene load again and keep that resolution when they
|
|
134
|
+
are edited, approved or retried.
|
|
135
|
+
- Image subject edits check the provider key before uploading the source image,
|
|
136
|
+
so a missing or placeholder key no longer sends the image to a public file
|
|
137
|
+
host for a request that cannot run.
|
|
138
|
+
- Settings loads when the page opens on it, or when it opens before the MCP
|
|
139
|
+
connection is ready.
|
|
140
|
+
- The multyculty workflow and video poster recovery tests no longer time out on
|
|
141
|
+
loaded hosts, and the chat-composer browser test no longer reads the runner's
|
|
142
|
+
`.env` or provider keys and matches the ranked model picker.
|
|
143
|
+
|
|
9
144
|
## [0.3.0] - 2026-10-24
|
|
10
145
|
|
|
11
146
|
### Added
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { findAudioFile, findPrimaryMediaFile, findVideoFile } from "videobook-engine";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
import { queue } from "./queue.js";
|
|
5
|
+
import { scheduleMediaDurationBackfill, scheduleVideoProxyAssessment, } from "./jobs/assess-media-probes.js";
|
|
6
|
+
import { videoProxyAssessmentMatches, videoProxyMetadataMatches, } from "./video-proxy.js";
|
|
7
|
+
export const PARTIAL_DOWNLOAD_RE = /\.(mp4|mov)\.part$/i;
|
|
8
|
+
const DURATION_PROBE_EXT_RE = /\.(mp4|mov|webm|mkv|mp3|wav|m4a|aac|ogg|flac)$/i;
|
|
9
|
+
const IMAGE_FILE_RE = /\.(jpg|jpeg|png|webp|gif)$/i;
|
|
10
|
+
export function findAssetMediaFile(files, asset) {
|
|
11
|
+
return asset.kind === "audio"
|
|
12
|
+
? findAudioFile(files, asset.kind)
|
|
13
|
+
: asset.kind === "video"
|
|
14
|
+
? findVideoFile(files.filter((file) => file.name !== "proxy.mp4"))
|
|
15
|
+
: findPrimaryMediaFile(files, asset.kind);
|
|
16
|
+
}
|
|
17
|
+
// Content fingerprint for cache-busting media URLs. Pure function of the
|
|
18
|
+
// manifest tuples; partial downloads are excluded because their size grows on
|
|
19
|
+
// every poll while no served URL points at them.
|
|
20
|
+
function computeAssetFingerprint(files) {
|
|
21
|
+
const settled = files.filter((f) => !PARTIAL_DOWNLOAD_RE.test(f.name));
|
|
22
|
+
if (settled.length === 0)
|
|
23
|
+
return "";
|
|
24
|
+
const sorted = [...settled].sort((a, b) => a.name.localeCompare(b.name));
|
|
25
|
+
const hash = createHash("sha1");
|
|
26
|
+
for (const f of sorted) {
|
|
27
|
+
hash.update(f.name);
|
|
28
|
+
hash.update("\0");
|
|
29
|
+
hash.update(String(f.sizeBytes ?? 0));
|
|
30
|
+
hash.update("\0");
|
|
31
|
+
hash.update(f.objectHash);
|
|
32
|
+
hash.update("\0");
|
|
33
|
+
}
|
|
34
|
+
return hash.digest("hex").slice(0, 12);
|
|
35
|
+
}
|
|
36
|
+
export function indexByArtifactId(rows) {
|
|
37
|
+
const map = new Map();
|
|
38
|
+
for (const row of rows)
|
|
39
|
+
map.set(row.artifactId, row);
|
|
40
|
+
return map;
|
|
41
|
+
}
|
|
42
|
+
export function groupByArtifactId(rows) {
|
|
43
|
+
const map = new Map();
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
const list = map.get(row.artifactId);
|
|
46
|
+
if (list)
|
|
47
|
+
list.push(row);
|
|
48
|
+
else
|
|
49
|
+
map.set(row.artifactId, [row]);
|
|
50
|
+
}
|
|
51
|
+
return map;
|
|
52
|
+
}
|
|
53
|
+
function streamSeconds(stream) {
|
|
54
|
+
return stream.durationTicks * stream.timeBase.numerator / stream.timeBase.denominator;
|
|
55
|
+
}
|
|
56
|
+
export function assetStreamProfile(streams, meta) {
|
|
57
|
+
const videoStream = streams.find((stream) => stream.kind === "video" && stream.video);
|
|
58
|
+
const audioStream = streams.find((stream) => stream.kind === "audio" && stream.audio);
|
|
59
|
+
const frameRate = videoStream?.video?.averageFrameRate
|
|
60
|
+
?? videoStream?.video?.nominalFrameRate;
|
|
61
|
+
const streamDuration = videoStream ? streamSeconds(videoStream) : 0;
|
|
62
|
+
const metadataDuration = Number(meta?.duration);
|
|
63
|
+
const sourceDuration = streamDuration > 0
|
|
64
|
+
? streamDuration
|
|
65
|
+
: Number.isFinite(metadataDuration) ? metadataDuration : 0;
|
|
66
|
+
const sourceProfile = sourceDuration > 0
|
|
67
|
+
? {
|
|
68
|
+
videoDuration: sourceDuration,
|
|
69
|
+
avgFps: frameRate ? frameRate.numerator / frameRate.denominator : null,
|
|
70
|
+
hasAudio: audioStream !== undefined || meta?.hasAudio === true,
|
|
71
|
+
}
|
|
72
|
+
: null;
|
|
73
|
+
return { videoStream, audioStream, frameRate, sourceProfile };
|
|
74
|
+
}
|
|
75
|
+
function isActiveWork(work) {
|
|
76
|
+
return work !== null && (work.status === "pending" || work.status === "working");
|
|
77
|
+
}
|
|
78
|
+
export function servedVideoProxyFile(input) {
|
|
79
|
+
if (input.artifact.kind !== "video" || !input.mediaFile)
|
|
80
|
+
return null;
|
|
81
|
+
const proxyFile = input.files.find((file) => file.name === "proxy.mp4");
|
|
82
|
+
if (!proxyFile)
|
|
83
|
+
return null;
|
|
84
|
+
const metadata = input.proxyMetadata;
|
|
85
|
+
if (metadata.ok && videoProxyMetadataMatches(metadata.value, input.mediaFile, proxyFile)) {
|
|
86
|
+
return proxyFile;
|
|
87
|
+
}
|
|
88
|
+
const assessed = metadata.ok
|
|
89
|
+
&& videoProxyAssessmentMatches(metadata.value, input.mediaFile, proxyFile);
|
|
90
|
+
if (!assessed && !isActiveWork(input.work) && input.sourceProfile) {
|
|
91
|
+
scheduleVideoProxyAssessment({
|
|
92
|
+
bookId: input.bookId,
|
|
93
|
+
artifactId: input.artifact.artifactId,
|
|
94
|
+
sourceFile: input.mediaFile,
|
|
95
|
+
proxyFile,
|
|
96
|
+
sourceProfile: input.sourceProfile,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
export function assetDuration(input) {
|
|
102
|
+
const stored = input.meta?.duration;
|
|
103
|
+
if (typeof stored === "number")
|
|
104
|
+
return stored;
|
|
105
|
+
const stream = input.profile.videoStream ?? input.profile.audioStream;
|
|
106
|
+
const measured = stream ? streamSeconds(stream) : 0;
|
|
107
|
+
if (measured > 0)
|
|
108
|
+
return measured;
|
|
109
|
+
if (input.meta && input.mediaFile && DURATION_PROBE_EXT_RE.test(input.mediaFile.name)) {
|
|
110
|
+
scheduleMediaDurationBackfill({
|
|
111
|
+
bookId: input.bookId,
|
|
112
|
+
artifactId: input.artifactId,
|
|
113
|
+
mediaFile: input.mediaFile,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function mediaCacheBust(kind, files, mediaFile) {
|
|
119
|
+
if (kind === "video" || kind === "image") {
|
|
120
|
+
const fingerprint = computeAssetFingerprint(files);
|
|
121
|
+
return fingerprint ? `?v=${fingerprint}` : "";
|
|
122
|
+
}
|
|
123
|
+
const mediaHash = mediaFile?.objectHash;
|
|
124
|
+
return mediaHash ? `?v=${mediaHash.slice(0, 12)}` : "";
|
|
125
|
+
}
|
|
126
|
+
export function assetMediaUrls(input) {
|
|
127
|
+
const { artifact, files } = input;
|
|
128
|
+
const base = `/files/${input.bookId}/${artifact.artifactId}`;
|
|
129
|
+
const cacheBust = mediaCacheBust(artifact.kind, files, input.mediaFile);
|
|
130
|
+
const characterImage = artifact.kind === "character"
|
|
131
|
+
? files.find((file) => IMAGE_FILE_RE.test(file.name)) ?? null
|
|
132
|
+
: null;
|
|
133
|
+
const served = characterImage ?? input.proxyFile ?? input.mediaFile;
|
|
134
|
+
const media_url = served ? `${base}/${served.name}${cacheBust}` : null;
|
|
135
|
+
if (characterImage)
|
|
136
|
+
return { media_url, thumbnail_url: media_url };
|
|
137
|
+
const hasThumbnail = files.some((file) => file.name === "original.jpg");
|
|
138
|
+
const thumbnail_url = hasThumbnail
|
|
139
|
+
? `${base}/original.jpg${cacheBust}`
|
|
140
|
+
: artifact.kind === "image" ? media_url : null;
|
|
141
|
+
return { media_url, thumbnail_url };
|
|
142
|
+
}
|
|
143
|
+
export async function scheduleMissingVideoPoster(engine, input) {
|
|
144
|
+
const { artifact, mediaFile } = input;
|
|
145
|
+
if (artifact.kind !== "video" || input.status !== "ready" || !mediaFile)
|
|
146
|
+
return;
|
|
147
|
+
if (input.files.some((file) => file.name === "original.jpg"))
|
|
148
|
+
return;
|
|
149
|
+
const recovery = await engine.metadata.artifacts.read(artifact.artifactId, "poster-recovery");
|
|
150
|
+
if (recovery.ok && recovery.value.failed && recovery.value.sourceHash === mediaFile.objectHash)
|
|
151
|
+
return;
|
|
152
|
+
void queue.enqueue(`extract-video-poster-${input.bookId}-${artifact.artifactId}-${mediaFile.objectHash}`, {
|
|
153
|
+
type: "extract_video_poster",
|
|
154
|
+
bookId: input.bookId,
|
|
155
|
+
posterArtifactId: artifact.artifactId,
|
|
156
|
+
sourceFilename: mediaFile.name,
|
|
157
|
+
sourceHash: mediaFile.objectHash,
|
|
158
|
+
}).catch((error) => {
|
|
159
|
+
logger.error("poster-enqueue-failed", { assetId: artifact.artifactId, error });
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -90,7 +90,7 @@ export function renderGenerationModels() {
|
|
|
90
90
|
|
|
91
91
|
Generated by \`npm run docs:commands\` from the media catalog (\`server/src/media/catalog.ts\`). Call \`generate_image\`, \`generate_video\` or \`generate_audio\` with one of these model ids; \`list_generation_models\` returns the same list with every option, enum, range and default read from the apicity schemas.
|
|
92
92
|
|
|
93
|
-
Image and video pickers rank exact catalog endpoints using the committed top-10 snapshots in @apicity/cost 0.
|
|
93
|
+
Image and video pickers rank exact catalog endpoints using the committed top-10 snapshots in @apicity/cost 0.18.1, originally sourced from Artificial Analysis. No leaderboard is fetched at runtime. Rankings are per operation: t2i, i2i, t2v, i2v, v2v, and r2v (derived from the text/image-to-video boards). Each ranked option shows rank, Elo, and a bundled price label; prices are indicative ranges, not a quote for the selected settings.
|
|
94
94
|
|
|
95
95
|
Configured frontier models appear first; other models retain their existing order in a separate group. Providers without credentials are marked unavailable. Saved selections and defaults stay unchanged. Catalog variants on a frontier board are included even if previously hidden; non-frontier hidden variants remain hidden. A model version or lite variant never inherits another endpoint's score.
|
|
96
96
|
|
|
@@ -98,6 +98,8 @@ Kling 4 supports text, frames, references and video editing; it appears in the u
|
|
|
98
98
|
|
|
99
99
|
HeyGen Avatar 3 and 4 digital twins are text-to-video with a stock avatar (Abigail Office Front) and voice (Ivy); the prompt is the spoken script. Avatar 5 uses the SDK defaults Abigail Sofa Front and Warm Pro Narrator. Avatar 4 image-to-video animates an image with the script and defaults to Ivy. Connected audio overrides speech synthesis. Video Agent v2 and v3 accept a production prompt. HeyGen lipsync, translation, filler-word removal, HeyGen Voice TTS and Ideogram vector endpoints are outside this catalog addition.
|
|
100
100
|
|
|
101
|
+
Kie Kling 4.0 reference-to-video and keyframe-to-video, PixVerse V6 Max text-to-video and image-to-video, and OmniHuman 2.0 are on no top-10 board, so they appear in the unranked group. Kling 4.0 prompts name connected media as Kling 4.0 documents them: \`@image_1\` and \`@video_1\` for references, \`@first_frame\`, \`@keyframe_1\` and \`@last_frame\` for frames; with no media connected, both endpoints generate from the prompt alone. PixVerse V6 Max image-to-video follows the image's aspect ratio. OmniHuman 2.0 renders as long as its driving audio. The Kie Grok Imagine Video 1.5 preview takes first and last frames, a loop image (\`loop_url\`) and up to four timed \`keyframes\`, and shares the Grok Imagine Video 1.5 image-to-video ranking.
|
|
102
|
+
|
|
101
103
|
Top-10 coverage policy: every registry endpoint must map exactly to a catalog entry or appear in the known gaps below. Models with no provider endpoint in the registry: ${unmapped.join(", ")}. Catalog gaps do not imply provider API support is missing; adding those endpoints is separate work.
|
|
102
104
|
|
|
103
105
|
| Known endpoint gap | Reason |
|
package/dist/diagnostics.js
CHANGED
|
@@ -14,10 +14,11 @@ const EVENTS = [
|
|
|
14
14
|
"process-video-enqueue-failed", "waveform-write-failed", "timeline-reencode",
|
|
15
15
|
"pad-video-started", "video-analysis-failed", "description-truncated",
|
|
16
16
|
"description-cache-failed", "watermark-placement-fallback", "waveform-enqueue-failed",
|
|
17
|
-
"poster-enqueue-failed",
|
|
17
|
+
"poster-enqueue-failed", "proxy-assessment-enqueue-failed", "duration-backfill-enqueue-failed",
|
|
18
18
|
"watermark-probe-fallback", "generation-error-clear-failed", "timeline-orientation-failed",
|
|
19
19
|
"generation-cost-estimate", "job-error", "semantic-index-deferred",
|
|
20
20
|
"tag-asset-failed", "voice-session-closed", "alive-captions-regions-failed",
|
|
21
|
+
"model-unresolvable",
|
|
21
22
|
];
|
|
22
23
|
const events = new Set(EVENTS);
|
|
23
24
|
const errorCodes = new Set([
|
|
@@ -25,9 +26,14 @@ const errorCodes = new Set([
|
|
|
25
26
|
"EIO", "EMFILE", "ENFILE", "ENOMEM", "ECONNREFUSED", "ECONNRESET",
|
|
26
27
|
"ETIMEDOUT", "EPIPE", "ENOTFOUND", "EADDRINUSE", "EADDRNOTAVAIL", "ABORT_ERR",
|
|
27
28
|
]);
|
|
28
|
-
const names = { tool: new Set(), jobType: new Set() };
|
|
29
|
+
const names = { tool: new Set(), jobType: new Set(), model: new Set() };
|
|
30
|
+
const NAME_PATTERNS = {
|
|
31
|
+
tool: /^[a-z][a-z0-9_]{0,95}$/,
|
|
32
|
+
jobType: /^[a-z][a-z0-9_]{0,95}$/,
|
|
33
|
+
model: /^[a-z][a-z0-9-]{0,31}:[a-z0-9][a-z0-9._/-]{0,159}$/,
|
|
34
|
+
};
|
|
29
35
|
export function registerDiagnosticName(kind, name) {
|
|
30
|
-
if (typeof name === "string" &&
|
|
36
|
+
if (typeof name === "string" && NAME_PATTERNS[kind].test(name))
|
|
31
37
|
names[kind].add(name);
|
|
32
38
|
}
|
|
33
39
|
function ownValue(value, key) {
|
|
@@ -82,7 +88,7 @@ export function diagnosticRecord(event, fields = {}) {
|
|
|
82
88
|
if (typeof value === "string" && ENUM_FIELDS[key].includes(value))
|
|
83
89
|
result[key] = value;
|
|
84
90
|
}
|
|
85
|
-
for (const key of ["tool", "jobType"]) {
|
|
91
|
+
for (const key of ["tool", "jobType", "model"]) {
|
|
86
92
|
const value = ownValue(fields, key);
|
|
87
93
|
if (typeof value === "string" && names[key].has(value))
|
|
88
94
|
result[key] = value;
|
|
@@ -65,6 +65,25 @@ export function rewriteSeedanceAssetMentions(prompt, refs) {
|
|
|
65
65
|
}
|
|
66
66
|
return replaceNotebookMentions(prompt, replacements);
|
|
67
67
|
}
|
|
68
|
+
function numberedMentions(refs, marker) {
|
|
69
|
+
return refMentionEntries(refs).map(({ ref, index }) => ({
|
|
70
|
+
reference: ref,
|
|
71
|
+
replacement: `@${marker}_${index + 1}`,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
export function rewriteKling4ReferenceMentions(prompt, imageRefs, videoRefs) {
|
|
75
|
+
return replaceNotebookMentions(prompt, [
|
|
76
|
+
...numberedMentions(imageRefs, "image"),
|
|
77
|
+
...numberedMentions(videoRefs, "video"),
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
export function rewriteKling4FrameMentions(prompt, frames) {
|
|
81
|
+
return replaceNotebookMentions(prompt, [
|
|
82
|
+
...frames.first.slice(0, 1).map((reference) => ({ reference, replacement: "@first_frame" })),
|
|
83
|
+
...numberedMentions(frames.keyframes, "keyframe"),
|
|
84
|
+
...frames.last.slice(0, 1).map((reference) => ({ reference, replacement: "@last_frame" })),
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
68
87
|
export function rewriteViduReferenceMentions(prompt, imageRefs, audioRefs) {
|
|
69
88
|
return replaceNotebookMentions(prompt, [
|
|
70
89
|
...refMentionEntries(imageRefs).map(({ ref, index }) => ({
|
package/dist/goal-plan-v3.js
CHANGED
|
@@ -4,6 +4,13 @@ import { goalPromptWords } from "./goal-prompt-budget.js";
|
|
|
4
4
|
import { LIGHTNING_COLD_OPEN_SECONDS, LIGHTNING_SCENE_ASPECT_RATIO, LIGHTNING_SCENE_MODEL, LIGHTNING_SCENE_OUTPUT_FORMAT, LIGHTNING_SCENE_PROMPT_WORDS, LIGHTNING_SCENE_RESOLUTION, LIGHTNING_VIDEO_DURATION_SECONDS, LIGHTNING_VIDEO_GENERATE_AUDIO, LIGHTNING_VIDEO_OUTPUT_FORMAT, LIGHTNING_VIDEO_PROMPT_WORDS, LIGHTNING_VIDEO_RESOLUTION, LIGHTNING_VIDEO_MODEL, LIGHTNING_WATERMARK_PROFILE, preservedPrompt, } from "./lightning-defaults.js";
|
|
5
5
|
const IMAGE_LIMIT = 30;
|
|
6
6
|
const VIDEO_LIMIT = 10;
|
|
7
|
+
const LEGACY_SCENE_RESOLUTION = "2K";
|
|
8
|
+
const SCENE_MODELS = ["kie:nano-banana-2-1"];
|
|
9
|
+
const SCENE_RESOLUTIONS = [LEGACY_SCENE_RESOLUTION, "4K"];
|
|
10
|
+
const SCENE_OUTPUT_FORMATS = ["jpg"];
|
|
11
|
+
const VIDEO_MODELS = ["kie:bytedance/seedance-2-5"];
|
|
12
|
+
const VIDEO_RESOLUTIONS = ["1080p"];
|
|
13
|
+
const VIDEO_OUTPUT_FORMATS = ["mp4"];
|
|
7
14
|
export const goalMediaRefSchema = z.object({
|
|
8
15
|
kind: z.enum(["image", "video"]),
|
|
9
16
|
artifactId: z.string().min(1),
|
|
@@ -20,21 +27,21 @@ export const goalPlanV3Schema = z.object({
|
|
|
20
27
|
scene: z.object({
|
|
21
28
|
prompt: z.string().trim().min(1).max(4000),
|
|
22
29
|
tool: z.literal("generate_image"),
|
|
23
|
-
model: z.
|
|
30
|
+
model: z.enum(SCENE_MODELS),
|
|
24
31
|
aspectRatio: z.enum(["16:9", "9:16", "1:1"]),
|
|
25
|
-
resolution: z.
|
|
26
|
-
outputFormat: z.
|
|
32
|
+
resolution: z.enum(SCENE_RESOLUTIONS),
|
|
33
|
+
outputFormat: z.enum(SCENE_OUTPUT_FORMATS),
|
|
27
34
|
imageRefs: z.array(goalMediaRefSchema).max(IMAGE_LIMIT),
|
|
28
35
|
}).strict(),
|
|
29
36
|
video: z.object({
|
|
30
37
|
prompt: z.string().trim().min(1).max(4000),
|
|
31
38
|
tool: z.literal("generate_video"),
|
|
32
|
-
model: z.
|
|
39
|
+
model: z.enum(VIDEO_MODELS),
|
|
33
40
|
aspectRatio: z.enum(["16:9", "9:16", "1:1", "adaptive"]),
|
|
34
|
-
resolution: z.
|
|
41
|
+
resolution: z.enum(VIDEO_RESOLUTIONS),
|
|
35
42
|
durationSeconds: z.number().int().min(4).max(30),
|
|
36
43
|
generateAudio: z.literal(true),
|
|
37
|
-
outputFormat: z.
|
|
44
|
+
outputFormat: z.enum(VIDEO_OUTPUT_FORMATS),
|
|
38
45
|
imageRefs: z.array(goalMediaRefSchema).max(IMAGE_LIMIT),
|
|
39
46
|
videoRefs: z.array(goalMediaRefSchema).max(VIDEO_LIMIT),
|
|
40
47
|
}).strict(),
|
|
@@ -59,6 +66,14 @@ export const goalPlanV3Schema = z.object({
|
|
|
59
66
|
}).strict(),
|
|
60
67
|
}).strict(),
|
|
61
68
|
}).strict();
|
|
69
|
+
function withLegacySceneResolution(plan) {
|
|
70
|
+
if (typeof plan !== "object" || plan === null || !("version" in plan) || plan.version !== 3)
|
|
71
|
+
return plan;
|
|
72
|
+
if (!("scene" in plan) || typeof plan.scene !== "object" || plan.scene === null || "resolution" in plan.scene)
|
|
73
|
+
return plan;
|
|
74
|
+
return { ...plan, scene: { ...plan.scene, resolution: LEGACY_SCENE_RESOLUTION } };
|
|
75
|
+
}
|
|
76
|
+
export const savedGoalPlanV3Schema = z.preprocess(withLegacySceneResolution, goalPlanV3Schema);
|
|
62
77
|
export function isGoalPlanV3(plan) {
|
|
63
78
|
return plan?.version === 3;
|
|
64
79
|
}
|
package/dist/goal-plan.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { LIGHTNING_VIDEO_MODEL } from "./lightning-defaults.js";
|
|
3
3
|
import { GOAL_PROMPT_BUDGET, goalPromptWords } from "./goal-prompt-budget.js";
|
|
4
4
|
import { GOAL_STYLE_IDS, goalStyleFragment, resolveGoalStyleOptions } from "./goal-styles.js";
|
|
5
|
-
export { compileGoalV3Approval, compileGoalV3Steps, goalMediaRefSchema, goalPlanV3Schema, isGoalPlanV3, refreshGoalPlanV3 } from "./goal-plan-v3.js";
|
|
5
|
+
export { compileGoalV3Approval, compileGoalV3Steps, goalMediaRefSchema, goalPlanV3Schema, isGoalPlanV3, refreshGoalPlanV3, savedGoalPlanV3Schema } from "./goal-plan-v3.js";
|
|
6
6
|
export { GOAL_PROMPT_BUDGET, goalPromptWords } from "./goal-prompt-budget.js";
|
|
7
7
|
export const GOAL_VIDEO_TOOL = "generate_video";
|
|
8
8
|
export const GOAL_VIDEO_MODEL = LIGHTNING_VIDEO_MODEL;
|
package/dist/goal-store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { firstEmptyNotebookGridSlot, notebookOccupiedSlots } from "videobook-engine/notebook-grid";
|
|
3
|
-
import { goalMediaRefSchema, goalPlanSchema,
|
|
3
|
+
import { goalMediaRefSchema, goalPlanSchema, goalStyleSchema, legacyGoalPlanSchema, savedGoalPlanV3Schema } from "./goal-plan.js";
|
|
4
4
|
import { mutateNotebook } from "./notebook-mutations.js";
|
|
5
5
|
import { arrangeGoalNotebook } from "./notebook-goal-layout.js";
|
|
6
6
|
const KEY = "application.goals.v2";
|
|
@@ -68,7 +68,7 @@ function mediaFrom(value) {
|
|
|
68
68
|
}
|
|
69
69
|
export function goalContent(cell) {
|
|
70
70
|
const inputs = cell.inputs ?? {};
|
|
71
|
-
const plan = z.union([
|
|
71
|
+
const plan = z.union([savedGoalPlanV3Schema, goalPlanSchema, legacyGoalPlanSchema]).safeParse(inputs.plan);
|
|
72
72
|
const requestedStyle = goalStyleSchema.safeParse(inputs.requestedStyle);
|
|
73
73
|
const media = mediaFrom(inputs.media);
|
|
74
74
|
return {
|
package/dist/har-capture.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createHarCapture } from "@apicity/har";
|
|
|
4
4
|
import { currentBookContext } from "./book-context.js";
|
|
5
5
|
import { harDirectory, readHarSettings } from "./har-settings.js";
|
|
6
6
|
import { logger } from "./logger.js";
|
|
7
|
+
const localFailures = new AsyncLocalStorage();
|
|
7
8
|
const pending = new Set();
|
|
8
9
|
let maintenance = Promise.resolve();
|
|
9
10
|
export async function maintainHarCaptures(operation) {
|
|
@@ -51,11 +52,36 @@ export async function withHarCapture(meta, operation, base = globalThis.fetch, c
|
|
|
51
52
|
try {
|
|
52
53
|
return await runCapture(meta, operation, base, context);
|
|
53
54
|
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
localFailures.getStore()?.add(error);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
54
59
|
finally {
|
|
55
60
|
finish();
|
|
56
61
|
pending.delete(finished);
|
|
57
62
|
}
|
|
58
63
|
}
|
|
64
|
+
export async function withHarLocalFailure(meta, operation) {
|
|
65
|
+
const failures = localFailures.getStore() ?? new Set();
|
|
66
|
+
return localFailures.run(failures, async () => {
|
|
67
|
+
try {
|
|
68
|
+
return await operation();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
let cause = error;
|
|
73
|
+
while (!seen.has(cause)) {
|
|
74
|
+
if (failures.has(cause))
|
|
75
|
+
throw error;
|
|
76
|
+
seen.add(cause);
|
|
77
|
+
if (!(cause instanceof Error) || cause.cause === undefined)
|
|
78
|
+
break;
|
|
79
|
+
cause = cause.cause;
|
|
80
|
+
}
|
|
81
|
+
return withHarCapture(meta, () => { throw error; });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
59
85
|
async function runCapture(meta, operation, base, context) {
|
|
60
86
|
const settings = await readHarSettings();
|
|
61
87
|
if (settings.mode === "off")
|
|
@@ -99,8 +125,15 @@ async function runCapture(meta, operation, base, context) {
|
|
|
99
125
|
}
|
|
100
126
|
}
|
|
101
127
|
const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
const SDK_ONLY_NAMESPACES = new Map([["kie", new Set(["veo", "suno"])]]);
|
|
129
|
+
function endpointDotPath(provider, path) {
|
|
130
|
+
const [head = "", next = ""] = path;
|
|
131
|
+
if (path.length > 1 && HTTP_METHODS.has(head))
|
|
132
|
+
return path.slice(1).join(".");
|
|
133
|
+
if (path.length < 3 || !HTTP_METHODS.has(next))
|
|
134
|
+
return path.join(".");
|
|
135
|
+
const rest = path.slice(2);
|
|
136
|
+
return (SDK_ONLY_NAMESPACES.get(provider)?.has(head) ? rest : [head, ...rest]).join(".");
|
|
104
137
|
}
|
|
105
138
|
const providerCalls = new WeakMap();
|
|
106
139
|
export function withProviderCall(provider, meta, operation) {
|
|
@@ -119,7 +152,7 @@ export function capturedProvider(provider, factory, base = globalThis.fetch) {
|
|
|
119
152
|
function wrap(target, path) {
|
|
120
153
|
return new Proxy(target, {
|
|
121
154
|
apply(fn, receiver, args) {
|
|
122
|
-
return withHarCapture({ provider, endpoint: endpointDotPath(path), input: args.length === 1 ? args[0] : args }, (fetch) => transports.run(fetch, () => Reflect.apply(fn, receiver, args)), base);
|
|
155
|
+
return withHarCapture({ provider, endpoint: endpointDotPath(provider, path), input: args.length === 1 ? args[0] : args }, (fetch) => transports.run(fetch, () => Reflect.apply(fn, receiver, args)), base);
|
|
123
156
|
},
|
|
124
157
|
get(object, property, receiver) {
|
|
125
158
|
const value = Reflect.get(object, property, receiver);
|
|
@@ -99,6 +99,7 @@ function kieImageEditAdapter(model, build) {
|
|
|
99
99
|
});
|
|
100
100
|
return {
|
|
101
101
|
id: model.apiId,
|
|
102
|
+
envKey: "KIE_API_KEY",
|
|
102
103
|
uploadAsset: (engine, artifactId, filename) => uploadAssetToKie(engine, artifactId, filename),
|
|
103
104
|
buildSemanticRequest: (input) => buildRequest({ ...input, imageUrls: [input.imageUrls[0]] }),
|
|
104
105
|
buildRegionRequest: buildRequest,
|
|
@@ -166,6 +167,7 @@ function falImageEditAdapter(model) {
|
|
|
166
167
|
const buildRequest = (input) => toProviderRequest(model.apiId, catalogEditRequest(model, input));
|
|
167
168
|
return {
|
|
168
169
|
id: model.apiId,
|
|
170
|
+
envKey: "FAL_API_KEY",
|
|
169
171
|
uploadAsset: (engine, artifactId, filename) => uploadAssetToFal(engine, artifactId, filename),
|
|
170
172
|
buildSemanticRequest: (input) => buildRequest({ ...input, imageUrls: [input.imageUrls[0]] }),
|
|
171
173
|
buildRegionRequest: buildRequest,
|
package/dist/jobs/_kie-upload.js
CHANGED
|
@@ -2,6 +2,7 @@ import { uploadKieFile } from "../usage-providers.js";
|
|
|
2
2
|
import { prepareAssetForUpload, uploadBytesToFreeHost, } from "../upload.js";
|
|
3
3
|
import { getApiKey } from "../api-key.js";
|
|
4
4
|
import { logger } from "../logger.js";
|
|
5
|
+
import { providerEndpoints } from "../media/endpoints.js";
|
|
5
6
|
// Upload prepared bytes to Kie's native storage via POST /api/file-stream-upload.
|
|
6
7
|
// Kie workers run in China — using their first-party CDN avoids the
|
|
7
8
|
// InvalidParameter.DataInspection failures we'd hit when the free-host URL
|
|
@@ -12,7 +13,8 @@ async function uploadPreparedToKie(data, hashedFilename, contentType) {
|
|
|
12
13
|
if (apiKey) {
|
|
13
14
|
try {
|
|
14
15
|
const blob = new Blob([new Uint8Array(data)], { type: contentType });
|
|
15
|
-
|
|
16
|
+
const uploadBaseURL = providerEndpoints().kieUploadBaseURL;
|
|
17
|
+
return await uploadKieFile({ apiKey, timeout: 120_000, ...(uploadBaseURL ? { uploadBaseURL } : {}) }, blob, hashedFilename);
|
|
16
18
|
}
|
|
17
19
|
catch (err) {
|
|
18
20
|
logger.warn("kie-upload-fallback", { error: err });
|