@kolbo/mcp 1.69.0 → 1.70.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/package.json +1 -1
- package/skill/GENERATED.md +1 -1
- package/skill/SKILL.md +6 -6
- package/skill/references/models/gpt-image.md +1 -1
- package/skill/references/models/nano-banana.md +1 -1
- package/skill/references/models/prompt-copilot.md +1 -0
- package/skill/references/models/seedance.md +44 -26
- package/skill/references/models/seedance25.md +65 -0
- package/skill/references/workflows/marketing-studio.md +1 -1
- package/skill/references/workflows/media-library.md +2 -13
- package/src/apps/widgets/generation.js +7 -2
- package/src/client.js +78 -11
- package/src/index.js +1 -1
- package/src/polling.js +2 -2
- package/src/progress.js +74 -3
- package/src/tools/_shared.js +11 -3
- package/src/tools/generate.js +23 -17
- package/src/tools/projects.js +0 -1
package/README.md
CHANGED
|
@@ -236,7 +236,12 @@ Every generation tool also accepts an optional `project_id` arg that routes the
|
|
|
236
236
|
| `list_presets` | Generation presets across image/image-edit/video/music/text-to-video catalogs. Pass the selected exact id as `preset_id`; never claim a preset was applied without it. |
|
|
237
237
|
| `list_cinematic_presets` | "Cinema mode" presets grouped by dimension (camera, lens, focal_length, aperture, angle, shot_type, color_palette, lighting) — pass ids via the `cinematic` arg on `generate_image` / `generate_image_edit`. Only when the user wants a specific cinematic look |
|
|
238
238
|
| `list_projects` | List owned + shared projects (id, name, role, is_default) — call first to resolve a project name into the `project_id` you pass to generation tools |
|
|
239
|
-
| `move_session` | Move
|
|
239
|
+
| `move_session` | Move ONE session (generation, chat, transcription…) and ALL its generations + media to another project |
|
|
240
|
+
| `bulk_move_sessions` | Move up to 100 sessions into one project in a single call — mixed types allowed, per-session failures reported |
|
|
241
|
+
| `list_session_generations` | A session's generations as complete groups (prompt + all its outputs) — the ids the two organize tools below take |
|
|
242
|
+
| `move_generations_to_session` | Move selected generations (and only THEIR output media) into another existing session |
|
|
243
|
+
| `split_session` | Carve selected generations out into a brand-new named session, atomically |
|
|
244
|
+
| `undo_session_organization` | Reverse a move/split within 15 minutes, using the `operation_id` it returned |
|
|
240
245
|
| `create_doc` / `list_docs` / `get_doc` / `update_doc` / `share_doc` / `delete_doc` | AI Docs (Magic Pad): author project-scoped HTML documents, edit them, get public share links |
|
|
241
246
|
| `generate_character_sheet` | Generate a multi-angle character sheet from reference images (credits) → pass URL to create_visual_dna for stronger character consistency |
|
|
242
247
|
| `list_visual_dna_folders` / `create_visual_dna_folder` / `update_visual_dna_folder` / `delete_visual_dna_folder` / `move_visual_dna_to_folder` | Organize Visual DNA characters into user folders (create/rename/recolor/delete + move DNAs in/out) |
|
package/package.json
CHANGED
package/skill/GENERATED.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# AUTO-GENERATED — do not edit
|
|
2
2
|
|
|
3
3
|
This skill/ tree is mirrored from kolbo-code (the single source of truth)
|
|
4
|
-
by .github/workflows/sync-skill-to-plugin.yml — synced from kolbo-code@
|
|
4
|
+
by .github/workflows/sync-skill-to-plugin.yml — synced from kolbo-code@3a7950b.
|
|
5
5
|
|
|
6
6
|
It is the skill that 'npx @kolbo/mcp install' deploys into the user's agent.
|
|
7
7
|
To change it, edit packages/opencode/skills/kolbo/ in kolbo-code and push;
|
package/skill/SKILL.md
CHANGED
|
@@ -65,6 +65,7 @@ For multi-scene / batch work this pairs with `generate_creative_director` (see b
|
|
|
65
65
|
| If the user wants to… | Read first |
|
|
66
66
|
|---|---|
|
|
67
67
|
| Generate a **Seedance 2** video | `references/models/seedance.md` |
|
|
68
|
+
| Generate a **Seedance 2.5** video | `references/models/seedance25.md` (also load `seedance.md` for the locked intro + craft layer) |
|
|
68
69
|
| Generate a **GPT Image 2** image | `references/models/gpt-image.md` |
|
|
69
70
|
| Generate a **Nano Banana / Gemini** image | `references/models/nano-banana.md` |
|
|
70
71
|
| Generate a **Veo 3 / 3.1** video | `references/models/veo.md` |
|
|
@@ -99,11 +100,11 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
|
|
|
99
100
|
| `generate_video` | Text-to-video. Does **not** support Visual DNA — use `generate_elements` for character-consistent video. |
|
|
100
101
|
| `generate_video_from_image` | Animate a still. Prompt describes motion, not subject. |
|
|
101
102
|
| `generate_video_from_video` | Restyle/transform an existing video. Keeps original motion. |
|
|
102
|
-
| `generate_elements` | Reference-driven video. **Primary route for DNA → video.**
|
|
103
|
+
| `generate_elements` | Reference-driven video. **Primary route for DNA → video.** |
|
|
103
104
|
| `generate_first_last_frame` | Keyframe interpolation between two frames. |
|
|
104
105
|
| `generate_lipsync` | Lipsync audio to an image or video face. |
|
|
105
106
|
| `generate_music` | Music generation (Suno + variants). |
|
|
106
|
-
| `generate_speech` | TTS. Use `list_voices` to pick a voice.
|
|
107
|
+
| `generate_speech` | TTS. Use `list_voices` to pick a voice. |
|
|
107
108
|
| `generate_sound` | Sound effects. |
|
|
108
109
|
| `generate_3d` | 3D models from text / single image / multi-view. Returns GLB/FBX/OBJ/USDZ. |
|
|
109
110
|
|
|
@@ -125,7 +126,6 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
|
|
|
125
126
|
| `clone_voice` / `import_elevenlabs_voice` / `delete_voice` | Custom voices (clone CHARGES CREDITS — confirm first; new voices show in `list_voices`) |
|
|
126
127
|
| `trim_video` | Frame-accurate trim of a Kolbo-hosted video (tool waits and returns the URL). `edit_video` also gained `remove_background`. |
|
|
127
128
|
| `create_doc` / `list_docs` / `get_doc` / `update_doc` / `share_doc` / `delete_doc` | AI Docs (Magic Pad): YOU author full HTML documents (plans, briefs, scripts, research) saved into the user's project, editable in the Kolbo app. `share_doc` returns a public link. `update_doc` content replaces the WHOLE doc — `get_doc` first. |
|
|
128
|
-
| `create_review_asset` / `list_review_assets` / `get_review_asset` / `update_review_asset` / `add_review_version` / `set_review_status` / `delete_review_asset` / `get_review_storage_usage` / review collection + comment + share-link tools | **Kolbo Review** (Frame.io-style): upload first via `upload_media` / ticket / widget → pass `media_id` to `create_review_asset`. Comments are text + optional timecodes. `create_review_share_link` returns a guest URL. |
|
|
129
129
|
| `chat_send_message` / `chat_list_conversations` / `chat_get_messages` | Kolbo chat with optional `media_urls` (up to 10 per call) |
|
|
130
130
|
| `publish_html_artifact` | Publish HTML / SVG / Mermaid to `sites.kolbo.ai`. Server dedupes by content hash. Strict CSP. |
|
|
131
131
|
|
|
@@ -166,6 +166,7 @@ A user-named tool — in any language — overrides every other rule. Recognized
|
|
|
166
166
|
- User named one → use it. Model identifiers resolve leniently — shorthand like `"z-image"` or `"nano banana 2"` auto-resolves to the exact identifier, so don't over-engineer exact-id lookups (`list_models` is still authoritative for constraints, caps, and pricing).
|
|
167
167
|
- Auto-select → only from "Auto-selectable" section (models with a `summary`). Cheapest fit. Prefer `[RECOMMENDED]` when cost is similar.
|
|
168
168
|
- Never auto-select from "Named-only" section.
|
|
169
|
+
- **Photoreal photo edits** (object removal, keep one person / remove the crowd, inpainting, "edit this photo") → `generate_image_edit` with **Nano Banana 2** or **GPT Image 2** only. Do not auto-pick Flux 2 / Flux Klein — those are generate-from-scratch / style, named-only for editing.
|
|
169
170
|
4. **Validate inputs** against model caps — see `references/workflows/cost-and-validation.md`.
|
|
170
171
|
5. **How calls work**: each tool blocks until generation is fully complete. Images: seconds. Video: minutes. Multiple tool calls in one response run concurrently. On hosts with live widgets the tool instead returns `submitted` instantly — the card updates on its own; you only need `get_generation_status` when a follow-up step needs the output URLs.
|
|
171
172
|
6. **Checking status — NEVER poll in a loop**: `get_generation_status` takes `wait=true` (blocks server-side until done, ~3 min) and `generation_ids` (check MANY generations in ONE call — returns `all_done` + which are still running). One `wait=true` call replaces any polling loop. If it comes back with some still processing, call it ONCE more with `wait=true` and the remaining ids.
|
|
@@ -177,10 +178,9 @@ Model types for `list_models`: `text_to_img`, `image_editing`, `text_to_video`,
|
|
|
177
178
|
|
|
178
179
|
Everything in Kolbo — sessions, generations, media, docs — lives inside a PROJECT. Getting this wrong is the #1 user complaint ("my work went to the wrong project").
|
|
179
180
|
|
|
180
|
-
1. **User names a project** ("in my Acme project", "for the film") → call `list_projects` ONCE to resolve the name to an ObjectId, then pass that id as `project_id` on **EVERY** subsequent `generate_*` / `upload_media` / `create_doc` / `chat_send_message` call in the conversation. It is **per-call, NOT sticky** — any call that omits it silently lands in the default "API Generations" bucket (`is_default: true`).
|
|
181
|
+
1. **User names a project** ("in my Acme project", "for the film") → call `list_projects` ONCE to resolve the name to an ObjectId, then pass that id as `project_id` on **EVERY** subsequent `generate_*` / `upload_media` / `create_doc` / `chat_send_message` call in the conversation. It is **per-call, NOT sticky** — any call that omits it silently lands in the default "API Generations" bucket (`is_default: true`). Accounts often hold hundreds of projects, so pass `list_projects({ search: "acme" })` rather than listing everything; the list is paginated (50/page) and hides archived projects unless you pass `include_archived: true`.
|
|
181
182
|
2. **No project mentioned** → omit `project_id`; the default bucket is correct. Don't ask unless intent is ambiguous.
|
|
182
183
|
3. **Work landed in the wrong project? MOVE it, never regenerate**: `move_session` relocates a whole session + all its media (works for any session type — the `session_id` from generation responses, chats, transcriptions); `move_media` / `bulk_move_media` / `move_folder_contents` relocate individual media items.
|
|
183
|
-
4. **Inside the project, keep a related set in ONE session.** Every generation response returns a `session_id`. When a set needs several calls — shot 2, 3, 4 of a sequence, or more takes of the same idea — pass the FIRST call's `session_id` back on each follow-up. Without it every call opens its own session and the user's sidebar fills with near-identical single-item sessions. Prefer a real batch where one exists (`prompts` on `generate_image` / `generate_video`, `items` on `generate_video_from_image` — up to 8 `{image_url, prompt}` pairs animated in one call and one widget — `num_images`, `generate_creative_director`); use `session_id` for the tools that animate/edit one thing per call (`generate_elements`, `generate_first_last_frame`, `generate_lipsync`, `generate_video_from_video`, `edit_image`, `edit_video`), and across batches when a sequence runs longer than 8. Start a NEW session (omit it) when the next generation is unrelated.
|
|
184
184
|
|
|
185
185
|
|
|
186
186
|
## Cost Awareness — Quick Rules
|
|
@@ -228,7 +228,7 @@ You are NOT allowed to:
|
|
|
228
228
|
|
|
229
229
|
Existing video → modify → **single `generate_video_from_video` call** with source video URL + edit prompt.
|
|
230
230
|
|
|
231
|
-
**Use a TRUE video-to-video model.** Image-to-video models reject with `WRONG_MODEL_TYPE`. Valid: `wan/2-7-videoedit`, `happyhorse/video-edit`, `kling-video/o3-video-to-video`,
|
|
231
|
+
**Use a TRUE video-to-video model.** Image-to-video models reject with `WRONG_MODEL_TYPE`. Valid: `wan/2-7-videoedit`, `happyhorse/video-edit`, `kling-video/o3-video-to-video`, or any model whose DB `type` includes `video_to_video` (use `list_models({ type: "video_to_video" })`).
|
|
232
232
|
|
|
233
233
|
**Motion-control / animate-move models invert the inputs**: `reference_images[0]` = the CHARACTER IMAGE to animate, `source_video` = the driving/reference video whose motion is transferred. Omitting the character image returns a `MOTION_CONTROL_INPUTS` error.
|
|
234
234
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Load this file when the user wants a **GPT Image 2 / gpt-image-2** image (OpenAI). For other image models see `models/nano-banana.md`, `models/creative-director.md`, or `models/prompt-copilot.md`.
|
|
8
8
|
|
|
9
|
-
**Kolbo MCP routing:** call `generate_image` (text-to-image) or `generate_image_edit` (edits with `source_images`). Pass `model: "gpt-image-2"` when the user named it
|
|
9
|
+
**Kolbo MCP routing:** call `generate_image` (text-to-image) or `generate_image_edit` (edits with `source_images`). Pass `model: "gpt-image-2"` when the user named it. For photoreal photo edits that need identity lock or readable on-image text, default to `gpt-image-2` via `generate_image_edit` — consult `list_models({ type: "image_editing" })`. Do not auto-pick Flux.
|
|
10
10
|
|
|
11
11
|
## CRITICAL Kolbo Platform Rules
|
|
12
12
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Load this file when the user wants a **Nano Banana 2 (Gemini 3.1 Flash Image)** or **Nano Banana Pro (Gemini 3 Pro Image)** image. For other image models see `models/gpt-image.md`, `models/creative-director.md`, or `models/prompt-copilot.md`.
|
|
8
8
|
|
|
9
|
-
**Kolbo MCP routing:** call `generate_image` or `generate_image_edit`. Pass `model: "nano-banana-2"` or `model: "nano-banana-pro"` when the user named one
|
|
9
|
+
**Kolbo MCP routing:** call `generate_image` or `generate_image_edit`. Pass `model: "nano-banana-2"` or `model: "nano-banana-pro"` when the user named one. For prompt-driven photoreal photo edits (object removal, keep subject / remove others), default to `nano-banana-2` via `generate_image_edit` — consult `list_models({ type: "image_editing" })`. Do not auto-pick Flux.
|
|
10
10
|
|
|
11
11
|
## CRITICAL Kolbo Platform Rules
|
|
12
12
|
|
|
@@ -123,6 +123,7 @@ If during the conversation it becomes clear the user is actually working with on
|
|
|
123
123
|
| User mentions / asks for | Switch to |
|
|
124
124
|
|---|---|
|
|
125
125
|
| Seedance / Seedance 2 / Bytedance video | `models/seedance.md` |
|
|
126
|
+
| Seedance 2.5 | `models/seedance25.md` (also load `models/seedance.md`) |
|
|
126
127
|
| GPT Image 2 / gpt-image-2 / OpenAI image | `models/gpt-image.md` |
|
|
127
128
|
| Nano Banana / Gemini image / Gemini 3 Pro Image | `models/nano-banana.md` |
|
|
128
129
|
| Veo / Veo 3 / Veo 3.1 / Google video | `models/veo.md` |
|
|
@@ -9,15 +9,13 @@ Load this file when the user wants a **Seedance 2 / Seedance 2.0** (ByteDance) v
|
|
|
9
9
|
|
|
10
10
|
**Kolbo MCP routing:** Seedance is a video model — call `generate_video` (text-to-video) or `generate_elements` (when video references / Visual DNA / first-last frames are involved). Run `list_models({ type: "text_to_video" })` and pick a Seedance variant by name.
|
|
11
11
|
|
|
12
|
-
**Multishot (Elements):** Seedance has **no** native provider Multishot switch. Pass `multi_shots: true` on `generate_elements` (optional `multi_shot_count`) for the UI Multishot badge only — it does **not** rewrite or reshape your prompt. **Always send `enhance_prompt: false` for crafted demos** (MCP default) so the prompt stays RAW. Put shot structure in the prompt yourself (`Multishot ON`, `Total: … / N shots`, numbered shots). Optional `session_name` sets a short sidebar title (skips AI auto-name from boilerplate).
|
|
13
|
-
|
|
14
12
|
## Universal Rules (apply to EVERY Seedance prompt)
|
|
15
13
|
|
|
16
14
|
- **First line ALWAYS declares shot structure**: total duration, shot count, aspect ratio. Example: `Total: 15s / 6 shots / 16:9`. Put it at the BOTTOM of the prompt too. For connected narrative sequences the proven phrasing is `N connected cinematic shots, 15 seconds total, 16:9, Multishot ON` — use it and keep `Multishot ON` for any multi-shot story.
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
15
|
+
- **Then the Locked Intro** — `[GLOBAL LOOK]` / `[CAST]` / `[LOCATION]` — before any shot. A one-liner `same character throughout` is not a character lock.
|
|
16
|
+
- **Order inside each shot**: Subject → Action → Camera → Constraints → (Audio/SFX if relevant). Do NOT restack GLOBAL LOOK style inside the shot.
|
|
17
|
+
- **Prompt length**: simple single-idea pieces ~120–280 words. Locked-intro cinematic typically 400–900 words. Shorter than ~120 words = random output. The 8000-char cap below always wins.
|
|
18
|
+
- **Shot count is user-directed, not capped at 3 or 6.** If the user asks for N shots, deliver exactly N shots in one prompt unless they explicitly ask to split it.
|
|
21
19
|
- **Always describe at least one camera movement per shot.**
|
|
22
20
|
- **Tell Seedance what the camera is NOT doing** (e.g. `no cuts, no zoom, natural head movement`) — this is what locks POV.
|
|
23
21
|
- **Final prompt is always English**, wrapped in a copy-ready code block. Detect intent in any language and reply in the user's language, but the prompt itself is English.
|
|
@@ -28,13 +26,42 @@ Load this file when the user wants a **Seedance 2 / Seedance 2.0** (ByteDance) v
|
|
|
28
26
|
- **Never** split into multiple prompts, multiple code blocks, or "part 1 / part 2" to evade the cap.
|
|
29
27
|
- Before outputting, internally count the characters of the final prompt as a single string. If > 8000, rewrite tighter and re-count. Repeat until ≤ 8000. Only then show the user.
|
|
30
28
|
|
|
29
|
+
## Locked Intro (DEFAULT for any multi-shot cinematic)
|
|
30
|
+
|
|
31
|
+
After the Total line, every multi-shot prompt — and any piece with recurring people or a recurring place — opens with three locked blocks. Same camera, same people, same world in every cut. Do not skip them. Do not restack this look as per-shot style.
|
|
32
|
+
|
|
33
|
+
Skip only for: true single-shot POV/orb, 3×3 grid-panel mode, or video-edit tasks. UGC/phone pieces still use the three headers, but GLOBAL LOOK is phone-native (no "cinematic", no film-camera body).
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Total: Xs / N shots / AR
|
|
37
|
+
N connected cinematic shots, Xs total, AR, Multishot ON
|
|
38
|
+
|
|
39
|
+
[GLOBAL LOOK – LOCKED, APPLIES TO EVERY SHOT]
|
|
40
|
+
Arricam LT, Cooke S4/i primes, 35mm Kodak Vision3 500T, 1.85:1 spherical, T2.8. Shallow depth of field, halation on highlights, fine organic grain, lifted milky blacks, low contrast, no sharpening, no HDR. [Named] grade: [hero tone], [field colors], [shadows], [bloom]. Handheld with micro-drift, never locked off. Naturalistic performance, real dialogue sync, no music.
|
|
41
|
+
|
|
42
|
+
[CAST – IDENTICAL IN EVERY SHOT]
|
|
43
|
+
NAME: age, build, hair, face, wardrobe, skin, signature details. @Tag when a reference or DNA exists.
|
|
44
|
+
PROP: recurring object.
|
|
45
|
+
|
|
46
|
+
[LOCATION]
|
|
47
|
+
Place in materials + light + color field. Blocking: who sits/stands where. Background LIFE (extras, ambient motion).
|
|
48
|
+
|
|
49
|
+
SHOT 1 — 0:00–0:02 — Medium / camera position
|
|
50
|
+
(director-dictated action — physical verbs, timed acting, quoted dialogue)
|
|
51
|
+
Total: Xs / N shots / AR
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Adapt the GLOBAL LOOK package to THIS world — never paste a beach grade onto a night interior. Equipment names (body / lens / stock / stop) live in GLOBAL LOOK only. Never name a real director.
|
|
55
|
+
|
|
56
|
+
**Acting:** shot size → physical action → line → timing. `laughs obnoxiously for half a second, then flat and certain` — unbounded laughs eat 2s. Background LIFE on every wide/medium or the frame reads as a ghost town.
|
|
57
|
+
|
|
31
58
|
## The 5 Formats
|
|
32
59
|
|
|
33
60
|
### 1. Transformations (highest-performing format)
|
|
34
61
|
- Numbered shots, beat by beat.
|
|
35
62
|
- Escalation arc: **calm → threat → transformation → aftermath**.
|
|
36
63
|
- 6 shots / 15s / 16:9 is the proven structure.
|
|
37
|
-
-
|
|
64
|
+
- Prefer the **Locked Intro** over a style dump. For a quick action montage with no recurring cast, a short GLOBAL LOOK still beats the old ARRI-prefix pile.
|
|
38
65
|
- **Realism trick**: for monsters/creatures, append `no 3D, no cartoon, no VFX` to force ultra-realism.
|
|
39
66
|
- **Comedy trick**: append `add a visual gag in the background` and Seedance invents one.
|
|
40
67
|
|
|
@@ -54,7 +81,7 @@ Load this file when the user wants a **Seedance 2 / Seedance 2.0** (ByteDance) v
|
|
|
54
81
|
- Always supply: **clear location, clear power mismatch, defined escalation arc**.
|
|
55
82
|
- Describe choreography beat by beat — Seedance executes what you write.
|
|
56
83
|
- Single continuous shot 15s works for two-fighter scenes; describe camera moves between beats (`crests rooftop edge`, `full 360 orbit`, `pulls back to wide`, `descends with them`).
|
|
57
|
-
- Use
|
|
84
|
+
- Use speed-ramping with impact slow-motion as the style anchor when comedic/stylized.
|
|
58
85
|
|
|
59
86
|
### 5. Animation (3D stylized)
|
|
60
87
|
- Break the 15s into **timed segments** (`0–3s`, `3–6s`, `6–9s`, `9–12s`, `12–15s`) and describe each explicitly.
|
|
@@ -66,10 +93,7 @@ Load this file when the user wants a **Seedance 2 / Seedance 2.0** (ByteDance) v
|
|
|
66
93
|
|
|
67
94
|
Use whenever the user gives named characters or multiple reference images (`@Image1`, `@Image2`, …) — a tactical unit clearing a bunker, a duel between two referenced characters, a war scene. **This is always an Elements-mode prompt** (route the card to `elements`). Structure:
|
|
68
95
|
|
|
69
|
-
1. **
|
|
70
|
-
- `Time of day:` — hour + light quality + atmosphere (dust, haze, heavy silence before action).
|
|
71
|
-
- `Location:` — the environment in concrete physical detail (materials, wear, light direction, high-contrast blown-out entrance, etc.).
|
|
72
|
-
- `Characters:` — ONE line per person: `Name @ImageN — wardrobe, position in frame, what they carry`. End with "All must match their character references exactly."
|
|
96
|
+
1. **Locked Intro FIRST** (GLOBAL LOOK / CAST / LOCATION) — CAST is one line per person: `Name @ImageN — wardrobe, position in frame, what they carry`. End CAST with "All must match their character references exactly." LOCATION carries time-of-day + materials + light direction.
|
|
73
97
|
2. **REFERENCE CONSISTENCY block** — map every reference and pin what must NOT change: `Reference Image 1 is <X>. Preserve exact face, hair, anatomy, wardrobe, colors, props.` Add per-character energy/aura color rules, and any already-established story state (e.g. "the gem is already shattered — no intact gem, no red glow"). End with "Do not redesign, morph, recolor, or swap either character, their clothing, anatomy, weapons, or the environment."
|
|
74
98
|
3. **Shots** — either titled (`Shot 1 — Medium Wide / Tactical Positioning`) or timecoded (`SHOT 1 — 0:00–0:03`); timecodes must sum to the total duration. Under each shot use **Camera → Action → Audio** in that order.
|
|
75
99
|
4. **Continuity** — to chain a series, open with `Begin as a seamless continuation from <the exact last beat of the previous video>.`
|
|
@@ -104,16 +128,9 @@ The model reacts to what can be **seen and measured**, not to mood words. Transl
|
|
|
104
128
|
- ❌ "tense scene" → ✅ "man freezes, slowly clenches his fist, light only from the side, half his face in shadow"
|
|
105
129
|
- ❌ "cool cinematic shot of a car, epic, fast" → ✅ "low tracking shot alongside the car as it powers through a wet curve, headlights glowing, spray off the tyres, hard buffeting camera shake"
|
|
106
130
|
|
|
107
|
-
### Style —
|
|
108
|
-
|
|
109
|
-
Never pile all style tokens at the top of the prompt. Each aspect lives in the block that already governs it:
|
|
131
|
+
### Style — LOCKED at the top, not redistributed
|
|
110
132
|
|
|
111
|
-
|
|
112
|
-
- Lens / FOV → in OPTICS
|
|
113
|
-
- Color → either an explicit grade (when strong / stylized) or folded into LOCATION + LIGHTING for naturalistic looks
|
|
114
|
-
- Skin / acting → in PERFORMANCE
|
|
115
|
-
- Physics → in PHYSICS
|
|
116
|
-
- Format / resolution / grain → at the END as a suffix stack (before LOCKS)
|
|
133
|
+
The GLOBAL LOOK block is the prefix. Per-shot style is ONLY what changes (shot size, move, height, this-shot light). Do not re-state film stock / lens / grade per shot. Do not pile a second style dump at the end.
|
|
117
134
|
|
|
118
135
|
### Shot sizes
|
|
119
136
|
|
|
@@ -153,7 +170,7 @@ Use only the discrete steps. Not "23°" — use 18° or 29°.
|
|
|
153
170
|
- **Emotion through muscle movement**, not labels. ❌ "she looks sad" → ✅ "her eyes drop to the table, jaw tightens, she swallows once before answering."
|
|
154
171
|
- **WB in Kelvin.** 3200K / 4000K / 5600K / 8500K. Pick ONE for the scene's mood.
|
|
155
172
|
- **Color as material + light + role**, never a flat list. ❌ "she wears red, he wears blue" → ✅ "crimson silk scarf catching the cold tungsten spill from the corridor".
|
|
156
|
-
- **
|
|
173
|
+
- **Equipment names live in GLOBAL LOOK only** (body, lens, stock, stop). Never inside shots. **Never name a real director** and never write "in the style of [person]".
|
|
157
174
|
|
|
158
175
|
### Cuts and timing
|
|
159
176
|
|
|
@@ -176,18 +193,19 @@ Use only the discrete steps. Not "23°" — use 18° or 29°.
|
|
|
176
193
|
|
|
177
194
|
### Camera placement
|
|
178
195
|
|
|
179
|
-
Place CAMERA in the **3rd position** of each shot's core layers (Subject → Action → Camera →
|
|
196
|
+
Place CAMERA in the **3rd position** of each shot's core layers (Subject → Action → Camera → Constraints). FOV gets ignored at the end, conflicts with identity at the front. GLOBAL LOOK already owns lens / stock / grade.
|
|
180
197
|
|
|
181
198
|
### Pre-flight checklist (before output)
|
|
182
199
|
|
|
183
|
-
-
|
|
200
|
+
- Locked Intro present (`[GLOBAL LOOK]`, `[CAST]`, `[LOCATION]`) unless a documented skip?
|
|
201
|
+
- GLOBAL LOOK not restated inside shots? Equipment names only there? No director names?
|
|
202
|
+
- Acting timed? Background LIFE on wides/mediums?
|
|
184
203
|
- One camera movement per time slice?
|
|
185
204
|
- FOV in degrees from the table (not mm, not arbitrary)?
|
|
186
205
|
- WB in Kelvin?
|
|
187
206
|
- Speed in km/h, atmosphere in % or meters?
|
|
188
|
-
- Color via material + light + role?
|
|
207
|
+
- Color via material + light + role (grade named as law in GLOBAL LOOK)?
|
|
189
208
|
- Positive phrasing (no "does not X")?
|
|
190
|
-
- No equipment / director names?
|
|
191
209
|
- Emotion through muscle, not labels?
|
|
192
210
|
- Multishot: FOV per segment + "no drift mid-segment"?
|
|
193
211
|
- 8000-char cap honored?
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!-- PARITY: this file mirrors getSeedance25PromptSystemPrompt() in
|
|
2
|
+
kolbo-api/src/config/systemPrompt.js.
|
|
3
|
+
Craft layer (formats, optics, grid mode) lives in models/seedance.md —
|
|
4
|
+
load that file too. Locked Intro is the same three blocks on both versions. -->
|
|
5
|
+
|
|
6
|
+
# Seedance 2.5 — Prompt Rules
|
|
7
|
+
|
|
8
|
+
Load this file when the user wants a **Seedance 2.5** video (they said "2.5" / "25", or they need longer than 15s, more than ~10 shots, or a large cast of references). Also load `models/seedance.md` for the shared craft layer.
|
|
9
|
+
|
|
10
|
+
**Kolbo MCP routing:** `generate_video` or `generate_elements` (refs / Visual DNA / first-last). Run `list_models({ type: "text_to_video" })` and pick the Seedance 2.5 variant by name.
|
|
11
|
+
|
|
12
|
+
## What's NEW in 2.5 (verified — never hedge)
|
|
13
|
+
|
|
14
|
+
- **Duration 4–30 seconds**, whole seconds. 30s IS supported.
|
|
15
|
+
- **Up to 30 shots/cuts in ONE generation.** Deliver exactly N if N ≤ 30.
|
|
16
|
+
- **Prompt cap 10,000 characters** for the entire prompt as one string.
|
|
17
|
+
- **Up to 50 reference medias / Visual DNA mentions** (`@Name`, `@ImageN`, `#Moodboard`). Every referenced asset must be tagged in the prompt text.
|
|
18
|
+
- **Multimodal refs:** images + video clips + audio can all anchor one generation.
|
|
19
|
+
|
|
20
|
+
## Locked Intro (DEFAULT — same shape as Seedance 2)
|
|
21
|
+
|
|
22
|
+
After the Total line, every multi-shot cinematic opens with:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
[GLOBAL LOOK – LOCKED, APPLIES TO EVERY SHOT]
|
|
26
|
+
[CAST – IDENTICAL IN EVERY SHOT]
|
|
27
|
+
[LOCATION]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
then timecoded `SHOT N — 0:00–0:02 — Medium / camera position` beats. Full skeleton, acting rules, and optical craft: `models/seedance.md`.
|
|
31
|
+
|
|
32
|
+
2.5 is where this format earns its keep: 15 shots timed to 30s, ~5k characters, one locked look so every cut matches camera / grade / cast. Do not skip the three blocks. Do not restack GLOBAL LOOK inside shots.
|
|
33
|
+
|
|
34
|
+
UGC / phone vertical: NEVER write "cinematic". GLOBAL LOOK is phone-native. Use `N connected phone shots, Xs total, 9:16, Multishot ON` and restate `9:16 vertical phone frame` inside every shot.
|
|
35
|
+
|
|
36
|
+
## Prompt length
|
|
37
|
+
|
|
38
|
+
Simple ≤15s ~120–280 words. Locked-intro cinematic 15s typically 400–900 words. Full 30s / 15+ shots typically 700–1200 words / ~4k–9k chars. Hard cap 10,000. Never split into part 1 / part 2.
|
|
39
|
+
|
|
40
|
+
## Feature-Block (optional, UNDER the Locked Intro)
|
|
41
|
+
|
|
42
|
+
Reach for extra department passes only when the user wants "their best possible 30 seconds" AND the 10k budget still has room after GLOBAL LOOK / CAST / LOCATION. Never replace the Locked Intro.
|
|
43
|
+
|
|
44
|
+
May add above GLOBAL LOOK: **EMOTIONAL INTENT** + **SIGNATURE MOMENT**.
|
|
45
|
+
May add under the shot list: CAMERA timecode pass, SOUND timestamps, PHYSICS contract, EDITING/CONTINUITY, DIRECTORIAL NOTES.
|
|
46
|
+
Skip CORE STYLE / SUBJECT / ENVIRONMENT — the three locked blocks already own those.
|
|
47
|
+
|
|
48
|
+
## References (ByteDance 2.5)
|
|
49
|
+
|
|
50
|
+
- Limits: up to 30 images (each ≤4K), up to 10 videos (≤30s combined), up to 10 audio clips (≤30s combined); up to 50 materials total.
|
|
51
|
+
- Role mapping is mandatory, one line per material:
|
|
52
|
+
- `@Image N defines <subject>'s <appearance, clothing, structure, or material>.`
|
|
53
|
+
- `@Video N defines <motion, camera movement, or pacing>.`
|
|
54
|
+
- `@Audio N defines <character or sound type>'s <voice, dialogue, ambience, or music>.`
|
|
55
|
+
- Add exclusions when a material's people/background could leak: "Do not use the image background." / "Do not use the people in the image."
|
|
56
|
+
|
|
57
|
+
## Task-locked parameters
|
|
58
|
+
|
|
59
|
+
- **Video editing:** aspect + duration auto-preserve the source. Do NOT declare AR / duration / shot-count headers. Use `[Edit Goal]` / `[Source Video Role]` / `[Target Material Role]` / `[Edit Scope]` / `[Content to Preserve]`.
|
|
60
|
+
- **First-frame / first-and-last-frame:** AR comes from the FIRST image. Duration CAN be set.
|
|
61
|
+
- **Video extension:** AR auto-preserves the input; duration CAN be set.
|
|
62
|
+
|
|
63
|
+
## Where to run in Kolbo
|
|
64
|
+
|
|
65
|
+
Same routing as Seedance 2 (`first_last_frame` / `elements` / `image_to_video` / `text_to_video`). Pair Visual DNA with `generate_elements` and tag `@<dna-name>` inside the prompt.
|
|
@@ -123,7 +123,7 @@ If the user gives a **product URL** instead of a photo, see `workflows/research-
|
|
|
123
123
|
|
|
124
124
|
## Multi-Slot Board Method (structured shot specs + character consistency)
|
|
125
125
|
|
|
126
|
-
For any multi-shot UGC / review / how-to where the SAME presenter must stay identical across shots, compose the prompt as explicit **slots** and lock identity with a **board-first** pass.
|
|
126
|
+
For any multi-shot UGC / review / how-to where the SAME presenter must stay identical across shots, compose the prompt as explicit **slots** and lock identity with a **board-first** pass. This is a prompt-only convention — no special MCP mode; it uses `generate_image` (board) + `generate_elements` / `generate_video_from_image` (per-slot animate) that already exist.
|
|
127
127
|
|
|
128
128
|
### 1. Structured input slots
|
|
129
129
|
|
|
@@ -42,7 +42,7 @@ client share a filesystem. Choose by transport:
|
|
|
42
42
|
|---|---|
|
|
43
43
|
| **Local (stdio) install** — `npx @kolbo/mcp` on the same machine | `upload_media` with the absolute path |
|
|
44
44
|
| **Remote connector + you can run shell commands** (Claude Code, Codex, Cursor, CI) | `create_upload_ticket`, then POST each file to `upload_url` |
|
|
45
|
-
| **Remote connector, no filesystem** (claude.ai web/mobile) | `media_upload_widget` — the user picks the file
|
|
45
|
+
| **Remote connector, no filesystem** (claude.ai web/mobile) | `media_upload_widget` — the user picks the file |
|
|
46
46
|
|
|
47
47
|
`create_upload_ticket` returns `upload_url` + a short-lived `token`. Upload with
|
|
48
48
|
multipart field `file` and `Authorization: Bearer <token>`; the stable CDN URL comes
|
|
@@ -50,20 +50,9 @@ back at `media.url`. One POST per file, ticket reusable for a batch. Then pass t
|
|
|
50
50
|
URLs to any generation tool.
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
curl -X POST "<upload_url>" -H "Authorization: Bearer <token>" -F "file=@/abs/path/clip.mp3
|
|
53
|
+
curl -X POST "<upload_url>" -H "Authorization: Bearer <token>" -F "file=@/abs/path/clip.mp3"
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
**Declare the type.** `curl` labels the part from its own mime table and falls back to
|
|
57
|
-
`application/octet-stream` for anything missing from it — `.mp3` included — which the
|
|
58
|
-
endpoint can reject as an unsupported type. Append `;type=<mime>` (`audio/mpeg`,
|
|
59
|
-
`audio/wav`, `video/mp4`, `image/png`, `application/pdf`, …) and it never comes up.
|
|
60
|
-
|
|
61
|
-
**Pace a batch.** The upload endpoint is rate limited — the ticket response says by
|
|
62
|
-
how much in `rate_limit` (currently 40 uploads per 60s). Firing 55 files back to
|
|
63
|
-
back stalls at file 41. Sleep ~2s between files, or read the 429: it carries a
|
|
64
|
-
`Retry-After` header and `retry_after_seconds` in the body. Wait exactly that long
|
|
65
|
-
and continue — a 429 is a "not yet", not a failed upload, and nothing was charged.
|
|
66
|
-
|
|
67
56
|
Do **not** fall back to `upload_media`'s `source_base64` for anything but a tiny file —
|
|
68
57
|
it pushes the whole file through the model's context, twice. And do not reach for
|
|
69
58
|
cloud credentials or a bucket of your own; the ticket is the sanctioned path.
|
|
@@ -16,7 +16,8 @@ const { widgetPage } = require('../html');
|
|
|
16
16
|
* model, model_icon, prompt, count,
|
|
17
17
|
* settings: { duration, resolution, aspect_ratio, quality, audio, voice, mode,
|
|
18
18
|
* enhance_prompt, web_search, visual_dna, moodboard, preset, cinematic },
|
|
19
|
-
*
|
|
19
|
+
* reference_images, // all reference thumbnail URLs (optional)
|
|
20
|
+
* reference_image, // legacy single-thumbnail fallback
|
|
20
21
|
* urls, thumbnail_url, title, duration, credits_used,
|
|
21
22
|
* tracks: [{ title, duration, thumbnail_url, model }], // optional audio metadata by URL index
|
|
22
23
|
* scenes: [{ scene_number, title, image_urls, video_urls }],
|
|
@@ -135,7 +136,11 @@ function renderChips(sc) {
|
|
|
135
136
|
}
|
|
136
137
|
if (s.mode) h += chip(esc(s.mode));
|
|
137
138
|
if (sc.count > 1) h += chip('×' + sc.count);
|
|
138
|
-
|
|
139
|
+
var refs = Array.isArray(sc.reference_images) && sc.reference_images.length
|
|
140
|
+
? sc.reference_images : (sc.reference_image ? [sc.reference_image] : []);
|
|
141
|
+
for (var i = 0; i < refs.length; i++) {
|
|
142
|
+
h += '<img class="k-ref-thumb" src="' + esc(refs[i]) + '" alt="" loading="lazy" title="Reference image ' + (i + 1) + ' of ' + refs.length + '" onerror="this.style.display=\\'none\\'">';
|
|
143
|
+
}
|
|
139
144
|
el('chips').innerHTML = h;
|
|
140
145
|
}
|
|
141
146
|
function chip(inner) { return '<span class="k-chip">' + inner + '</span>'; }
|
package/src/client.js
CHANGED
|
@@ -47,6 +47,26 @@ function isAbortError(err) {
|
|
|
47
47
|
return err && (err.name === 'TimeoutError' || err.name === 'AbortError');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function composeSignals(signals) {
|
|
51
|
+
const active = signals.filter(Boolean);
|
|
52
|
+
if (active.length === 0) return { signal: undefined, cleanup: () => {} };
|
|
53
|
+
if (active.length === 1) return { signal: active[0], cleanup: () => {} };
|
|
54
|
+
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const onAbort = () => controller.abort();
|
|
57
|
+
for (const item of active) {
|
|
58
|
+
if (item.aborted) {
|
|
59
|
+
controller.abort();
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
item.addEventListener('abort', onAbort, { once: true });
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
signal: controller.signal,
|
|
66
|
+
cleanup: () => active.forEach(item => item.removeEventListener('abort', onAbort))
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
// ---------------------------------------------------------------------------
|
|
51
71
|
// 429 handling
|
|
52
72
|
// ---------------------------------------------------------------------------
|
|
@@ -279,18 +299,18 @@ class KolboClient {
|
|
|
279
299
|
return false;
|
|
280
300
|
}
|
|
281
301
|
|
|
282
|
-
async request(method, reqPath, body = null) {
|
|
302
|
+
async request(method, reqPath, body = null, requestOptions = {}) {
|
|
283
303
|
if (!this.apiKey) await this._ensureLogin();
|
|
284
|
-
const result = await this._doRequest(method, reqPath, body);
|
|
304
|
+
const result = await this._doRequest(method, reqPath, body, requestOptions);
|
|
285
305
|
|
|
286
306
|
// On 401, try re-reading auth store and retry once
|
|
287
307
|
if (result._status === 401 && this._tryRefreshKey()) {
|
|
288
|
-
return this._doRequest(method, reqPath, body);
|
|
308
|
+
return this._doRequest(method, reqPath, body, requestOptions);
|
|
289
309
|
}
|
|
290
310
|
return result;
|
|
291
311
|
}
|
|
292
312
|
|
|
293
|
-
async _doRequest(method, reqPath, body = null) {
|
|
313
|
+
async _doRequest(method, reqPath, body = null, requestOptions = {}) {
|
|
294
314
|
const url = `${this.baseUrl}${reqPath}`;
|
|
295
315
|
const headers = {
|
|
296
316
|
'X-API-Key': this.apiKey,
|
|
@@ -313,21 +333,32 @@ class KolboClient {
|
|
|
313
333
|
options.body = JSON.stringify(body);
|
|
314
334
|
}
|
|
315
335
|
|
|
316
|
-
|
|
336
|
+
const callerSignal = requestOptions.ignoreCallerSignal ? null : progress.signal();
|
|
337
|
+
const requestTimeoutMs = requestOptions.timeoutMs || REQUEST_TIMEOUT_MS;
|
|
338
|
+
const composed = composeSignals([callerSignal, timeoutSignal(requestTimeoutMs)]);
|
|
339
|
+
options.signal = composed.signal;
|
|
317
340
|
|
|
318
341
|
let response;
|
|
319
342
|
try {
|
|
320
343
|
response = await fetch(url, options);
|
|
321
344
|
} catch (err) {
|
|
322
345
|
if (isAbortError(err)) {
|
|
346
|
+
if (callerSignal?.aborted) {
|
|
347
|
+
throw new KolboApiError(
|
|
348
|
+
`Request cancelled by caller: ${method} ${reqPath}`,
|
|
349
|
+
{ code: 'REQUEST_CANCELLED', status: 499 }
|
|
350
|
+
);
|
|
351
|
+
}
|
|
323
352
|
throw new KolboApiError(
|
|
324
|
-
`Request timed out after ${
|
|
353
|
+
`Request timed out after ${requestTimeoutMs / 1000}s: ${method} ${reqPath}. ` +
|
|
325
354
|
'The job may still be running server-side — poll get_generation_status before retrying. ' +
|
|
326
355
|
'Raise KOLBO_HTTP_TIMEOUT_MS if this is a legitimately slow endpoint.',
|
|
327
356
|
{ code: 'REQUEST_TIMEOUT', status: 504 }
|
|
328
357
|
);
|
|
329
358
|
}
|
|
330
359
|
throw err;
|
|
360
|
+
} finally {
|
|
361
|
+
composed.cleanup();
|
|
331
362
|
}
|
|
332
363
|
|
|
333
364
|
let data;
|
|
@@ -367,16 +398,37 @@ class KolboClient {
|
|
|
367
398
|
const generationId = data?.generation_id || data?.generationId;
|
|
368
399
|
if (method === 'POST' && generationId) {
|
|
369
400
|
await progress.generation(generationId);
|
|
401
|
+
if (!requestOptions.suppressGenerationTracking) {
|
|
402
|
+
progress.trackGeneration(generationId, () => this._cancelAfterCallerAbort(generationId));
|
|
403
|
+
}
|
|
370
404
|
}
|
|
371
405
|
return data;
|
|
372
406
|
}
|
|
373
407
|
|
|
374
|
-
async
|
|
375
|
-
|
|
408
|
+
async _cancelAfterCallerAbort(generationId) {
|
|
409
|
+
try {
|
|
410
|
+
await this._doRequest(
|
|
411
|
+
'POST',
|
|
412
|
+
`/v1/generate/${encodeURIComponent(generationId)}/cancel`,
|
|
413
|
+
{},
|
|
414
|
+
{
|
|
415
|
+
ignoreCallerSignal: true,
|
|
416
|
+
suppressGenerationTracking: true,
|
|
417
|
+
timeoutMs: 30000
|
|
418
|
+
}
|
|
419
|
+
);
|
|
420
|
+
} catch (_) {
|
|
421
|
+
// The host has already cancelled the visible tool call. This is a
|
|
422
|
+
// best-effort cleanup and must never become an unhandled rejection.
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async post(reqPath, body, requestOptions) {
|
|
427
|
+
return this.request('POST', reqPath, body, requestOptions);
|
|
376
428
|
}
|
|
377
429
|
|
|
378
|
-
async get(reqPath) {
|
|
379
|
-
return this.request('GET', reqPath);
|
|
430
|
+
async get(reqPath, requestOptions) {
|
|
431
|
+
return this.request('GET', reqPath, null, requestOptions);
|
|
380
432
|
}
|
|
381
433
|
|
|
382
434
|
async put(reqPath, body = null) {
|
|
@@ -426,16 +478,24 @@ class KolboClient {
|
|
|
426
478
|
const body = formData.getBuffer();
|
|
427
479
|
headers['Content-Length'] = String(body.length);
|
|
428
480
|
|
|
481
|
+
const callerSignal = progress.signal();
|
|
482
|
+
const composed = composeSignals([callerSignal, timeoutSignal(UPLOAD_TIMEOUT_MS)]);
|
|
429
483
|
let response;
|
|
430
484
|
try {
|
|
431
485
|
response = await fetch(url, {
|
|
432
486
|
method: 'POST',
|
|
433
487
|
headers,
|
|
434
488
|
body,
|
|
435
|
-
signal:
|
|
489
|
+
signal: composed.signal
|
|
436
490
|
});
|
|
437
491
|
} catch (err) {
|
|
438
492
|
if (isAbortError(err)) {
|
|
493
|
+
if (callerSignal?.aborted) {
|
|
494
|
+
throw new KolboApiError(
|
|
495
|
+
`Upload cancelled by caller: POST ${reqPath}`,
|
|
496
|
+
{ code: 'REQUEST_CANCELLED', status: 499 }
|
|
497
|
+
);
|
|
498
|
+
}
|
|
439
499
|
throw new KolboApiError(
|
|
440
500
|
`Upload timed out after ${UPLOAD_TIMEOUT_MS / 1000}s: POST ${reqPath} ` +
|
|
441
501
|
`(${Math.round(body.length / 1024)}KB). Raise KOLBO_UPLOAD_TIMEOUT_MS for slow links.`,
|
|
@@ -443,6 +503,8 @@ class KolboClient {
|
|
|
443
503
|
);
|
|
444
504
|
}
|
|
445
505
|
throw err;
|
|
506
|
+
} finally {
|
|
507
|
+
composed.cleanup();
|
|
446
508
|
}
|
|
447
509
|
|
|
448
510
|
let data;
|
|
@@ -476,6 +538,11 @@ class KolboClient {
|
|
|
476
538
|
throw apiError;
|
|
477
539
|
}
|
|
478
540
|
|
|
541
|
+
const generationId = data?.generation_id || data?.generationId;
|
|
542
|
+
if (generationId) {
|
|
543
|
+
await progress.generation(generationId);
|
|
544
|
+
progress.trackGeneration(generationId, () => this._cancelAfterCallerAbort(generationId));
|
|
545
|
+
}
|
|
479
546
|
return data;
|
|
480
547
|
}
|
|
481
548
|
}
|
package/src/index.js
CHANGED
|
@@ -127,7 +127,7 @@ function createServer(opts = {}) {
|
|
|
127
127
|
'7. SESSION CONTINUITY — one task, one session, always: every generation tool returns a `session_id`. For ANY follow-up, refinement, retry, or next step on the SAME task, pass that session_id back — never start fresh. BATCH RULE (critical): when a single user request produces multiple parallel generations (e.g. "animate these 5 images", "generate 3 variants"), do NOT launch them all at once without a session_id. Instead: (1) run the FIRST generation without session_id to create the session, (2) capture the session_id from its response, (3) pass that session_id to ALL remaining generations in the batch. This keeps the entire batch in one session. Exception: only omit session_id and start fresh when the user explicitly starts an unrelated new task.',
|
|
128
128
|
'8. LOCAL FILES / REFERENCE MEDIA — HOW TO HANDLE EVERY CASE. (A) User has a LOCAL file (audio, video, image, document) on their machine. What matters is WHERE THIS SERVER RUNS, not what your client can do — your own filesystem access is irrelevant if the server is somewhere else. On a LOCAL stdio install (server and client share a machine) → call `upload_media` with the absolute path, or pass the path straight to tools like `transcribe_audio` that accept local paths. Over a REMOTE connector the server cannot see that path no matter how capable you are, so a local path will always fail: if you can run shell commands or issue HTTP requests → call `create_upload_ticket` and POST the file to the returned upload_url yourself (fastest, no user interaction); if you cannot → call `media_upload_widget` IMMEDIATELY, the user uploads, and a `media.kolbo.ai` CDN URL comes back for any follow-up call. (B) You already have a public URL (media.kolbo.ai, any CDN, any direct link) → pass it directly; all Kolbo tools accept public URLs. NEVER search for DO Spaces keys, DigitalOcean credentials, or server-side upload credentials. NEVER ask the user to put the file on Google Drive, Dropbox, or Loom. NEVER invent or guess a URL. NEVER base64 anything but a tiny file — it costs context in proportion to file size; use the ticket or the widget instead.',
|
|
129
129
|
'9. MODEL SELECTION — ROUTE BY THE STRENGTHS SUMMARY, NEVER BY THE BADGE OR THE PRICE TAG: ALWAYS pass a specific `model` on every generation tool — do NOT omit it (omitting falls back to "Smart Select" auto-routing, which hides the choice from the user; use it ONLY if the user explicitly asks you to auto-pick). To choose: call `list_models` with the matching `type` and read each model\'s STRENGTHS SUMMARY — the "— …" clause printed after the credit cost. That summary IS the routing instruction: match it against what the user actually asked for (subject, style, motion, length, quality bar, speed), then pick the CHEAPEST model whose summary covers the task. `[NEW]` and `[RECOMMENDED]` badges, a high credit number, and "flagship"/"most intelligent" wording are NOT selection signals — never pick a model because it is newest, biggest or most expensive. Escalate to a premium/frontier model only when the user explicitly asks for maximum quality, or when no cheaper summary covers the requirement. Models printed under "Named-only" (no summary) are opt-in: use them only when the user names them. TEXT/CHAT: `chat_send_message` bills PER TOKEN, so the listed credit number is not the cost — a frontier text model (Claude Fable 5, GPT-5.6 Sol, Pro-class) costs 5-30x a mid-tier one per reply. Default ordinary chat (writing, brainstorming, Q&A, summarising) to a balanced mid-tier model and reserve the frontier tier for hard reasoning or long-form code the user asked for.',
|
|
130
|
-
'10. IMAGE EDITING: for ANY prompt-driven / content edit of an existing image — "make it night", changing scene/lighting/colors, adding/removing/replacing objects, restyling — use `generate_image_edit` (
|
|
130
|
+
'10. IMAGE EDITING: for ANY prompt-driven / content edit of an existing image — "make it night", changing scene/lighting/colors, adding/removing/replacing objects, restyling — use `generate_image_edit`. Auto-pick only Nano Banana 2 (`nano-banana-2` / `nano-banana-2-image-editing`) or GPT Image 2 (`gpt-image-2` / `gpt-image-2/edit`) for photoreal photo edits, object removal, keep-subject/remove-others, or crowd cleanup. Do NOT auto-pick Flux 2 / flux-2/edit / Flux Klein — those are generate-from-scratch / style, named-only for editing. Do NOT use `edit_image` for content edits — `edit_image` is ONLY for mechanical enhancements (upscale, expand/outpaint, remove-background, skin retouch). Its `magic_edit` operation is deprecated in favor of `generate_image_edit`. EXPANDING AN IMAGE: to widen/extend/uncrop an image or fit it into a wider frame while KEEPING the existing artwork, use `edit_image` with operation="zoom_out" (outpainting — original pixels preserved; size it with `zoom_out_percentage` or the `expand_left/right/top/bottom` pixel args). The "reframe" operation is NOT this: it re-generates the whole picture at a new aspect ratio and the subject comes back re-imagined. Only pick "reframe" when the user wants the shot re-taken, never when they want their image extended.',
|
|
131
131
|
'11. PRESET CONTRACT: if the user asks for a preset, names a preset, or says to use one of their/Kolbo presets, you MUST call `list_presets` with the matching type before generation, resolve the named or closest matching preset, and pass its exact returned `id` as `preset_id`. Use type="image" for generate_image and type="image_edit" for generate_image_edit. Never silently ignore a preset request, never invent an id, and never claim a preset was applied unless `preset_id` was present in the generation call.'
|
|
132
132
|
].join('\n')
|
|
133
133
|
});
|
package/src/polling.js
CHANGED
|
@@ -80,7 +80,7 @@ async function pollUntilDone(client, generationId, options = {}) {
|
|
|
80
80
|
}
|
|
81
81
|
const backoff = Math.min(interval * Math.pow(1.5, Math.min(transientFailures - 1, 5)), 30000);
|
|
82
82
|
await progress.tick(); // same keepalive as the normal path — backoff waits up to 30s
|
|
83
|
-
await
|
|
83
|
+
await progress.wait(backoff);
|
|
84
84
|
continue;
|
|
85
85
|
}
|
|
86
86
|
// Non-transient (auth, 4xx other than 408/425/429) — bubble up.
|
|
@@ -109,7 +109,7 @@ async function pollUntilDone(client, generationId, options = {}) {
|
|
|
109
109
|
// cadence could return at 60s). That is the difference between landing
|
|
110
110
|
// inside the caller's transport window and blowing straight through it.
|
|
111
111
|
const remaining = timeout - (Date.now() - startTime);
|
|
112
|
-
await
|
|
112
|
+
await progress.wait(Math.max(0, Math.min(interval, remaining)));
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
package/src/progress.js
CHANGED
|
@@ -2,8 +2,79 @@ const { AsyncLocalStorage } = require('async_hooks');
|
|
|
2
2
|
|
|
3
3
|
const storage = new AsyncLocalStorage();
|
|
4
4
|
|
|
5
|
-
function run(extra, fn) {
|
|
6
|
-
return storage.run(extra,
|
|
5
|
+
async function run(extra, fn) {
|
|
6
|
+
return storage.run(extra, async () => {
|
|
7
|
+
try {
|
|
8
|
+
return await fn();
|
|
9
|
+
} finally {
|
|
10
|
+
if (extra?.signal && extra.__kolboAbortListener) {
|
|
11
|
+
extra.signal.removeEventListener('abort', extra.__kolboAbortListener);
|
|
12
|
+
}
|
|
13
|
+
if (extra) {
|
|
14
|
+
delete extra.__kolboAbortListener;
|
|
15
|
+
delete extra.__kolboTrackedGenerations;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function signal() {
|
|
22
|
+
return storage.getStore()?.signal;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function abortError() {
|
|
26
|
+
const error = new Error('The MCP tool call was cancelled by the caller.');
|
|
27
|
+
error.name = 'AbortError';
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Wait without keeping a cancelled tool call alive until the next poll tick. */
|
|
32
|
+
function wait(ms) {
|
|
33
|
+
const callerSignal = signal();
|
|
34
|
+
if (!callerSignal) return new Promise(resolve => setTimeout(resolve, ms));
|
|
35
|
+
if (callerSignal.aborted) return Promise.reject(abortError());
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
callerSignal.removeEventListener('abort', onAbort);
|
|
40
|
+
resolve();
|
|
41
|
+
}, ms);
|
|
42
|
+
const onAbort = () => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
callerSignal.removeEventListener('abort', onAbort);
|
|
45
|
+
reject(abortError());
|
|
46
|
+
};
|
|
47
|
+
callerSignal.addEventListener('abort', onAbort, { once: true });
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Associate a submitted backend generation with the current MCP tool call.
|
|
53
|
+
* If Kolbo Code, Claude, or another MCP host cancels the tool while it is
|
|
54
|
+
* polling, every submitted generation is cancelled server-side as well.
|
|
55
|
+
*/
|
|
56
|
+
function trackGeneration(id, cancel) {
|
|
57
|
+
const extra = storage.getStore();
|
|
58
|
+
if (!extra?.signal || !id || typeof cancel !== 'function') return;
|
|
59
|
+
|
|
60
|
+
if (!extra.__kolboTrackedGenerations) extra.__kolboTrackedGenerations = new Map();
|
|
61
|
+
extra.__kolboTrackedGenerations.set(String(id), cancel);
|
|
62
|
+
|
|
63
|
+
const cancelTracked = () => {
|
|
64
|
+
const tracked = extra.__kolboTrackedGenerations;
|
|
65
|
+
if (!tracked) return;
|
|
66
|
+
extra.__kolboTrackedGenerations = new Map();
|
|
67
|
+
for (const cancelOne of tracked.values()) {
|
|
68
|
+
Promise.resolve().then(cancelOne).catch(() => {});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (!extra.__kolboAbortListener) {
|
|
73
|
+
extra.__kolboAbortListener = cancelTracked;
|
|
74
|
+
extra.signal.addEventListener('abort', cancelTracked, { once: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (extra.signal.aborted) cancelTracked();
|
|
7
78
|
}
|
|
8
79
|
|
|
9
80
|
async function generation(id) {
|
|
@@ -52,4 +123,4 @@ async function sendGenerationProgress(extra, token, id) {
|
|
|
52
123
|
});
|
|
53
124
|
}
|
|
54
125
|
|
|
55
|
-
module.exports = { run, generation, tick };
|
|
126
|
+
module.exports = { run, generation, tick, signal, wait, trackGeneration };
|
package/src/tools/_shared.js
CHANGED
|
@@ -518,7 +518,7 @@ async function modelChipFields(client, model) {
|
|
|
518
518
|
* kind 'image' | 'video' | 'audio' | '3d' | 'scenes'
|
|
519
519
|
* gen the submit response ({ generation_id, poll_interval_hint })
|
|
520
520
|
* client KolboClient (for model icon lookup)
|
|
521
|
-
* model, prompt, count, settings,
|
|
521
|
+
* model, prompt, count, settings, reference_images, estimated_seconds
|
|
522
522
|
* voice resolved voice record { name, thumbnail } (speech only)
|
|
523
523
|
* poll_tool widget-side status tool (default 'get_generation_status')
|
|
524
524
|
* status_args args for poll_tool (default { generation_id, wait: true })
|
|
@@ -542,7 +542,12 @@ async function uiGenerating(p) {
|
|
|
542
542
|
prompt: p.prompt,
|
|
543
543
|
count: p.count || 1,
|
|
544
544
|
settings: p.settings || {},
|
|
545
|
-
reference_image
|
|
545
|
+
// `reference_image` is retained for older widget builds. New widgets render
|
|
546
|
+
// every browser-loadable image supplied to the generation.
|
|
547
|
+
reference_images: Array.isArray(p.reference_images)
|
|
548
|
+
? p.reference_images.filter(Boolean)
|
|
549
|
+
: (p.reference_image ? [p.reference_image] : []),
|
|
550
|
+
reference_image: p.reference_image || p.reference_images?.find(Boolean),
|
|
546
551
|
open_url: buildOpenUrl(p.tool, p.gen),
|
|
547
552
|
};
|
|
548
553
|
// Batch mode (prompts[] fan-out): ONE widget tracks every id in the set.
|
|
@@ -581,7 +586,10 @@ async function uiCompleted(p, textPayload) {
|
|
|
581
586
|
prompt: p.prompt,
|
|
582
587
|
count: p.count || 1,
|
|
583
588
|
settings: p.settings || {},
|
|
584
|
-
|
|
589
|
+
reference_images: Array.isArray(p.reference_images)
|
|
590
|
+
? p.reference_images.filter(Boolean)
|
|
591
|
+
: (p.reference_image ? [p.reference_image] : []),
|
|
592
|
+
reference_image: p.reference_image || p.reference_images?.find(Boolean),
|
|
585
593
|
urls: p.urls,
|
|
586
594
|
thumbnail_url: p.thumbnail_url,
|
|
587
595
|
title: p.title,
|
package/src/tools/generate.js
CHANGED
|
@@ -178,7 +178,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
178
178
|
generation_ids: batch.ids, prompts: batch.ok.map((o) => o.prompt),
|
|
179
179
|
failed_submissions: batch.failed,
|
|
180
180
|
status_args: { generation_ids: batch.ids, wait: true },
|
|
181
|
-
|
|
181
|
+
reference_images
|
|
182
182
|
});
|
|
183
183
|
return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 3) * 1000, timeout: 240000 });
|
|
184
184
|
}
|
|
@@ -188,7 +188,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
188
188
|
if (ui()) return uiGenerating({
|
|
189
189
|
tool: 'generate_image', kind: 'image', gen, client, model, prompt,
|
|
190
190
|
count: num_images, settings: imageSettings(shared),
|
|
191
|
-
|
|
191
|
+
reference_images
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
@@ -221,7 +221,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
221
221
|
'THE tool for ANY prompt-driven / content edit of an existing image — changing the scene ("make it night", "change the sky to sunset"), adding/removing/replacing objects, restyling, recoloring, compositing, or any "edit this image to…" request. This is the image-editing equivalent of generate_image and runs on strong dedicated editing models (nano-banana-2, gpt-image-2). Provide the source image URL(s) in `source_images` and the instruction in `prompt`. Supports Visual DNA profiles, moodboards, and Kolbo image-editing presets. PRESET CONTRACT: if the user requests a preset, call list_presets type="image_edit" and pass its exact id as `preset_id`; never silently omit it. Do NOT use `edit_image` for these — that tool is only for mechanical enhancements (upscale/reframe/remove-background/skin). For a brand-new image from scratch, use generate_image. Returns the edited image URL(s) when complete.',
|
|
222
222
|
{
|
|
223
223
|
prompt: z.string().describe('Description of the edit to apply (e.g., "remove the background", "change the sky to sunset")'),
|
|
224
|
-
model: z.string().optional().describe('Model identifier — REQUIRED in practice: pick a specific model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Many text-to-image ids double as editors: the server auto-routes a base id to its editing variant when source_images is present (e.g. "gpt-image-2" → gpt-image-2/edit, "nano-banana-2" → nano-banana-2
|
|
224
|
+
model: z.string().optional().describe('Model identifier — REQUIRED in practice: pick a specific model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Many text-to-image ids double as editors: the server auto-routes a base id to its editing variant when source_images is present (e.g. "gpt-image-2" → gpt-image-2/edit, "nano-banana-2" → nano-banana-2-image-editing) — passing the bare id is fine, no need to hunt for the "/edit" suffix yourself. BUT this only works for models that actually have a registered edit variant. For prompt-driven photoreal photo edits (object removal, keep-this-person/remove-the-rest, crowd cleanup, inpainting) the ONLY auto-pick defaults are "nano-banana-2" or "gpt-image-2" (use GPT Image 2 when the image needs readable text). Do NOT auto-pick Flux 2 / flux-2/edit / Flux Klein — those are generate-from-scratch / style models; use them only if the user names Flux. If unsure, confirm the model appears in `list_models type="image_editing"` and choose by the strengths summary — Flux edit variants are named-only.'),
|
|
225
225
|
source_images: z.array(z.string()).describe('PIXEL-ACCURATE compositing. Array of source image URLs whose pixel content is composited into the output. **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.** Three modes the model auto-detects from input shape: (1) Single image → edit/transform that image. (2) Multiple images, one base + others → composite the others into the base. (3) Multiple images with no clear base → generate a new scene that pixel-accurately embeds the supplied images at positions described in the prompt. Mode 3 is the canonical pattern for thumbnails / branded compositions where exact-pixel logo + face fidelity matter. Refer to source images in the prompt by ordinal position ("FIRST source image", "SECOND source image") or use @image1/@image2 tags. Add "composite AS-IS, do not redraw or restyle" to lock pixels.'),
|
|
226
226
|
aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "1:1", "16:9", "9:16"). Must be in the chosen model\'s `supported_aspect_ratios` from list_models. Default: "1:1"'),
|
|
227
227
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
|
|
@@ -247,7 +247,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
247
247
|
tool: 'generate_image_edit', kind: 'image', gen, client, model, prompt,
|
|
248
248
|
count: num_images,
|
|
249
249
|
settings: imageSettings({ resolution, aspect_ratio, enhance_prompt, enable_web_search, visual_dna_ids, moodboard_id, preset_id, cinematic }),
|
|
250
|
-
|
|
250
|
+
reference_images: source_images
|
|
251
251
|
});
|
|
252
252
|
|
|
253
253
|
// Multi-source compositing or DNA-anchored edits routinely exceed 120s
|
|
@@ -309,7 +309,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
309
309
|
tool: 'generate_creative_director', kind: 'scenes', gen, client, model, prompt,
|
|
310
310
|
count: scene_count || 4,
|
|
311
311
|
settings: { duration, resolution, aspect_ratio, mode: workflow_type || 'image' },
|
|
312
|
-
|
|
312
|
+
reference_images,
|
|
313
313
|
poll_tool: 'get_creative_director_status',
|
|
314
314
|
status_args: { generation_id: gen.generation_id, wait: true }
|
|
315
315
|
});
|
|
@@ -466,7 +466,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
466
466
|
generation_ids: batch.ids, prompts: batch.ok.map((o) => o.prompt),
|
|
467
467
|
failed_submissions: batch.failed,
|
|
468
468
|
status_args: { generation_ids: batch.ids, wait: true },
|
|
469
|
-
|
|
469
|
+
reference_images
|
|
470
470
|
});
|
|
471
471
|
return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 8) * 1000, timeout: 900000 });
|
|
472
472
|
}
|
|
@@ -476,7 +476,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
476
476
|
if (ui()) return uiGenerating({
|
|
477
477
|
tool: 'generate_video', kind: 'video', gen, client, model, prompt,
|
|
478
478
|
settings: { duration, resolution, aspect_ratio },
|
|
479
|
-
|
|
479
|
+
reference_images
|
|
480
480
|
});
|
|
481
481
|
|
|
482
482
|
// 15 min — Kling O3 4K, Veo 3.1 at higher durations/resolutions, Hailuo 2.3,
|
|
@@ -550,7 +550,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
550
550
|
generation_ids: batch.ids, prompts: batch.ok.map((o) => o.prompt),
|
|
551
551
|
failed_submissions: batch.failed,
|
|
552
552
|
status_args: { generation_ids: batch.ids, wait: true },
|
|
553
|
-
|
|
553
|
+
reference_images: items.map((item) => item.image_url)
|
|
554
554
|
});
|
|
555
555
|
return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 8) * 1000, timeout: 900000 });
|
|
556
556
|
}
|
|
@@ -560,7 +560,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
560
560
|
if (ui()) return uiGenerating({
|
|
561
561
|
tool: 'generate_video_from_image', kind: 'video', gen, client, model, prompt,
|
|
562
562
|
settings: { duration, resolution, aspect_ratio },
|
|
563
|
-
|
|
563
|
+
reference_images: [image_url]
|
|
564
564
|
});
|
|
565
565
|
|
|
566
566
|
// 15 min — same real-world generation times as generate_video (Kling O3 4K,
|
|
@@ -791,7 +791,8 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
791
791
|
|
|
792
792
|
if (ui()) return uiGenerating({
|
|
793
793
|
tool: 'generate_sound', kind: 'audio', gen, client, model, prompt,
|
|
794
|
-
settings: { duration }
|
|
794
|
+
settings: { duration },
|
|
795
|
+
reference_images: seed_reference_image_url ? [seed_reference_image_url] : []
|
|
795
796
|
});
|
|
796
797
|
|
|
797
798
|
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
@@ -1050,7 +1051,11 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1050
1051
|
if (ui()) return uiGenerating({
|
|
1051
1052
|
tool: 'generate_elements', kind: 'video', gen: startResponse, client, model, prompt,
|
|
1052
1053
|
settings: { duration, resolution, aspect_ratio },
|
|
1053
|
-
|
|
1054
|
+
reference_images: [
|
|
1055
|
+
...(reference_images || []),
|
|
1056
|
+
...(keyframes || []).map((keyframe) => keyframe.image_url),
|
|
1057
|
+
...(files || []).filter((source) => /^https?:\/\//i.test(source))
|
|
1058
|
+
]
|
|
1054
1059
|
});
|
|
1055
1060
|
|
|
1056
1061
|
const poll = await pollOrTimedOut(client, startResponse.generation_id, {
|
|
@@ -1134,7 +1139,8 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1134
1139
|
if (ui()) return uiGenerating({
|
|
1135
1140
|
tool: 'generate_first_last_frame', kind: 'video', gen: startResponse, client, model, prompt,
|
|
1136
1141
|
settings: { duration, resolution, aspect_ratio },
|
|
1137
|
-
|
|
1142
|
+
reference_images: [first_frame_url || first_frame, last_frame_url || last_frame]
|
|
1143
|
+
.filter((source) => /^https?:\/\//i.test(source || ''))
|
|
1138
1144
|
});
|
|
1139
1145
|
|
|
1140
1146
|
const poll = await pollOrTimedOut(client, startResponse.generation_id, {
|
|
@@ -1249,7 +1255,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1249
1255
|
if (ui()) return uiGenerating({
|
|
1250
1256
|
tool: 'generate_lipsync', kind: 'video', gen: startResponse, client, model,
|
|
1251
1257
|
prompt: text_prompt, settings: { mode: 'lipsync' },
|
|
1252
|
-
|
|
1258
|
+
reference_images: sourceIsUrl && !/\.(mp4|mov|webm|mkv|avi|m4v)(\?|$)/i.test(source) ? [source] : [],
|
|
1253
1259
|
});
|
|
1254
1260
|
|
|
1255
1261
|
const poll = await pollOrTimedOut(client, startResponse.generation_id, {
|
|
@@ -1354,7 +1360,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1354
1360
|
tool: 'generate_video_from_video', kind: 'video', gen: startResponse, client, model,
|
|
1355
1361
|
prompt: prompt || (preset ? `Subtitles preset: ${preset}` : undefined),
|
|
1356
1362
|
settings: { duration, resolution, aspect_ratio, mode: preset ? 'subtitles' : 'restyle' },
|
|
1357
|
-
|
|
1363
|
+
reference_images: [...(reference_images || []), ...(elements || [])]
|
|
1358
1364
|
});
|
|
1359
1365
|
|
|
1360
1366
|
const poll = await pollOrTimedOut(client, startResponse.generation_id, {
|
|
@@ -1497,7 +1503,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1497
1503
|
if (ui()) return uiGenerating({
|
|
1498
1504
|
tool: 'generate_3d', kind: '3d', gen: startResponse, client, model, prompt,
|
|
1499
1505
|
settings: { mode: mode || (reference_images?.length > 1 ? 'multi' : reference_images?.length === 1 ? 'single' : 'text') },
|
|
1500
|
-
|
|
1506
|
+
reference_images
|
|
1501
1507
|
});
|
|
1502
1508
|
|
|
1503
1509
|
const poll = await pollOrTimedOut(client, startResponse.generation_id, {
|
|
@@ -1643,7 +1649,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1643
1649
|
tool: 'edit_image', kind: 'image', gen, client, model,
|
|
1644
1650
|
prompt: prompt || operation,
|
|
1645
1651
|
settings: { mode: operation, aspect_ratio, scale, resolution },
|
|
1646
|
-
|
|
1652
|
+
reference_images: [image_url, mask_image_url, ...(additional_images || [])].filter(Boolean)
|
|
1647
1653
|
});
|
|
1648
1654
|
|
|
1649
1655
|
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
@@ -1808,7 +1814,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1808
1814
|
tool: 'edit_video', kind: 'video', gen, client, model,
|
|
1809
1815
|
prompt: prompt || operation,
|
|
1810
1816
|
settings: { mode: operation, duration, aspect_ratio, resolution },
|
|
1811
|
-
|
|
1817
|
+
reference_images: image_url ? [image_url] : []
|
|
1812
1818
|
});
|
|
1813
1819
|
|
|
1814
1820
|
const poll = await pollOrTimedOut(client, gen.generation_id, {
|