videobook 0.2.1 → 0.2.3
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 +41 -0
- package/README.md +7 -8
- package/dist/book-manager.js +40 -2
- package/dist/chat-router.js +1 -1
- package/dist/env-secrets.js +1 -0
- package/dist/guides/seedance_2_prompting_guide.md +1 -1
- package/dist/image-edit-models.js +2 -0
- package/dist/jobs/generate-media.js +18 -6
- package/dist/jobs/run-notebook.js +23 -4
- package/dist/media/adapters/fal.js +7 -0
- package/dist/media/catalog.js +17 -2
- package/dist/media/cost.js +80 -7
- package/dist/media/json-schema.js +7 -0
- package/dist/media/ref-fields.js +11 -3
- package/dist/media/refs.js +13 -0
- package/dist/media/request.js +56 -0
- package/dist/media/seedance-edit.js +9 -0
- package/dist/provider-usage.js +22 -8
- package/dist/tools/estimate-cost.js +8 -5
- package/dist/tools/generate-media.js +2 -1
- package/dist/tools/provider-key-status.js +2 -0
- package/dist/tools/reconcile-project-work.js +2 -1
- package/package.json +13 -13
- package/ui/assets/{alive-captions-e-n3xmCd.js → alive-captions-4QySEGdt.js} +1 -1
- package/ui/assets/confirm-bodies-Bw818V9J.js +1 -0
- package/ui/assets/{fullscreen-VY7f4mJ-.js → fullscreen-CzTnPPG5.js} +1 -1
- package/ui/assets/{goal-ui-B__NgZYz.js → goal-ui-CosrK_ko.js} +2 -2
- package/ui/assets/index-C9xjFEGL.js +37 -0
- package/ui/assets/{inspector-BBuZIaqg.js → inspector-BN2fezh4.js} +1 -1
- package/ui/assets/{jobs-ui-vmEH4zBT.js → jobs-ui-C4Wgy6I1.js} +1 -1
- package/ui/assets/{music-video-CklLKD1R.js → music-video-BSj8SgbQ.js} +1 -1
- package/ui/assets/ui-Cpr6zMTA.js +2 -0
- package/ui/assets/{vendor-flow-DY81i6dC.js → vendor-flow-zBavaNEW.js} +1 -1
- package/ui/assets/{vendor-mcp-BsGNAD4I.js → vendor-mcp-DQ7EusT9.js} +1 -1
- package/ui/assets/vendor-yctIZ9Dz.js +19 -0
- package/ui/assets/videobook-B5G0jMoi.js +20 -0
- package/ui/index.html +11 -11
- package/ui/assets/confirm-bodies-DS6jh0Kb.js +0 -1
- package/ui/assets/index-BPco0HoT.js +0 -37
- package/ui/assets/ui-CtYXY_po.js +0 -2
- package/ui/assets/vendor-OClqq_Da.js +0 -19
- package/ui/assets/videobook-lhLK6e6Q.js +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,47 @@ and the package uses [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.3] - 2026-10-21
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Fal FLUX 3 Max text-to-video, image-to-video, and keyframes-to-video,
|
|
14
|
+
with generated audio and resolution-tiered per-second cost estimates.
|
|
15
|
+
- Bria Fibo 2 text-to-image with structured VGL prompts, and Fibo 2 Edit
|
|
16
|
+
for one to four reference images, including the image editor.
|
|
17
|
+
- H3 Max Reframe Video, which outpaints a source video to a target aspect
|
|
18
|
+
ratio at 480p, 768p, or 1080p while keeping its audio and timing.
|
|
19
|
+
Priced from fal's historical average because the request does not fix
|
|
20
|
+
the billed duration.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Upgrade all application `@apicity/*` dependencies to 0.16.3.
|
|
25
|
+
- Windows x64 is supported. The release check runs the pack-and-install
|
|
26
|
+
smoke on Windows and blocks the release when it fails. Stopping
|
|
27
|
+
`videobook serve` from another process still ends it without a clean
|
|
28
|
+
shutdown, because Windows has no SIGTERM, and the next start replaces
|
|
29
|
+
the lock file.
|
|
30
|
+
- Dynamic fal requests are priced from fal's historical average when
|
|
31
|
+
`FAL_ADMIN_API_KEY` is set. Without the key, and for the two Seedance
|
|
32
|
+
formula gaps, the estimate stays at the local $0.
|
|
33
|
+
|
|
34
|
+
## [0.2.2] - 2026-10-21
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- KIE FLUX 3 Klein text-to-image and image editing, with 1K defaults,
|
|
39
|
+
up to ten edit references, and resolution-aware per-image cost estimates.
|
|
40
|
+
- FLUX 3 Klein Edit in the image editor for whole-image and region edits.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- Upgrade all application `@apicity/*` dependencies to 0.16.2.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- Accept npm 12 workspace pack results in the installed-package smoke gate.
|
|
49
|
+
|
|
9
50
|
## [0.2.1] - 2026-10-20
|
|
10
51
|
|
|
11
52
|
### Changed
|
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ graph, and every change is recorded as a revision. This package installs one
|
|
|
12
12
|
## Requirements
|
|
13
13
|
|
|
14
14
|
- Node.js 22 or newer.
|
|
15
|
-
- macOS (Apple Silicon or Intel)
|
|
16
|
-
|
|
15
|
+
- macOS (Apple Silicon or Intel), Linux (x64 or arm64), or Windows x64.
|
|
16
|
+
See the platform notes.
|
|
17
17
|
- About 800 MB of disk. Installing downloads the ffmpeg and yt-dlp binaries
|
|
18
18
|
from GitHub, and the first video render downloads Chrome Headless Shell
|
|
19
19
|
(about 150 MB) into `~/.cache/videobook/remotion`.
|
|
@@ -121,6 +121,7 @@ Caches live in `~/.cache/videobook`. Other settings: `PORT`,
|
|
|
121
121
|
## Provider keys
|
|
122
122
|
|
|
123
123
|
The keys are `OPENAI_API_KEY`, `KIE_API_KEY`, `XAI_API_KEY`, `FAL_API_KEY`,
|
|
124
|
+
`FAL_ADMIN_API_KEY`,
|
|
124
125
|
`DASHSCOPE_API_KEY`, `ELEVENLABS_API_KEY`, `KIMI_CODING_API_KEY`,
|
|
125
126
|
`X_CLIENT_ID` and `X_CLIENT_SECRET`, `DROPBOX_CLIENT_ID` and
|
|
126
127
|
`DROPBOX_CLIENT_SECRET`, and the `B2_*` storage settings. The server starts
|
|
@@ -177,12 +178,10 @@ come from 1Password are read-only in Settings.
|
|
|
177
178
|
- macOS on Intel works, but ONNX Runtime has no Intel Mac build, so semantic
|
|
178
179
|
search is unavailable there.
|
|
179
180
|
- Linux x64 and arm64 are supported.
|
|
180
|
-
- Windows is
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Release check workflow runs the smoke test on Windows for every release tag
|
|
185
|
-
without blocking the release.
|
|
181
|
+
- Windows x64 is supported. Windows has no SIGTERM, so stopping
|
|
182
|
+
`videobook serve` from another process ends it without a clean shutdown,
|
|
183
|
+
and the next start replaces the lock file it leaves. The Release check
|
|
184
|
+
workflow runs the pack-and-install smoke on Windows for every release tag.
|
|
186
185
|
|
|
187
186
|
## Security
|
|
188
187
|
|
package/dist/book-manager.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { lstat, mkdir, readdir, rename, rm } from "node:fs/promises";
|
|
2
|
+
import { cp, lstat, mkdir, readdir, rename, rm } from "node:fs/promises";
|
|
3
3
|
import { join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { performance } from "node:perf_hooks";
|
|
5
5
|
import { BOOK_SLUG_ERROR, normalizeBookSlug } from "videobook-engine/book-slug";
|
|
@@ -8,6 +8,43 @@ import { createEngine, err, isUuidV7, ok, } from "videobook-engine";
|
|
|
8
8
|
function engineError(code, message) {
|
|
9
9
|
return { code, message };
|
|
10
10
|
}
|
|
11
|
+
const MOVE_ATTEMPTS = 8;
|
|
12
|
+
const RETRYABLE_MOVE_CODES = new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
13
|
+
function systemCode(cause) {
|
|
14
|
+
if (!cause || typeof cause !== "object" || !("code" in cause))
|
|
15
|
+
return undefined;
|
|
16
|
+
const code = cause.code;
|
|
17
|
+
return typeof code === "string" ? code : undefined;
|
|
18
|
+
}
|
|
19
|
+
function delay(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
// Windows denies renaming a directory while Defender, or a Dolt file that just
|
|
23
|
+
// closed, still holds a handle. Retry, then copy and drop the staging tree.
|
|
24
|
+
async function moveDirectory(source, destination) {
|
|
25
|
+
let last;
|
|
26
|
+
for (let attempt = 0; attempt < MOVE_ATTEMPTS; attempt += 1) {
|
|
27
|
+
try {
|
|
28
|
+
await rename(source, destination);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
catch (cause) {
|
|
32
|
+
last = cause;
|
|
33
|
+
if (!RETRYABLE_MOVE_CODES.has(systemCode(cause) ?? ""))
|
|
34
|
+
throw cause;
|
|
35
|
+
if (attempt < MOVE_ATTEMPTS - 1)
|
|
36
|
+
await delay(50 * 2 ** attempt);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
await cp(source, destination, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
await rm(destination, { recursive: true, force: true }).catch(() => undefined);
|
|
44
|
+
throw last;
|
|
45
|
+
}
|
|
46
|
+
await rm(source, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => undefined);
|
|
47
|
+
}
|
|
11
48
|
function isInside(parent, candidate) {
|
|
12
49
|
const path = relative(parent, candidate);
|
|
13
50
|
return path !== "" && !path.startsWith(`..${sep}`) && path !== "..";
|
|
@@ -114,8 +151,9 @@ export class BookManager {
|
|
|
114
151
|
const created = temporaryEngine.book.get();
|
|
115
152
|
temporaryEngine.close();
|
|
116
153
|
temporaryEngine = null;
|
|
154
|
+
await delay(0);
|
|
117
155
|
const destination = this.bookRoot(created.bookId);
|
|
118
|
-
await
|
|
156
|
+
await moveDirectory(temporaryRoot, destination);
|
|
119
157
|
finalRoot = destination;
|
|
120
158
|
const engine = this.createEngine(finalRoot);
|
|
121
159
|
finalEngine = engine;
|
package/dist/chat-router.js
CHANGED
|
@@ -576,7 +576,7 @@ Parameter guidelines:
|
|
|
576
576
|
- Preserve every consumed notebook @address in the generation prompt at the semantic position where it belongs. Do not strip, rename, or expand notebook references into prose
|
|
577
577
|
- For generate_video: put the compact generation description in "prompt" and preserve every consumed @ reference
|
|
578
578
|
- For generate_image: put the compact image description in "prompt" and preserve every consumed image @address there; use refs.image ONLY for the corresponding resolved asset tags whose asset context type is "image", in the same order (include @ prefix). Never put a video or audio asset tag in refs.image — if the user wants an image inspired by a video, describe that source without connecting it as an image
|
|
579
|
-
- ByteDance naming: Seedance is video, Seedream is image. Users often say Seedance for both. When the query names seedance, bytedance, or seedream: if the edit/replace target is an image, use generate_image with model kie:seedream/5-lite-image-to-image and put the edit-target image first in refs.image, followed by replacement image refs. If the target is a video, use generate_video with model kie:bytedance/seedance-2-5,
|
|
579
|
+
- ByteDance naming: Seedance is video, Seedream is image. Users often say Seedance for both. When the query names seedance, bytedance, or seedream: if the edit/replace target is an image, use generate_image with model kie:seedream/5-lite-image-to-image and put the edit-target image first in refs.image, followed by replacement image refs. If the target is a video, use generate_video with model kie:bytedance/seedance-2-5, mode v2v, one 4–30 second clip in refs.video and replacement stills in refs.image. Edit mode follows the source aspect ratio and duration. Never route an image-target Seedance ask to a video model or to Nano Banana Pro
|
|
580
580
|
- Treat "Seedance 5.0 edit", "Seedance 5", and "Seedream 5" as Seedream 5. Generic chat uses Seedream 5 Lite image-to-image; Seedream 5 Pro edit lives in the subject/region pipeline. When Seedream is used for an image ask that said Seedance, the status message must note that substitution
|
|
581
581
|
- For create_watermark_card (QR code / watermark card): use this tool for real scannable QR codes — never generate_image. Pass url from the referenced asset's source_url/webpage_url when the user says "from the url in @vid-…" or similar; optional handle is an X or TikTok alias without @; set platform to "tiktok" for TikTok or "x" for X. apply_watermark uses the same platform field for its alias, icon, and default QR destination
|
|
582
582
|
- For create_qart (artistic / picture QR): use when turning an image asset into scan-safe color and monochrome QR mosaics; pass image_ref and value (URL/text). Prefer over create_watermark_card when the user wants a photo integrated into the QR modules
|
package/dist/env-secrets.js
CHANGED
|
@@ -85,7 +85,7 @@ Use Seedance 2.0 reference-to-video when replacing people in a clip. Pass the so
|
|
|
85
85
|
Replace the on-screen person matching @Image1 with @Image1 as character reference. Replace the on-screen person matching @Image2 with @Image2 as character reference. Replace the on-screen person matching @Image3 with @Image3 as character reference. Keep camera movement, pacing, and framing of @Video1.
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
Notebook wiring: connect the source video plus the identity stills to a Seedance generation cell (model `kie:bytedance/seedance-2-5`, mode `
|
|
88
|
+
Notebook wiring: connect the source video plus the identity stills to a Seedance generation cell (model `kie:bytedance/seedance-2-5`, mode `v2v` / Edit video). Use one source video with a known duration of 4–30 seconds; import URL sources into the book first. Edit mode sets `aspect_ratio=adaptive` and `duration=-1` to preserve the source shape and length. Ordinary reference generation remains `r2v` with its chosen output settings. The runner fills `refs.video` from connected video cells and `refs.image` from connected image cells. The result is a normal generated video artifact (`original.mp4` / `proxy.mp4`), not an orientation render.
|
|
89
89
|
|
|
90
90
|
## Camera Movement Vocabulary
|
|
91
91
|
|
|
@@ -9,6 +9,7 @@ const STANDARD_IMAGE_EDIT_CAPABILITIES = {
|
|
|
9
9
|
};
|
|
10
10
|
const EDIT_MODELS = [
|
|
11
11
|
{ catalogModel: `kie:${DEFAULT_IMAGE_EDIT_MODEL_ID}`, label: "Nano Banana 2.1 Edit" },
|
|
12
|
+
{ catalogModel: "kie:flux-3/klein-edit-image", label: "FLUX 3 Klein Edit" },
|
|
12
13
|
{ catalogModel: "kie:seedream/5-pro-image-to-image", label: "Seedream 5 Pro" },
|
|
13
14
|
{ catalogModel: "kie:gpt-image-2-5-flare-image-to-image", label: "GPT Image 2.5 Flare" },
|
|
14
15
|
{ catalogModel: "kie:gpt-image-2-5-sunburst-image-to-image", label: "GPT Image 2.5 Sunburst" },
|
|
@@ -17,6 +18,7 @@ const EDIT_MODELS = [
|
|
|
17
18
|
{ catalogModel: "fal:fal-ai/nano-banana-2/edit", label: "Nano Banana 2" },
|
|
18
19
|
{ catalogModel: "fal:alibaba/qwen-image-3/edit", label: "Qwen 3.0 Edit" },
|
|
19
20
|
{ catalogModel: "fal:fal-ai/wan/v2.7/edit", label: "Wan 2.7 Edit" },
|
|
21
|
+
{ catalogModel: "fal:bria/fibo-2/edit", label: "Fibo 2 Edit" },
|
|
20
22
|
];
|
|
21
23
|
function imageSlotMax(model) {
|
|
22
24
|
return model.slots.find((slot) => slot.role === "image")?.max ?? null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSeedanceEdit } from "../media/seedance-edit.js";
|
|
1
2
|
import { registerJobHandler } from "../queue.js";
|
|
2
3
|
import { assertProviderPayload } from "../provider-prompt-transform.js";
|
|
3
4
|
import { requireApiKey } from "../api-key.js";
|
|
@@ -5,7 +6,7 @@ import { referenceVideoDuration } from "../reference-video-duration.js";
|
|
|
5
6
|
import { resolveModel } from "../media/descriptor.js";
|
|
6
7
|
import { buildWireRequest } from "../media/request.js";
|
|
7
8
|
import { applyPromptDialect } from "../media/prompt-dialects.js";
|
|
8
|
-
import { checkInsertVideoTiming, checkRefDurations, resolveRefInput, uploadRefs } from "../media/refs.js";
|
|
9
|
+
import { checkSeedanceEditSource, checkInsertVideoTiming, checkRefDurations, resolveRefInput, uploadRefs } from "../media/refs.js";
|
|
9
10
|
import { adapterFor } from "../media/adapters/index.js";
|
|
10
11
|
import { completeMediaGeneration } from "../media/complete.js";
|
|
11
12
|
function storedPayload(model, request) {
|
|
@@ -17,11 +18,21 @@ async function replaceAudioMeta(engine, ref) {
|
|
|
17
18
|
const input = await resolveRefInput(engine, "audio", ref);
|
|
18
19
|
return { replaceAudioWith: { assetId: input.assetId, filename: input.filename } };
|
|
19
20
|
}
|
|
20
|
-
async function
|
|
21
|
-
|
|
21
|
+
export async function generationSourceCostHints(engine, refs, job) {
|
|
22
|
+
const videoSeconds = await referenceVideoDuration(engine, refs.sourceVideoIds);
|
|
23
|
+
if (videoSeconds !== undefined) {
|
|
24
|
+
return { inputDurationSeconds: videoSeconds, ...(isSeedanceEdit(job.model, job.mode) ? { durationSeconds: videoSeconds } : {}) };
|
|
25
|
+
}
|
|
26
|
+
if (resolveModel(job.model).provider !== "fal")
|
|
22
27
|
return {};
|
|
23
|
-
const
|
|
24
|
-
|
|
28
|
+
for (const assetId of refs.sourceAudioIds) {
|
|
29
|
+
const metadata = await engine.metadata.artifacts.read(assetId, "original");
|
|
30
|
+
const seconds = metadata.ok ? metadata.value.duration : undefined;
|
|
31
|
+
if (typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0) {
|
|
32
|
+
return { inputDurationSeconds: seconds };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
25
36
|
}
|
|
26
37
|
function generationMeta(job, model, refs) {
|
|
27
38
|
return {
|
|
@@ -40,6 +51,7 @@ export async function runGenerateMedia(engine, job, ctx) {
|
|
|
40
51
|
await engine.metadata.artifacts.write(job.assetId, "generation-request", job);
|
|
41
52
|
const prompt = job.prompt === undefined ? undefined : applyPromptDialect(model.entry.dialect, job.prompt, job.refs);
|
|
42
53
|
const durationError = (await checkInsertVideoTiming(engine, model, job.refs, job.options))
|
|
54
|
+
?? (await checkSeedanceEditSource(engine, model, job.refs, job.mode))
|
|
43
55
|
?? await checkRefDurations(engine, model, job.refs);
|
|
44
56
|
if (durationError)
|
|
45
57
|
throw new Error(durationError);
|
|
@@ -58,7 +70,7 @@ export async function runGenerateMedia(engine, job, ctx) {
|
|
|
58
70
|
assetId: job.assetId,
|
|
59
71
|
assetDir: job.assetDir,
|
|
60
72
|
meta,
|
|
61
|
-
costHints: await
|
|
73
|
+
costHints: await generationSourceCostHints(engine, refs, job),
|
|
62
74
|
});
|
|
63
75
|
if (result.kind === "done") {
|
|
64
76
|
await completeMediaGeneration({ engine, assetId: job.assetId, assetDir: job.assetDir, bookId: job.bookId, meta, outputs: result.outputs });
|
|
@@ -14,7 +14,7 @@ import { smoothSceneConcatDef } from "../tools/smooth-scene-concat.js";
|
|
|
14
14
|
import { spliceVideoDef } from "../tools/splice-video.js";
|
|
15
15
|
import { tryResolveModel } from "../media/descriptor.js";
|
|
16
16
|
import { previewGeneration } from "../media/preview.js";
|
|
17
|
-
import { checkInsertVideoTiming, normalizeRefs } from "../media/refs.js";
|
|
17
|
+
import { checkSeedanceEditSource, checkInsertVideoTiming, normalizeRefs } from "../media/refs.js";
|
|
18
18
|
import { REF_ROLES } from "../media/types.js";
|
|
19
19
|
import { captureFrame, concatSourceRanges, exportSequenceMaster, extractSourceAudio, MINIMUM_SPLIT_PIECE_SECONDS, normalizeSceneVideo, splitSourceRange, splitSourceRanges, } from "../notebook-media-ops.js";
|
|
20
20
|
import { analyzeNotebookSource } from "../notebook-live-analysis.js";
|
|
@@ -830,6 +830,9 @@ async function prepareMediaGeneration(model, document, cell, bookId, engine, opt
|
|
|
830
830
|
const timingError = await checkInsertVideoTiming(engine, model, normalizeRefs(prepared.input.refs), preview.preview.payload.input ?? {});
|
|
831
831
|
if (timingError)
|
|
832
832
|
throw new Error(timingError);
|
|
833
|
+
const sourceError = await checkSeedanceEditSource(engine, model, normalizeRefs(prepared.input.refs), prepared.input.mode);
|
|
834
|
+
if (sourceError)
|
|
835
|
+
throw new Error(sourceError);
|
|
833
836
|
}
|
|
834
837
|
return {
|
|
835
838
|
tool: `generate_${model.media}`,
|
|
@@ -2357,7 +2360,7 @@ export async function failQueuedNotebookCells(engine, notebookId, options) {
|
|
|
2357
2360
|
function notebookRunPlanRecords(document) {
|
|
2358
2361
|
return document.notebookRunPlans ?? [];
|
|
2359
2362
|
}
|
|
2360
|
-
async function updateNotebookRunPlanState(engine, notebookId, planId, status, details = {}) {
|
|
2363
|
+
async function updateNotebookRunPlanState(engine, notebookId, planId, status, details = {}, recordedRun) {
|
|
2361
2364
|
const result = await mutateNotebook(engine, notebookId, "update_notebook_run_plan", (document) => ({
|
|
2362
2365
|
...document,
|
|
2363
2366
|
notebookRunPlans: notebookRunPlanRecords(document).map((record) => record.planId === planId
|
|
@@ -2367,6 +2370,17 @@ async function updateNotebookRunPlanState(engine, notebookId, planId, status, de
|
|
|
2367
2370
|
...record,
|
|
2368
2371
|
status,
|
|
2369
2372
|
...details,
|
|
2373
|
+
...(recordedRun ? {
|
|
2374
|
+
plan: {
|
|
2375
|
+
...record.plan,
|
|
2376
|
+
recordedRunId: recordedRun.recordedRunId,
|
|
2377
|
+
recordedRuns: [
|
|
2378
|
+
...(record.plan.recordedRuns ?? []),
|
|
2379
|
+
recordedRun,
|
|
2380
|
+
],
|
|
2381
|
+
},
|
|
2382
|
+
error: recordedRun.error,
|
|
2383
|
+
} : {}),
|
|
2370
2384
|
updatedAt: new Date().toISOString(),
|
|
2371
2385
|
}
|
|
2372
2386
|
: record),
|
|
@@ -2922,7 +2936,7 @@ export function registerRunNotebookJob(engine) {
|
|
|
2922
2936
|
}
|
|
2923
2937
|
const now = new Date().toISOString();
|
|
2924
2938
|
const run = {
|
|
2925
|
-
id:
|
|
2939
|
+
id: uuidv7(),
|
|
2926
2940
|
notebookId: input.notebookId,
|
|
2927
2941
|
status: errors.length === 0 ? "completed" : "failed",
|
|
2928
2942
|
startedAt: now,
|
|
@@ -2939,11 +2953,16 @@ export function registerRunNotebookJob(engine) {
|
|
|
2939
2953
|
runId,
|
|
2940
2954
|
outputs,
|
|
2941
2955
|
...(errors.length > 0 ? { error: errors.join("\n") } : {}),
|
|
2956
|
+
}, {
|
|
2957
|
+
recordedRunId: run.id,
|
|
2958
|
+
status: run.status,
|
|
2959
|
+
completedAt: now,
|
|
2960
|
+
...(run.error ? { error: run.error } : {}),
|
|
2942
2961
|
});
|
|
2943
2962
|
}
|
|
2944
2963
|
if (errors.length > 0)
|
|
2945
2964
|
throw new Error(errors.join("\n"));
|
|
2946
|
-
return run;
|
|
2965
|
+
return { ...run, id: runId, runId, recordedRunId: run.id };
|
|
2947
2966
|
};
|
|
2948
2967
|
registerJobHandler("run_notebook", async (payload) => {
|
|
2949
2968
|
const input = payload;
|
|
@@ -3,6 +3,7 @@ import { requireApiKey } from "../../api-key.js";
|
|
|
3
3
|
import { uploadAssetToFal } from "../../jobs/_fal-upload.js";
|
|
4
4
|
import { describeFalError } from "../../jobs/_fal-shared.js";
|
|
5
5
|
import { withProviderHandoff } from "../../jobs/_lease.js";
|
|
6
|
+
import { generationCostHints } from "../cost.js";
|
|
6
7
|
import { providerEndpoints } from "../endpoints.js";
|
|
7
8
|
import { writePendingTask } from "./pending.js";
|
|
8
9
|
import { MEDIA_TASK_TYPES } from "./types.js";
|
|
@@ -30,6 +31,12 @@ async function submitQueued(request, context, ownerId) {
|
|
|
30
31
|
return submitted.request_id;
|
|
31
32
|
}
|
|
32
33
|
async function submit(request, context) {
|
|
34
|
+
const hints = generationCostHints(request, context.costHints);
|
|
35
|
+
if (Object.keys(hints).length > 0) {
|
|
36
|
+
const written = await context.engine.metadata.artifacts.write(context.assetId, "generation-cost-hints", hints);
|
|
37
|
+
if (!written.ok)
|
|
38
|
+
throw new Error(written.error.message);
|
|
39
|
+
}
|
|
33
40
|
const taskId = await withProviderHandoff(context.engine, context.assetId, context.ctx, "generate", async (ownerId) => {
|
|
34
41
|
const id = await submitQueued(request, context, ownerId);
|
|
35
42
|
return { taskId: id, result: id };
|
package/dist/media/catalog.js
CHANGED
|
@@ -8,6 +8,13 @@ const SEEDANCE_MODES = [
|
|
|
8
8
|
const FRAME_MODES = [
|
|
9
9
|
{ id: "i2v", label: "Frames", roles: ["first_frame", "last_frame"] },
|
|
10
10
|
];
|
|
11
|
+
const FLUX_3_MAX_DEFAULTS = {
|
|
12
|
+
aspect_ratio: "auto",
|
|
13
|
+
duration: 5,
|
|
14
|
+
generate_audio: true,
|
|
15
|
+
resolution: "720p",
|
|
16
|
+
reasoning_level: "high",
|
|
17
|
+
};
|
|
11
18
|
const SEEDANCE_US_REFERENCE_MODES = [
|
|
12
19
|
{ id: "r2v", label: "References", roles: ["image", "video", "audio"], requireAny: ["image", "video", "audio"] },
|
|
13
20
|
{ id: "v2v", label: "Edit video", roles: ["video", "image", "audio"], requireAny: ["video"], defaults: { task: "editing" } },
|
|
@@ -60,6 +67,8 @@ function h3MaxStyle(style) {
|
|
|
60
67
|
}
|
|
61
68
|
export const MODEL_ENTRIES = [
|
|
62
69
|
{ id: "kie:nano-banana-2-1", media: "image", operations: ["text_to_image", "edit_image"], dialect: "natural_numbered", defaults: { ...LIGHTNING_SCENE_OPTIONS } },
|
|
70
|
+
{ id: "kie:flux-3/klein-text-to-image", media: "image", operations: ["text_to_image"], dialect: "natural", defaults: { resolution: "1K" } },
|
|
71
|
+
{ id: "kie:flux-3/klein-edit-image", media: "image", operations: ["edit_image"], dialect: "natural", defaults: { resolution: "1K" } },
|
|
63
72
|
{ id: "kie:nano-banana-pro", media: "image", operations: ["text_to_image", "edit_image"], guide: "nano_banana", dialect: "natural" },
|
|
64
73
|
{ id: "kie:gpt-image-2-5-flare-text-to-image", media: "image", operations: ["text_to_image"], guide: "gpt_image", dialect: "natural", defaults: { resolution: "2K" } },
|
|
65
74
|
{ id: "kie:gpt-image-2-5-flare-image-to-image", media: "image", operations: ["edit_image"], guide: "gpt_image", dialect: "natural", defaults: { resolution: "2K" } },
|
|
@@ -108,6 +117,8 @@ export const MODEL_ENTRIES = [
|
|
|
108
117
|
{ id: "fal:ideogram/v4.5/edit", media: "image", operations: ["edit_image"], refs: { image: ["image_url", "reference_image_urls"] } },
|
|
109
118
|
{ id: "fal:blackforestlabs/flux-3/text-to-image", media: "image", operations: ["text_to_image"] },
|
|
110
119
|
{ id: "fal:blackforestlabs/flux-3/edit-image", media: "image", operations: ["edit_image"] },
|
|
120
|
+
{ id: "fal:bria/fibo-2/generate", media: "image", operations: ["text_to_image"] },
|
|
121
|
+
{ id: "fal:bria/fibo-2/edit", media: "image", operations: ["edit_image"], prompt: "instruction" },
|
|
111
122
|
{ id: "fal:fal-ai/flux-2-flex", media: "image", operations: ["text_to_image"] },
|
|
112
123
|
{ id: "fal:bria/guided-remove-background", media: "image", operations: ["edit_image"] },
|
|
113
124
|
{ id: "fal:bria/fibo-edit-1.5/expand", media: "image", operations: ["edit_image"] },
|
|
@@ -115,10 +126,10 @@ export const MODEL_ENTRIES = [
|
|
|
115
126
|
{
|
|
116
127
|
id: "kie:bytedance/seedance-2-5",
|
|
117
128
|
media: "video",
|
|
118
|
-
operations: ["text_to_video", "image_to_video", "reference_to_video"],
|
|
129
|
+
operations: ["text_to_video", "image_to_video", "reference_to_video", "edit_video"],
|
|
119
130
|
guide: "seedance25",
|
|
120
131
|
dialect: "seedance",
|
|
121
|
-
modes: SEEDANCE_MODES,
|
|
132
|
+
modes: [...SEEDANCE_MODES, { id: "v2v", label: "Edit video", roles: ["video", "image", "audio"], requireAny: ["video"] }],
|
|
122
133
|
defaults: { ...LIGHTNING_VIDEO_OPTIONS },
|
|
123
134
|
frameOptions: { aspect_ratio: "adaptive" },
|
|
124
135
|
},
|
|
@@ -170,6 +181,10 @@ export const MODEL_ENTRIES = [
|
|
|
170
181
|
{ id: "fal:alibaba/wan-3.0-prime/text-to-video", media: "video", operations: ["text_to_video"], hidden: true },
|
|
171
182
|
{ id: "fal:alibaba/wan-3.0-prime/image-to-video", media: "video", operations: ["image_to_video"], hidden: true },
|
|
172
183
|
{ id: "fal:alibaba/wan-3.0-prime/reference-to-video", media: "video", operations: ["reference_to_video"], hidden: true },
|
|
184
|
+
{ id: "fal:blackforestlabs/flux-3-max/text-to-video", media: "video", operations: ["text_to_video"], title: "FLUX 3 Max", defaults: FLUX_3_MAX_DEFAULTS },
|
|
185
|
+
{ id: "fal:blackforestlabs/flux-3-max/image-to-video", media: "video", operations: ["image_to_video"], title: "FLUX 3 Max Image", refs: { first_frame: "image_url" }, modes: FRAME_MODES, defaults: FLUX_3_MAX_DEFAULTS },
|
|
186
|
+
{ id: "fal:blackforestlabs/flux-3-max/keyframes-to-video", media: "video", operations: ["image_to_video"], title: "FLUX 3 Max Keyframes", refs: { image: "keyframes" }, keyframeFps: 24, defaults: FLUX_3_MAX_DEFAULTS },
|
|
187
|
+
{ id: "fal:minimax/h3-max/reframe-video", media: "video", operations: ["edit_video"] },
|
|
173
188
|
{ id: "fal:minimax/h3-max/insert-video", media: "video", operations: ["edit_video"], refs: { video: ["video_url", "reference_video_urls"] } },
|
|
174
189
|
{ id: "fal:minimax/h3-max/recast", media: "video", operations: ["edit_video"] },
|
|
175
190
|
{ id: "fal:minimax/h3-max-turbo/extend-video", media: "video", operations: ["extend_video"] },
|
package/dist/media/cost.js
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createFal } from "@apicity/fal";
|
|
2
|
+
import { computeEstimate, createFalEstimateCache, resolveFalDynamicEstimate, } from "@apicity/cost";
|
|
2
3
|
import { buildWireRequest } from "./request.js";
|
|
4
|
+
const FAL_ESTIMATE_TTL_MS = 5 * 60 * 1000;
|
|
5
|
+
let falEstimateCache = createFalEstimateCache({
|
|
6
|
+
ttlMs: FAL_ESTIMATE_TTL_MS,
|
|
7
|
+
maxEntries: 128,
|
|
8
|
+
});
|
|
9
|
+
let estimatorOverrideActive = false;
|
|
10
|
+
let estimatorOverride = null;
|
|
11
|
+
let liveEstimatorKey;
|
|
12
|
+
let liveEstimator;
|
|
13
|
+
function freshFalEstimateCache() {
|
|
14
|
+
return createFalEstimateCache({ ttlMs: FAL_ESTIMATE_TTL_MS, maxEntries: 128 });
|
|
15
|
+
}
|
|
16
|
+
export function installFalDynamicEstimator(estimator) {
|
|
17
|
+
const previous = {
|
|
18
|
+
active: estimatorOverrideActive,
|
|
19
|
+
estimator: estimatorOverride,
|
|
20
|
+
cache: falEstimateCache,
|
|
21
|
+
};
|
|
22
|
+
estimatorOverrideActive = true;
|
|
23
|
+
estimatorOverride = estimator;
|
|
24
|
+
falEstimateCache = freshFalEstimateCache();
|
|
25
|
+
return () => {
|
|
26
|
+
estimatorOverrideActive = previous.active;
|
|
27
|
+
estimatorOverride = previous.estimator;
|
|
28
|
+
falEstimateCache = previous.cache;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function falAdminKey() {
|
|
32
|
+
const value = process.env.FAL_ADMIN_API_KEY?.trim();
|
|
33
|
+
return value ? value : undefined;
|
|
34
|
+
}
|
|
35
|
+
function falPricingEstimator(apiKey) {
|
|
36
|
+
const estimate = createFal({ apiKey }).v1.models.pricing.estimate;
|
|
37
|
+
return (request) => estimate(request);
|
|
38
|
+
}
|
|
39
|
+
function currentFalEstimator() {
|
|
40
|
+
if (estimatorOverrideActive)
|
|
41
|
+
return estimatorOverride;
|
|
42
|
+
const apiKey = falAdminKey();
|
|
43
|
+
if (!apiKey)
|
|
44
|
+
return null;
|
|
45
|
+
if (liveEstimator && liveEstimatorKey === apiKey)
|
|
46
|
+
return liveEstimator;
|
|
47
|
+
liveEstimatorKey = apiKey;
|
|
48
|
+
liveEstimator = falPricingEstimator(apiKey);
|
|
49
|
+
return liveEstimator;
|
|
50
|
+
}
|
|
51
|
+
export async function estimateFalRequest(endpoint, payload, hints) {
|
|
52
|
+
const estimator = currentFalEstimator();
|
|
53
|
+
if (!estimator) {
|
|
54
|
+
return computeEstimate({
|
|
55
|
+
provider: "fal",
|
|
56
|
+
payload,
|
|
57
|
+
endpoint,
|
|
58
|
+
...(hints ? { costHints: hints } : {}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return resolveFalDynamicEstimate(endpoint, payload, estimator, {
|
|
62
|
+
...(hints ? { hints } : {}),
|
|
63
|
+
// Queue polls omit x-fal-billable-units, so this is one historical call.
|
|
64
|
+
callQuantity: 1,
|
|
65
|
+
cache: falEstimateCache,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
3
68
|
function costProvider(model) {
|
|
4
69
|
return model.provider;
|
|
5
70
|
}
|
|
@@ -18,26 +83,34 @@ function durationHint(input) {
|
|
|
18
83
|
?? (numeric(input.music_length_ms) !== undefined ? numeric(input.music_length_ms) / 1000 : undefined);
|
|
19
84
|
return seconds !== undefined && seconds > 0 ? { durationSeconds: seconds } : {};
|
|
20
85
|
}
|
|
21
|
-
function
|
|
86
|
+
export function generationCostHints(input, hints) {
|
|
22
87
|
const combined = { ...durationHint(input), ...hints };
|
|
23
88
|
return combined.durationSeconds === undefined && combined.inputDurationSeconds !== undefined
|
|
24
89
|
? { ...combined, durationSeconds: combined.inputDurationSeconds }
|
|
25
90
|
: combined;
|
|
26
91
|
}
|
|
27
|
-
function estimateWireRequest(model, request, input, hints = {}) {
|
|
92
|
+
async function estimateWireRequest(model, request, input, hints = {}) {
|
|
93
|
+
const payload = costPayload(model, request, input);
|
|
94
|
+
const costHints = generationCostHints(input, hints);
|
|
95
|
+
if (model.provider === "fal")
|
|
96
|
+
return estimateFalRequest(model.apiId, payload, costHints);
|
|
28
97
|
return computeEstimate({
|
|
29
98
|
provider: costProvider(model),
|
|
30
|
-
payload
|
|
99
|
+
payload,
|
|
31
100
|
endpoint: model.apiId,
|
|
32
|
-
costHints
|
|
101
|
+
costHints,
|
|
33
102
|
});
|
|
34
103
|
}
|
|
35
|
-
export function estimateGeneration(model, call, hints = {}) {
|
|
104
|
+
export async function estimateGeneration(model, call, hints = {}) {
|
|
36
105
|
const built = buildWireRequest(model, call);
|
|
37
106
|
if (!built.ok)
|
|
38
107
|
return built;
|
|
39
108
|
try {
|
|
40
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
estimate: await estimateWireRequest(model, built.request, built.input, hints),
|
|
112
|
+
request: built.request,
|
|
113
|
+
};
|
|
41
114
|
}
|
|
42
115
|
catch (error) {
|
|
43
116
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
@@ -36,6 +36,13 @@ export function isUrlObject(schema) {
|
|
|
36
36
|
return false;
|
|
37
37
|
return branches(schema).some((branch) => branch.type === "object" && branch.properties?.url !== undefined);
|
|
38
38
|
}
|
|
39
|
+
export function isKeyframeItem(schema) {
|
|
40
|
+
if (!schema)
|
|
41
|
+
return false;
|
|
42
|
+
return branches(schema).some((branch) => branch.type === "object"
|
|
43
|
+
&& branch.properties?.image_url !== undefined
|
|
44
|
+
&& branch.properties?.frame_index !== undefined);
|
|
45
|
+
}
|
|
39
46
|
export function isArraySchema(schema) {
|
|
40
47
|
return branches(schema).some((branch) => branch.type === "array");
|
|
41
48
|
}
|
package/dist/media/ref-fields.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { arrayBounds, arrayItems, isArraySchema, isUrlObject, propertyAt } from "./json-schema.js";
|
|
1
|
+
import { arrayBounds, arrayItems, isArraySchema, isKeyframeItem, isUrlObject, propertyAt } from "./json-schema.js";
|
|
2
2
|
const CANDIDATES = {
|
|
3
3
|
first_frame: [
|
|
4
4
|
"first_frame_url",
|
|
@@ -55,12 +55,14 @@ function expandField(input, field) {
|
|
|
55
55
|
const required = isRequired(input, field);
|
|
56
56
|
if (isArraySchema(schema)) {
|
|
57
57
|
const bounds = arrayBounds(schema);
|
|
58
|
+
const items = arrayItems(schema);
|
|
58
59
|
return [{
|
|
59
60
|
path: field,
|
|
60
61
|
multi: true,
|
|
61
62
|
max: bounds.max,
|
|
62
63
|
min: required ? Math.max(1, bounds.min) : 0,
|
|
63
|
-
urlObject: isUrlObject(
|
|
64
|
+
urlObject: isUrlObject(items),
|
|
65
|
+
...(isKeyframeItem(items) ? { keyframe: true } : {}),
|
|
64
66
|
}];
|
|
65
67
|
}
|
|
66
68
|
return [{ path: field, multi: false, max: 1, min: required ? 1 : 0, urlObject: isUrlObject(schema) }];
|
|
@@ -83,7 +85,13 @@ export function resolveSlot(role, input, entry) {
|
|
|
83
85
|
throw new Error(`${entry.id}: ref field ${name} for ${role} is not in the apicity schema`);
|
|
84
86
|
}
|
|
85
87
|
const expanded = names.flatMap((name) => expandField(input, name));
|
|
86
|
-
const fields = expanded.map(({ path, multi, max, urlObject }) => ({
|
|
88
|
+
const fields = expanded.map(({ path, multi, max, urlObject, keyframe }) => ({
|
|
89
|
+
path,
|
|
90
|
+
multi,
|
|
91
|
+
max,
|
|
92
|
+
urlObject,
|
|
93
|
+
...(keyframe ? { keyframe: true } : {}),
|
|
94
|
+
}));
|
|
87
95
|
const max = expanded.some((field) => field.max === null)
|
|
88
96
|
? null
|
|
89
97
|
: expanded.reduce((sum, field) => sum + (field.max ?? 0), 0);
|
package/dist/media/refs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSeedanceEdit } from "./seedance-edit.js";
|
|
1
2
|
import { referenceVideoDuration } from "../reference-video-duration.js";
|
|
2
3
|
import { insertVideoTimingError, isInsertVideoModel } from "./insert-video-timing.js";
|
|
3
4
|
import { resolveAudioRef, resolveImageRef, resolveVideoRef } from "../tools/_media-refs.js";
|
|
@@ -129,3 +130,15 @@ export async function checkInsertVideoTiming(engine, model, refs, options) {
|
|
|
129
130
|
const duration = source ? await referenceVideoDuration(engine, [source]) : undefined;
|
|
130
131
|
return insertVideoTimingError(options, duration);
|
|
131
132
|
}
|
|
133
|
+
export async function checkSeedanceEditSource(engine, model, refs, mode) {
|
|
134
|
+
if (!isSeedanceEdit(model.id, mode))
|
|
135
|
+
return null;
|
|
136
|
+
if (refs.video?.length !== 1)
|
|
137
|
+
return "Seedance Edit video requires exactly one source video (4–30 seconds).";
|
|
138
|
+
const duration = await referenceVideoDuration(engine, refs.video);
|
|
139
|
+
if (duration === undefined)
|
|
140
|
+
return "Seedance Edit video requires a source with known duration. Import the video into the book and use its artifact reference (4–30 seconds).";
|
|
141
|
+
return duration < 4 || duration > 30
|
|
142
|
+
? `Seedance Edit video source is ${duration} seconds; use a source between 4 and 30 seconds.`
|
|
143
|
+
: null;
|
|
144
|
+
}
|