@kolbo/mcp 1.91.3 → 1.92.0
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 +4 -1
- package/package.json +1 -1
- package/skill/GENERATED.md +1 -1
- package/skill/SKILL.md +1 -1
- package/skill/VERSION +1 -1
- package/skill/assets/filmmaking/shot-card.template.json +3 -0
- package/skill/references/filmmaking/validation.md +14 -0
- package/skill/references/models/seedance.md +11 -7
- package/skill/references/models/seedance25.md +3 -1
- package/skill/references/workflows/filmmaking.md +6 -0
- package/skill/references/workflows/visual-dna.md +4 -0
- package/skill/scripts/filmmaking/lint_prompt.py +49 -3
- package/src/toolAnnotations.js +3 -0
- package/src/tools/_shared.js +45 -11
- package/src/tools/adobe.js +154 -0
- package/src/tools/generate.js +3 -0
- package/src/tools/video-prompt.js +20 -0
- package/src/tools/visual_dna.js +4 -2
package/README.md
CHANGED
|
@@ -264,7 +264,7 @@ Every generation tool also accepts an optional `project_id` arg that routes the
|
|
|
264
264
|
| `blender_get_command_status` | Read bounded command status/result/error plus absolute `expires_at`; records and idempotency claims expire after 24 hours, and `awaiting_approval` means stop and wait for the user |
|
|
265
265
|
|
|
266
266
|
**Premiere Pro & After Effects Bridge**
|
|
267
|
-
Requires the Kolbo Studio panel open in Premiere Pro or After Effects with **AI agents** switched on in its header. Every edit is approved by the editor inside the panel;
|
|
267
|
+
Requires the Kolbo Studio panel open in Premiere Pro or After Effects with **AI agents** switched on in its header. Every edit is approved by the editor inside the panel; scripts are shown in full before they run.
|
|
268
268
|
|
|
269
269
|
| Tool | Description |
|
|
270
270
|
|------|-------------|
|
|
@@ -275,6 +275,9 @@ Requires the Kolbo Studio panel open in Premiere Pro or After Effects with **AI
|
|
|
275
275
|
| `adobe_place_on_timeline` | Import and place one Kolbo media item at the playhead of the work sequence / active comp (no time or track control in v1) |
|
|
276
276
|
| `adobe_create_sequence` | Create and open a Premiere Pro sequence matching the open sequence's settings, no dialog (Premiere only) |
|
|
277
277
|
| `adobe_import_captions` | Import a Kolbo-hosted SRT onto the active Premiere sequence (Premiere only) |
|
|
278
|
+
| `adobe_edit_composition` | After Effects: one undo step of structured edits - comps, timed and trimmed Kolbo media, titles with stroke, solids, transforms, keyframes, audio fades, layer deletion |
|
|
279
|
+
| `adobe_run_script` | Approval-gated ExtendScript for real motion graphics (shape layers, trim paths, text animators, effects, expressions, cameras); the editor reviews the exact code |
|
|
280
|
+
| `adobe_capture_frame` | Render a comp/sequence frame to the Kolbo library so the agent can check its work |
|
|
278
281
|
| `adobe_get_command_status` | Read command status/result/error; `awaiting_approval` means stop and wait for the editor |
|
|
279
282
|
|
|
280
283
|
**Discovery & Account**
|
package/package.json
CHANGED
package/skill/GENERATED.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AUTO-GENERATED — do not edit
|
|
2
2
|
|
|
3
|
-
This tree is mirrored from kolbo-code@
|
|
3
|
+
This tree is mirrored from kolbo-code@972d2b4, the single source of truth.
|
|
4
4
|
Canonical source: packages/opencode/skills/kolbo/
|
|
5
5
|
Distribution: .github/workflows/sync-skill-to-plugin.yml
|
|
6
6
|
|
package/skill/SKILL.md
CHANGED
package/skill/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.17
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Pre-generation audit
|
|
4
4
|
|
|
5
|
+
Read the current creative brief, including rejected concepts and scene-specific
|
|
6
|
+
exceptions. Compare it to the final prompt and actual tool arguments, not the
|
|
7
|
+
assistant's explanation. Run `scripts/filmmaking/lint_prompt.py` against the shot
|
|
8
|
+
card for saved prompts. It checks Total duration/aspect/count, SHOT numbering,
|
|
9
|
+
continuous-take conflicts and exact multilingual tags. Optional card fields:
|
|
10
|
+
`shot_count`, `continuous_take`, dialogue items' `prompt_text` (exact text sent
|
|
11
|
+
to the model, including requested transliteration), and `post_voiceover` (a list
|
|
12
|
+
of exact narration strings excluded from generation). These checks do not prove
|
|
13
|
+
camera semantics, humor, visual quality or model adherence; review those separately.
|
|
14
|
+
|
|
15
|
+
Elements also rejects explicit Total declarations contradicting tool duration,
|
|
16
|
+
aspect or shot flags before submission. Resolve the mismatch against the brief;
|
|
17
|
+
do not strip declarations just to bypass validation. Free-form prompts remain supported.
|
|
18
|
+
|
|
5
19
|
### Story and edit
|
|
6
20
|
|
|
7
21
|
- Does the shot have a necessary dramatic/editorial job?
|
|
@@ -11,6 +11,10 @@ Load this file when the user wants a **Seedance 2 / Seedance 2.0** (ByteDance) v
|
|
|
11
11
|
|
|
12
12
|
**Elements uses this same file.** `generate_elements` is not a second prompt language. Do not write `SCENE CONTEXT` / `OPTICS` / `ACTION` department packs for Elements or Seedance — those are filmmaking audit contracts, not the generation compile shape.
|
|
13
13
|
|
|
14
|
+
## Creative direction takes precedence
|
|
15
|
+
|
|
16
|
+
The current user brief overrides template defaults and illustrative examples. Keep the two-layer organization, but include only relevant locks. State concrete camera trajectory and visible action prominently; optics numbers, equipment names, repetition and word counts are not guarantees of fidelity. Preserve a continuous-shot exception even when other scenes are multishot. For one shot use `Single continuous shot`, `Total: Xs / 1 shot / AR`, one SHOT heading and `multi_shots: false`; for multiple shots use `Multishot ON` and matching counts. AR comes from the brief, never a copied example. Keep dialogue in the user's requested language or phonetic spelling; test pronunciation rather than claiming guaranteed support or impossibility. Narration reserved for post does not belong in the generation prompt.
|
|
17
|
+
|
|
14
18
|
## Universal Rules (apply to EVERY Seedance / Elements prompt)
|
|
15
19
|
|
|
16
20
|
- **NO MUSIC BY DEFAULT (HARD):** Unless the user explicitly asks for music, every final Seedance prompt—including every Elements/reference-driven prompt—must explicitly say `No music. No musical score.` Keep requested dialogue, synchronized production sound, ambience, and SFX; "no music" does not mean "no audio." If the user explicitly requests music, describe that music instead and omit the no-music lock. Never invent background music from cinematic tone alone.
|
|
@@ -26,13 +30,13 @@ Load this file when the user wants a **Seedance 2 / Seedance 2.0** (ByteDance) v
|
|
|
26
30
|
- A prompt with only shot body and no Total / Multishot header is a **failed turn** — rewrite before calling `generate_*`.
|
|
27
31
|
- **MCP `duration` must match the Total line.** Pass `duration: X` (whole seconds) on `generate_video` / `generate_elements` / `generate_video_from_image` equal to the `Xs` in `Total: Xs / …`. Mismatch = wrong-length clip.
|
|
28
32
|
- **Then the Locked Intro** — `[GLOBAL LOOK]` / `[CAST]` / `[LOCATION]` (+ LOCATION MAP / CONTINUITY / PHYSICS for multi-shot) — before any shot. A one-liner `same character throughout` is not a character lock.
|
|
29
|
-
- **
|
|
30
|
-
- **Prompt length**: simple single-idea pieces ~120–280 words. Locked-intro cinematic typically 400–900 words.
|
|
33
|
+
- **Inside each shot:** make the camera trajectory, subject action and timing easy to find. Put a requested signature camera move in the heading. Do not restack GLOBAL LOOK style inside the shot.
|
|
34
|
+
- **Prompt length**: simple single-idea pieces ~120–280 words. Locked-intro cinematic typically 400–900 words. These are examples, not minimum lengths. Do not pad. The selected model catalog cap wins; Seedance 2.5 uses its own adapter.
|
|
31
35
|
- **Shot count is user-directed.** If the user asks for N shots, deliver exactly N in one prompt unless they ask to split.
|
|
32
|
-
- **
|
|
36
|
+
- **Describe the camera behavior requested for each shot.** Preserve intentional static shots; do not replace requested dynamic moves with static dialogue coverage.
|
|
33
37
|
- **Tell Seedance what the camera is NOT doing** (e.g. `no cuts, no zoom, natural head movement`) — this is what locks POV.
|
|
34
|
-
- **
|
|
35
|
-
- **HARD CAP: 10,000 characters TOTAL for the ENTIRE prompt** — measured as one single string including all shots, boilerplate, SFX lines, and the Total lines. It is per PROMPT, not per shot. **Never** split into multiple prompts, code blocks, or "part 1 / part 2" to evade the cap. Count the final prompt before output; if over, trim (cut adjectives, collapse boilerplate, shorten SFX lists,
|
|
38
|
+
- **Control prose defaults to English; exact dialogue and asset tags retain the user-requested language**, wrapped in a copy-ready code block. Reply in the user's language; preserve any explicit request for the prompt language too.
|
|
39
|
+
- **HARD CAP: 10,000 characters TOTAL for the ENTIRE prompt** — measured as one single string including all shots, boilerplate, SFX lines, and the Total lines. It is per PROMPT, not per shot. **Never** split into multiple prompts, code blocks, or "part 1 / part 2" to evade the cap. Count the final prompt before output; if over, trim (cut adjectives, collapse boilerplate, shorten SFX lists, tighten redundant shot prose without dropping user-requested shots) and re-count until it fits.
|
|
36
40
|
|
|
37
41
|
## Locked Intro (DEFAULT for any multi-shot cinematic — including Elements)
|
|
38
42
|
|
|
@@ -138,7 +142,7 @@ Appearance locks WHO. Persona locks HOW THEY BEHAVE — without it Seedance rend
|
|
|
138
142
|
## Dialogue & expression
|
|
139
143
|
|
|
140
144
|
- **Dialogue is PERFORMED by the model, never by a TTS tool.** Quoted lines in the prompt come back as synced speech with lip movement and room tone, together with the SFX you name in AUDIO. Scene dialogue therefore never routes through `generate_speech` or `generate_lipsync` — write the line in quotes inside its shot beat and let Seedance act it.
|
|
141
|
-
- **
|
|
145
|
+
- **Preserve requested dialogue and its language.** If the user requests Hebrew in Latin letters, preserve that phonetic text as dialogue, not an English translation. Native pronunciation and lip-sync require actual output inspection. Do not promise success or claim the language is impossible without current evidence. Offer a separately authorized dubbing pass only when needed; keep narration reserved for post out of the prompt.
|
|
142
146
|
- `list_models` reports `sound_generation_type: "none"` for Seedance 2 / 2.5 because there is no in-app sound toggle (`sound_baked_in: true`). That field does NOT mean the model is silent. Do not read it as a reason to add TTS.
|
|
143
147
|
- For silent tension, deliver it as expression, not speech: `He does not speak. His expression clearly says: "…"`.
|
|
144
148
|
|
|
@@ -231,7 +235,7 @@ Use only the discrete steps. Not "23°" — use 18° or 29°.
|
|
|
231
235
|
|
|
232
236
|
### Camera placement
|
|
233
237
|
|
|
234
|
-
Place
|
|
238
|
+
Place a requested signature CAMERA trajectory prominently in the shot heading, then describe its timing with the subject action. GLOBAL LOOK owns shared lens / stock / grade. No fixed word order guarantees adherence.
|
|
235
239
|
|
|
236
240
|
### Pre-flight checklist (before output)
|
|
237
241
|
|
|
@@ -11,7 +11,7 @@ Load this file when the user wants a **Seedance 2.5** video (they said "2.5" / "
|
|
|
11
11
|
|
|
12
12
|
**Audio:** Seedance 2.5 emits real synced audio. `list_models` shows `sound_generation_type: none` only because there is no in-app toggle (`sound_baked_in: true`) — it does NOT mean the model is silent, and it is never a reason to reach for TTS. Quoted dialogue is PERFORMED (synced voices, lip movement, room tone) alongside the SFX named in AUDIO, so scene dialogue never goes through `generate_speech` or `generate_lipsync`; write the lines in quotes inside their shot beats.
|
|
13
13
|
|
|
14
|
-
**Dialogue language
|
|
14
|
+
**Dialogue language follows the user.** Preserve requested Hebrew or Hebrew-in-Latin transliteration; do not translate it into English or change the spoken content. Pronunciation and lip-sync must be inspected in the generated output, not guaranteed from the prompt. Keep post-production VO out of the generation prompt. Asset tags always retain their exact stored spelling, including `@אביב` / `#ישראל` literally.
|
|
15
15
|
|
|
16
16
|
**Use the cheapest supported tier unless the user selected an output resolution.** Resolution is a credit MULTIPLIER, not a flat rate. Relative to 720p: 480p ×0.44, 1080p ×2.25. A 30s pass costs ~540cr at 480p against ~1230cr at 720p and ~2770cr at 1080p. When no output resolution was selected and 480p is the cheapest supported tier, block the film at 480p, get the user's sign-off on staging, performance and timing, then re-run only the approved cut at a higher delivery resolution if the user explicitly authorizes that resolution increase. Approval of the creative cut alone does not authorize a more expensive resolution. If no output resolution was selected, use the cheapest supported tier from the live catalog even for final work; pass it explicitly.
|
|
17
17
|
|
|
@@ -27,6 +27,8 @@ Load this file when the user wants a **Seedance 2.5** video (they said "2.5" / "
|
|
|
27
27
|
|
|
28
28
|
## Universal Rules (HARD — same as help widget OUTPUT CONTRACT)
|
|
29
29
|
|
|
30
|
+
User-selected shot structure wins over examples. One continuous take uses `Single continuous shot`, `Total: Xs / 1 shot / AR`, one SHOT heading and `multi_shots: false`. Use Multishot ON only for multiple shots. Copy aspect, duration, dialogue and camera direction from the current scene brief. Expand craft blocks only when they resolve a real staging need; do not pad or introduce contradictory locks.
|
|
31
|
+
|
|
30
32
|
- **First lines ALWAYS declare shot structure** (text-to-video / Elements / reference gen — NOT video-edit):
|
|
31
33
|
1. `N connected cinematic shots, Xs total, AR, Multishot ON`
|
|
32
34
|
2. `Total: Xs / N shots / AR`
|
|
@@ -6,6 +6,10 @@ Operate as a filmmaking system, not merely a prompt writer. Preserve project tru
|
|
|
6
6
|
|
|
7
7
|
## Start here
|
|
8
8
|
|
|
9
|
+
For a continuing production, read `.kolbo/creative-brief.md` before compiling or revising scenes. Keep it as a compact working table: stable scene ID, current world/action/tone, first-frame and camera trajectory/framing, shot count, duration/aspect, asset IDs and exact tags, spoken lines versus VO reserved for post, approval state, rejected concepts, and pending job IDs. Update only the dimensions changed by the user. Preserve the brief across compaction; do not put unapproved generations into `.kolbo/production.md`.
|
|
10
|
+
|
|
11
|
+
User direction wins over template defaults and examples. A custom skill supplements this workflow; do not require its missing supporting files without explaining the gap, and never claim to have read them. Check the final prompt and tool arguments against the brief before dispatch. A request for active movement does not require running; bright lighting does not imply pastel colors or restrained action. Replace rejected worlds substantively. Preserve approved scenes and local single-shot exceptions.
|
|
12
|
+
|
|
9
13
|
1. Identify the requested production stage and deliverable.
|
|
10
14
|
2. Read only the reference files required by the routing table below.
|
|
11
15
|
3. Preserve or establish the relevant production truth before writing a shot.
|
|
@@ -115,6 +119,8 @@ When a generation fails:
|
|
|
115
119
|
5. Log the change and verdict.
|
|
116
120
|
6. After repeated failures, redesign the shot: bake the state into an asset, add a staging/layout reference, reduce actions, split the shot, change the angle, or switch model/mode.
|
|
117
121
|
|
|
122
|
+
Validate the revised approach on one representative shot before another batch, within existing authorization and budget; an explicit request for the whole batch wins. Do not silently split a user-requested continuous take, switch their selected model, or add paid tests. Distinguish defects observed in video/audio from hypotheses inferred from the prompt. Completion alone never verifies camera movement, cuts or performance.
|
|
123
|
+
|
|
118
124
|
Do not keep polishing adjectives when the shot is physically or structurally overconstrained.
|
|
119
125
|
|
|
120
126
|
## Validate
|
|
@@ -8,6 +8,10 @@ Visual DNA profiles capture the visual "identity" of a character, style, product
|
|
|
8
8
|
|
|
9
9
|
## Workflow
|
|
10
10
|
|
|
11
|
+
### Creation failure recovery
|
|
12
|
+
|
|
13
|
+
Create DNA through the available tool when the user requested it; do not default to asking for manual wizard work. A reference-preparation error **before submission** means this attempt did not create a profile. For an uncertain submission, reconcile with a personal/project-scoped list and inspect the matching profile before retrying. Existence alone does not establish that an errored call created it. Stop repeating an identical runtime error; report the exact failure and the verified scope, without inventing an auth outage or claiming reconnect will fix it. After success, check the returned ID, exact stored name, type and references; correct confirmed metadata errors in place rather than recreating. A headless wardrobe sheet for a recurring person remains character DNA.
|
|
14
|
+
|
|
11
15
|
1. **Sheet first, then DNA.** For any production asset (character / location / prop), resolve the sheet **preset** (`list_presets` with `search`) and `generate_image` with that `preset_id` — custom instructions live on the preset. Then `create_visual_dna` with the sheet as `character_sheet_url` (max 4 extra images — if the user gives more, pick the 4 most representative **that share the same identity and vibe**; never pass 5+). Optionally video and audio. See **Purity** below before you generate those stills.
|
|
12
16
|
2. **Types**: `character` (default), `style`, `product`, `scene`, `environment`.
|
|
13
17
|
3. **Use** the profile by passing its `id` in `visual_dna_ids` in: `generate_image`, `generate_creative_director`, `generate_elements`, `generate_video_from_image`, `generate_video_from_video`, `generate_first_last_frame`.
|
|
@@ -10,8 +10,9 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
TAG_RE = re.compile(r"@[
|
|
13
|
+
TAG_RE = re.compile(r"@[\w][\w.:-]*(?:\s+\d+)?", re.UNICODE)
|
|
14
14
|
SHOT_RE = re.compile(r"(?im)^\s*(?:SHOT|SEGMENT)\s+(\d+)\b")
|
|
15
|
+
TOTAL_RE = re.compile(r"(?im)^\s*Total:\s*(\d+(?:\.\d+)?)s\s*/\s*(\d+)\s*shots?\s*/\s*(\d+:\d+)\s*$")
|
|
15
16
|
RANGE_RE = re.compile(
|
|
16
17
|
r"(?i)(\d+(?:\.\d+)?)\s*s\s*(?:-|–|—|to)\s*(\d+(?:\.\d+)?)\s*s"
|
|
17
18
|
)
|
|
@@ -98,7 +99,7 @@ def check_adapter(
|
|
|
98
99
|
|
|
99
100
|
def check_contradictions(prompt: str, report: Report) -> None:
|
|
100
101
|
continuous = re.search(
|
|
101
|
-
r"(?i)\b(one|single)\s+(?:unbroken\s+)?continuous\s+take\b|\bno\s+cuts?\b",
|
|
102
|
+
r"(?i)\b(one|single)\s+(?:unbroken\s+)?continuous\s+(?:take|shot)\b|\bno\s+cuts?\b",
|
|
102
103
|
prompt,
|
|
103
104
|
)
|
|
104
105
|
cuts = re.search(r"(?i)\b(hard|match|smash|jump|whip)\s+cut\b|\bhard\s+cuts\b", prompt)
|
|
@@ -144,8 +145,18 @@ def check_stale_language(prompt: str, report: Report) -> None:
|
|
|
144
145
|
|
|
145
146
|
|
|
146
147
|
def check_tags(prompt: str, card: dict[str, Any] | None, report: Report) -> tuple[set[str], set[str]]:
|
|
147
|
-
found = {value.rstrip(".,;:!?") for value in TAG_RE.findall(prompt)}
|
|
148
148
|
expected = expected_tags(card)
|
|
149
|
+
# Exact stored names may contain spaces, punctuation and non-Latin letters.
|
|
150
|
+
# Match those first, then scan the remainder for unregistered tags. Never
|
|
151
|
+
# normalize a canonical name, or accept @maya2 as a match for @maya.
|
|
152
|
+
remaining = prompt
|
|
153
|
+
found: set[str] = set()
|
|
154
|
+
for tag in sorted(expected, key=len, reverse=True):
|
|
155
|
+
pattern = re.escape(tag) + r"(?![\w:-])"
|
|
156
|
+
if re.search(pattern, remaining):
|
|
157
|
+
found.add(tag)
|
|
158
|
+
remaining = re.sub(pattern, "", remaining)
|
|
159
|
+
found.update(value.rstrip(".,;:!?") for value in TAG_RE.findall(remaining))
|
|
149
160
|
for tag in sorted(expected - found):
|
|
150
161
|
report.error("missing_active_asset", f"Shot-card asset is absent from prompt: {tag}")
|
|
151
162
|
for tag in sorted(found - expected):
|
|
@@ -162,6 +173,40 @@ def check_tags(prompt: str, card: dict[str, Any] | None, report: Report) -> tupl
|
|
|
162
173
|
return found, expected
|
|
163
174
|
|
|
164
175
|
|
|
176
|
+
def check_contract(prompt: str, card: dict[str, Any] | None, report: Report) -> None:
|
|
177
|
+
"""Check explicit structure, not subjective direction or rendered quality."""
|
|
178
|
+
totals = [(float(seconds), int(count), aspect) for seconds, count, aspect in TOTAL_RE.findall(prompt)]
|
|
179
|
+
if totals and any(total != totals[0] for total in totals):
|
|
180
|
+
report.error("conflicting_totals", "Opening and closing Total declarations disagree.")
|
|
181
|
+
shots = [int(value) for value in re.findall(r"(?im)^\s*SHOT\s+(\d+)\b", prompt)]
|
|
182
|
+
if shots and shots != list(range(1, len(shots) + 1)):
|
|
183
|
+
report.error("shot_sequence", "SHOT headings must be unique and sequential from 1.")
|
|
184
|
+
if totals and shots and totals[0][1] != len(shots):
|
|
185
|
+
report.error("shot_count_mismatch", "Total shot count differs from the SHOT headings.")
|
|
186
|
+
if not card:
|
|
187
|
+
return
|
|
188
|
+
for duration, count, aspect in totals:
|
|
189
|
+
if card.get("duration_seconds") is not None and duration != card["duration_seconds"]:
|
|
190
|
+
report.error("duration_mismatch", "Prompt Total duration differs from the shot card.")
|
|
191
|
+
if card.get("aspect_ratio") and aspect != card["aspect_ratio"]:
|
|
192
|
+
report.error("aspect_mismatch", "Prompt Total aspect differs from the shot card.")
|
|
193
|
+
if card.get("shot_count") is not None and count != card["shot_count"]:
|
|
194
|
+
report.error("brief_shot_count_mismatch", "Prompt Total shot count differs from the shot card.")
|
|
195
|
+
if card.get("shot_count") is not None and shots and len(shots) != card["shot_count"]:
|
|
196
|
+
report.error("brief_shot_count_mismatch", "SHOT headings differ from the shot card count.")
|
|
197
|
+
if card.get("continuous_take") is True:
|
|
198
|
+
if len(shots) > 1 or any(count != 1 for _, count, _ in totals) or re.search(r"(?i)\bMultishot\s+ON\b", prompt):
|
|
199
|
+
report.error("brief_continuous_take", "The brief requires one continuous take; prompt declares multiple shots.")
|
|
200
|
+
for item in card.get("dialogue", []):
|
|
201
|
+
if isinstance(item, dict):
|
|
202
|
+
text = item.get("prompt_text")
|
|
203
|
+
if isinstance(text, str) and text not in prompt:
|
|
204
|
+
report.error("missing_dialogue", "Exact prompt_text from the dialogue card is missing or rewritten.")
|
|
205
|
+
for text in card.get("post_voiceover", []):
|
|
206
|
+
if isinstance(text, str) and text and text in prompt:
|
|
207
|
+
report.error("post_voiceover_in_prompt", "Narration reserved for post-production appears in the generation prompt.")
|
|
208
|
+
|
|
209
|
+
|
|
165
210
|
def check_timing(prompt: str, card: dict[str, Any] | None, report: Report) -> None:
|
|
166
211
|
duration = card.get("duration_seconds") if card else None
|
|
167
212
|
for start_text, end_text in RANGE_RE.findall(prompt):
|
|
@@ -220,6 +265,7 @@ def main() -> int:
|
|
|
220
265
|
found, expected = check_tags(prompt, card, report)
|
|
221
266
|
check_timing(prompt, card, report)
|
|
222
267
|
check_dialogue_card(card, report)
|
|
268
|
+
check_contract(prompt, card, report)
|
|
223
269
|
|
|
224
270
|
result = {
|
|
225
271
|
"prompt": str(args.prompt.resolve()),
|
package/src/toolAnnotations.js
CHANGED
|
@@ -105,12 +105,15 @@ const OPEN_WORLD_WRITE = [
|
|
|
105
105
|
'publish_html_artifact', 'create_review_share_link', 'blender_capture_viewport',
|
|
106
106
|
// Adobe edits add bins, clips, sequences or caption tracks; none delete or overwrite.
|
|
107
107
|
'adobe_import_media', 'adobe_place_on_timeline', 'adobe_create_sequence', 'adobe_import_captions',
|
|
108
|
+
'adobe_capture_frame',
|
|
108
109
|
];
|
|
109
110
|
|
|
110
111
|
const OPEN_WORLD_DESTRUCTIVE = [
|
|
111
112
|
'share_doc', 'revoke_review_share_link',
|
|
112
113
|
'blender_apply_operations', 'blender_import_media', 'blender_render',
|
|
113
114
|
'blender_undo', 'blender_file_operation', 'blender_execute_python',
|
|
115
|
+
// Can delete layers; approved once per batch in the Kolbo panel.
|
|
116
|
+
'adobe_edit_composition', 'adobe_run_script',
|
|
114
117
|
];
|
|
115
118
|
|
|
116
119
|
const CONTRACT_GROUPS = [
|
package/src/tools/_shared.js
CHANGED
|
@@ -24,7 +24,10 @@ const fs = require('fs');
|
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const net = require('net');
|
|
26
26
|
const dns = require('dns').promises;
|
|
27
|
-
|
|
27
|
+
// Never load Bun's incomplete built-in undici shim. Node uses the installed
|
|
28
|
+
// dispatcher below; Bun uses native fetch pinned to vetted IPs (its node:tls
|
|
29
|
+
// compatibility transport can return empty peer certificates intermittently).
|
|
30
|
+
const { Agent, fetch: undiciFetch } = require('undici/index.js');
|
|
28
31
|
|
|
29
32
|
const MAX_FILE_BYTES = 500 * 1024 * 1024; // 500 MB — larger than visual_dna because
|
|
30
33
|
// lipsync/v2v/transcription accept full
|
|
@@ -201,42 +204,73 @@ async function resolvePublicAddresses(hostname) {
|
|
|
201
204
|
return rows;
|
|
202
205
|
}
|
|
203
206
|
|
|
204
|
-
function pinnedDispatcher(addresses) {
|
|
207
|
+
function pinnedDispatcher(addresses) {
|
|
205
208
|
let cursor = 0;
|
|
206
|
-
return new Agent({
|
|
207
|
-
connect: {
|
|
208
|
-
lookup(_hostname, options, callback) {
|
|
209
|
+
return new Agent({
|
|
210
|
+
connect: {
|
|
211
|
+
lookup(_hostname, options, callback) {
|
|
209
212
|
if (options?.all) return callback(null, addresses);
|
|
210
213
|
const row = addresses[cursor++ % addresses.length];
|
|
211
214
|
return callback(null, row.address, row.family);
|
|
212
215
|
},
|
|
213
216
|
},
|
|
214
217
|
});
|
|
215
|
-
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function release(dispatcher) {
|
|
221
|
+
try { await dispatcher.close(); } catch (_) { /* Cleanup must not mask fetch errors. */ }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function bunFetch(url, addresses, signal) {
|
|
225
|
+
let error;
|
|
226
|
+
for (const row of addresses) {
|
|
227
|
+
const pinned = new URL(url);
|
|
228
|
+
pinned.hostname = row.family === 6 ? `[${row.address}]` : row.address;
|
|
229
|
+
try {
|
|
230
|
+
return await fetch(pinned, {
|
|
231
|
+
redirect: 'manual', signal,
|
|
232
|
+
// Disable environment proxy routing: the connection must use the
|
|
233
|
+
// checked address, while HTTP routing and TLS verification use the
|
|
234
|
+
// original hostname. Never disable certificate verification.
|
|
235
|
+
proxy: '',
|
|
236
|
+
headers: { Host: url.host },
|
|
237
|
+
tls: url.protocol === 'https:' ? {
|
|
238
|
+
serverName: url.hostname.replace(/^\[|\]$/g, ''), rejectUnauthorized: true,
|
|
239
|
+
} : undefined,
|
|
240
|
+
});
|
|
241
|
+
} catch (err) {
|
|
242
|
+
error = err;
|
|
243
|
+
if (signal?.aborted) throw err;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
216
248
|
|
|
217
249
|
async function safeFetch(rawUrl, opts = {}) {
|
|
218
250
|
let current = rawUrl;
|
|
219
251
|
for (let i = 0; i <= MAX_REDIRECTS; i++) {
|
|
220
252
|
const url = assertSafeUrl(current);
|
|
221
253
|
const addresses = await resolvePublicAddresses(url.hostname);
|
|
222
|
-
const dispatcher = pinnedDispatcher(addresses);
|
|
254
|
+
const dispatcher = process.versions.bun ? null : pinnedDispatcher(addresses);
|
|
223
255
|
let res;
|
|
224
256
|
try {
|
|
225
|
-
res =
|
|
257
|
+
res = process.versions.bun
|
|
258
|
+
? await bunFetch(url, addresses, opts.signal)
|
|
259
|
+
: await undiciFetch(current, { redirect: 'manual', signal: opts.signal, dispatcher });
|
|
226
260
|
} catch (err) {
|
|
227
|
-
|
|
261
|
+
if (dispatcher) await release(dispatcher);
|
|
228
262
|
throw err;
|
|
229
263
|
}
|
|
230
264
|
if (res.status >= 300 && res.status < 400 && res.headers.get('location')) {
|
|
231
265
|
const next = new URL(res.headers.get('location'), current).toString();
|
|
232
266
|
await res.body?.cancel().catch(() => {});
|
|
233
|
-
|
|
267
|
+
if (dispatcher) await release(dispatcher);
|
|
234
268
|
current = next;
|
|
235
269
|
continue;
|
|
236
270
|
}
|
|
237
271
|
// close() waits for this response body to be consumed, so schedule it but
|
|
238
272
|
// do not await it before returning the Response to the caller.
|
|
239
|
-
dispatcher
|
|
273
|
+
if (dispatcher) void release(dispatcher);
|
|
240
274
|
return res;
|
|
241
275
|
}
|
|
242
276
|
throw new Error(`Too many redirects fetching ${rawUrl}`);
|
package/src/tools/adobe.js
CHANGED
|
@@ -31,6 +31,114 @@ const mediaUrl = z.string().url().max(4096).optional()
|
|
|
31
31
|
const mediaKind = z.enum(['video', 'image', 'audio']).optional()
|
|
32
32
|
.describe('Media kind. Inferred from the Kolbo record or file extension when omitted.');
|
|
33
33
|
|
|
34
|
+
// ─── adobe_edit_composition operation schema (mirrors kolbo-api adobe/schemas.js) ───
|
|
35
|
+
const seconds = (min = 0) => z.number().finite().min(min).max(3600);
|
|
36
|
+
const color = () => z.array(z.number().finite().min(0).max(1)).length(3).describe('[r, g, b], each 0-1');
|
|
37
|
+
const point = () => z.array(z.number().finite().min(-100000).max(100000)).length(2).describe('[x, y] in composition pixels; [0, 0] is top-left');
|
|
38
|
+
const layerRef = z.union([z.number().int().min(1).max(10000), noControls(128)])
|
|
39
|
+
.describe('Layer name (exact) or 1-based index, 1 = top layer');
|
|
40
|
+
// Titles may span lines; every other control character is refused.
|
|
41
|
+
const TITLE_CONTROL = new RegExp('[\\x00-\\x09\\x0b\\x0c\\x0e-\\x1f]');
|
|
42
|
+
const titleText = z.string().min(1).max(500).refine(
|
|
43
|
+
(value) => value.trim().length > 0 && !TITLE_CONTROL.test(value),
|
|
44
|
+
'Text must be 1-500 characters; newlines allowed, no other control characters.',
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const compOperation = z.discriminatedUnion('op', [
|
|
48
|
+
z.object({
|
|
49
|
+
op: z.literal('comp.create'),
|
|
50
|
+
name: noControls(128),
|
|
51
|
+
width: z.number().int().min(16).max(8192).optional().describe('Default 1920'),
|
|
52
|
+
height: z.number().int().min(16).max(8192).optional().describe('Default 1080'),
|
|
53
|
+
frame_rate: z.number().finite().min(1).max(120).optional().describe('Default 30'),
|
|
54
|
+
duration_seconds: seconds(0.1),
|
|
55
|
+
background_color: color().optional(),
|
|
56
|
+
}).strict(),
|
|
57
|
+
z.object({
|
|
58
|
+
op: z.literal('comp.update'),
|
|
59
|
+
name: noControls(128).optional(),
|
|
60
|
+
duration_seconds: seconds(0.1).optional(),
|
|
61
|
+
background_color: color().optional(),
|
|
62
|
+
}).strict(),
|
|
63
|
+
z.object({
|
|
64
|
+
op: z.literal('layer.add_media'),
|
|
65
|
+
media_id: mediaId,
|
|
66
|
+
url: mediaUrl,
|
|
67
|
+
kind: mediaKind,
|
|
68
|
+
name: noControls(128).optional().describe('Layer name; defaults to the file name. Name layers you will animate later.'),
|
|
69
|
+
start_seconds: seconds().optional().describe('Where the layer starts on the comp timeline. Default 0.'),
|
|
70
|
+
trim_start_seconds: seconds().optional().describe('Seconds skipped at the head of the source clip. Default 0.'),
|
|
71
|
+
duration_seconds: seconds(0.04).optional().describe('Visible length. Default: rest of the source (stills: rest of the comp).'),
|
|
72
|
+
fit: z.enum(['cover', 'contain', 'none']).optional().describe('Scale to fill the frame (cover, default), fit inside it, or keep size.'),
|
|
73
|
+
}).strict(),
|
|
74
|
+
z.object({
|
|
75
|
+
op: z.literal('layer.add_text'),
|
|
76
|
+
text: titleText,
|
|
77
|
+
name: noControls(128).optional(),
|
|
78
|
+
start_seconds: seconds().optional(),
|
|
79
|
+
duration_seconds: seconds(0.04),
|
|
80
|
+
font: noControls(128).optional().describe('PostScript font name. Default "Arial-BoldMT".'),
|
|
81
|
+
font_size: z.number().finite().min(4).max(1000).optional().describe('Default 100'),
|
|
82
|
+
color: color().optional().describe('Default white'),
|
|
83
|
+
stroke_color: color().optional().describe('Outline colour. Default black when stroke_width is set.'),
|
|
84
|
+
stroke_width: z.number().finite().min(0).max(100).optional().describe('Outline in pixels. Use 2-6 whenever text sits over bright or busy footage.'),
|
|
85
|
+
position: point().optional().describe('Centre of the text block. Default: centre of the comp.'),
|
|
86
|
+
}).strict(),
|
|
87
|
+
z.object({
|
|
88
|
+
op: z.literal('layer.add_solid'),
|
|
89
|
+
color: color(),
|
|
90
|
+
name: noControls(128).optional(),
|
|
91
|
+
start_seconds: seconds().optional(),
|
|
92
|
+
duration_seconds: seconds(0.04).optional(),
|
|
93
|
+
}).strict(),
|
|
94
|
+
z.object({
|
|
95
|
+
op: z.literal('layer.update'),
|
|
96
|
+
layer: layerRef,
|
|
97
|
+
new_name: noControls(128).optional(),
|
|
98
|
+
start_seconds: seconds().optional(),
|
|
99
|
+
trim_start_seconds: seconds().optional(),
|
|
100
|
+
duration_seconds: seconds(0.04).optional(),
|
|
101
|
+
position: point().optional(),
|
|
102
|
+
scale: z.number().finite().min(0).max(10000).optional().describe('Percent, uniform'),
|
|
103
|
+
opacity: z.number().finite().min(0).max(100).optional(),
|
|
104
|
+
rotation: z.number().finite().min(-36000).max(36000).optional().describe('Degrees'),
|
|
105
|
+
audio_levels: z.number().finite().min(-96).max(24).optional().describe('dB'),
|
|
106
|
+
enabled: z.boolean().optional(),
|
|
107
|
+
}).strict(),
|
|
108
|
+
z.object({
|
|
109
|
+
op: z.literal('layer.animate'),
|
|
110
|
+
layer: layerRef,
|
|
111
|
+
property: z.enum(['opacity', 'scale', 'position', 'rotation', 'audio_levels'])
|
|
112
|
+
.describe('opacity 0-100, scale percent, rotation degrees, audio_levels dB, position [x, y]'),
|
|
113
|
+
keyframes: z.array(z.object({
|
|
114
|
+
time_seconds: seconds().describe('Comp time of this key'),
|
|
115
|
+
value: z.union([z.number().finite().min(-36000).max(36000), point()]),
|
|
116
|
+
}).strict()).min(1).max(50),
|
|
117
|
+
easing: z.enum(['ease', 'linear']).optional().describe('Default ease'),
|
|
118
|
+
}).strict(),
|
|
119
|
+
z.object({
|
|
120
|
+
op: z.literal('layer.delete'),
|
|
121
|
+
layer: layerRef,
|
|
122
|
+
}).strict(),
|
|
123
|
+
]);
|
|
124
|
+
const compOperations = z.array(compOperation).min(1).max(100)
|
|
125
|
+
.superRefine((ops, ctx) => {
|
|
126
|
+
ops.forEach((operation, opIndex) => {
|
|
127
|
+
if (operation.op !== 'layer.animate') return;
|
|
128
|
+
const wantsPoint = operation.property === 'position';
|
|
129
|
+
operation.keyframes.forEach((key, index) => {
|
|
130
|
+
if (Array.isArray(key.value) !== wantsPoint) {
|
|
131
|
+
ctx.addIssue({
|
|
132
|
+
code: z.ZodIssueCode.custom,
|
|
133
|
+
path: [opIndex, 'keyframes', index, 'value'],
|
|
134
|
+
message: wantsPoint ? 'position keyframes need [x, y]' : `${operation.property} keyframes need a number`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
})
|
|
140
|
+
.refine((ops) => Buffer.byteLength(JSON.stringify(ops), 'utf8') <= 256 * 1024, 'Operations must be at most 256 KiB as JSON.');
|
|
141
|
+
|
|
34
142
|
function text(value) {
|
|
35
143
|
return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
|
|
36
144
|
}
|
|
@@ -162,6 +270,52 @@ function registerAdobeTools(server, client) {
|
|
|
162
270
|
}
|
|
163
271
|
);
|
|
164
272
|
|
|
273
|
+
server.tool(
|
|
274
|
+
'adobe_edit_composition',
|
|
275
|
+
'After Effects only. Apply a batch of structured edits to a composition as ONE undo step: create or update a comp, add Kolbo media (trimmed and timed), text titles and solids, change layer timing/transform/opacity/audio, animate with keyframes, or delete layers. Times are in seconds on the composition timeline. Layers are addressed by the exact name you gave them or by 1-based index (1 = top). New layers stack on top; solids go to the bottom. Operations run in order and stop at the first failure (earlier ones stay applied). The editor approves the whole batch once in the Kolbo panel. Read workflows/adobe.md before the first call; verify with adobe_get_timeline afterwards.',
|
|
276
|
+
{
|
|
277
|
+
session_id: sessionId,
|
|
278
|
+
operations: compOperations,
|
|
279
|
+
idempotency_key: idempotencyKey,
|
|
280
|
+
},
|
|
281
|
+
async (args) => {
|
|
282
|
+
for (const operation of args.operations) {
|
|
283
|
+
if (operation.op === 'layer.add_media') mediaSource(operation, 'adobe_edit_composition layer.add_media');
|
|
284
|
+
}
|
|
285
|
+
return command(client, 'comp.edit', args, { operations: args.operations });
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
server.tool(
|
|
290
|
+
'adobe_run_script',
|
|
291
|
+
'Run ExtendScript inside the connected After Effects (or Premiere Pro) for real motion graphics: shape layers, trim paths, text animators, effects, masks, expressions, cameras, precomps. `code` is a FUNCTION BODY: use log(...) for progress and `return` a JSON-serialisable result. In After Effects the whole script is one undo step. The editor sees the exact code in the Kolbo panel and must approve it. Scripts have full access to the project and the computer, so never read or write files, call system.callSystem, or touch the network unless the user explicitly asked. Read workflows/after-effects-motion.md before writing motion graphics, then verify with adobe_capture_frame.',
|
|
292
|
+
{
|
|
293
|
+
session_id: sessionId,
|
|
294
|
+
code: z.string().min(1).max(64 * 1024).refine(
|
|
295
|
+
(value) => value.trim().length > 0 && Buffer.byteLength(value, 'utf8') <= 64 * 1024,
|
|
296
|
+
'Script must be non-empty and at most 64 KiB as UTF-8.',
|
|
297
|
+
),
|
|
298
|
+
purpose: noControls(500).describe('Plain-language reason shown to the editor next to the code.'),
|
|
299
|
+
idempotency_key: idempotencyKey,
|
|
300
|
+
},
|
|
301
|
+
async (args) => command(client, 'script.run', args, { code: args.code, purpose: args.purpose })
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
server.tool(
|
|
305
|
+
'adobe_capture_frame',
|
|
306
|
+
'Render one frame of the active After Effects composition (PNG) or Premiere Pro sequence (JPEG) and save it to the Kolbo media library. The result carries the image `url` - look at it to check your motion graphics or edit before telling the user it is done. The editor approves it in the Kolbo panel.',
|
|
307
|
+
{
|
|
308
|
+
session_id: sessionId,
|
|
309
|
+
time_seconds: z.number().finite().min(0).max(3600).optional().describe('Frame time. Default: current playhead / comp time.'),
|
|
310
|
+
project_id: z.string().min(1).max(128).regex(SAFE_ID).optional().describe('Kolbo project to file the capture in.'),
|
|
311
|
+
idempotency_key: idempotencyKey,
|
|
312
|
+
},
|
|
313
|
+
async (args) => command(client, 'frame.capture', args, {
|
|
314
|
+
...(args.time_seconds !== undefined ? { time_seconds: args.time_seconds } : {}),
|
|
315
|
+
...(args.project_id ? { project_id: args.project_id } : {}),
|
|
316
|
+
})
|
|
317
|
+
);
|
|
318
|
+
|
|
165
319
|
server.tool(
|
|
166
320
|
'adobe_get_command_status',
|
|
167
321
|
'Read the state, expires_at and bounded result/error of one Adobe command owned by the caller. awaiting_approval is not a polling state: stop and ask the user to approve or deny it in the Kolbo panel.',
|
package/src/tools/generate.js
CHANGED
|
@@ -11,6 +11,7 @@ const { ownedUrl } = require('./owned-url');
|
|
|
11
11
|
const { UI, uiResult, canonicalModelId, assertModelSupportsType, modelInfo, voiceInfo, resolveCatalogAspectRatio } = require('../apps');
|
|
12
12
|
const { modelTypeForEditOperation, assertExecutableEditModel } = require('./editModelCatalog');
|
|
13
13
|
const { withLocalRehost } = require('./local-rehost');
|
|
14
|
+
const { validateVideoPrompt } = require('./video-prompt');
|
|
14
15
|
|
|
15
16
|
// ─── Cinematic Dimensions schema (shared by generate_image + generate_image_edit) ───
|
|
16
17
|
// Kolbo's "Cinema mode": eight independent photographic dimensions, each an OPTIONAL
|
|
@@ -1454,8 +1455,10 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1454
1455
|
session_id: sessionIdField
|
|
1455
1456
|
},
|
|
1456
1457
|
async ({ prompt, model, reference_images, reference_videos, reference_audio_urls, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt = false, visual_dna_ids, resolution, sound_enabled, keyframes, multi_shots, multi_shot_count, session_name, project_id, session_id }) => {
|
|
1458
|
+
validateVideoPrompt({ prompt, duration, aspect_ratio, multi_shots, multi_shot_count });
|
|
1457
1459
|
model = await canonicalModelId(client, model, 'elements'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1458
1460
|
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, 'elements');
|
|
1461
|
+
validateVideoPrompt({ prompt, duration, aspect_ratio, multi_shots, multi_shot_count });
|
|
1459
1462
|
if (!prompt) throw new Error('prompt is required');
|
|
1460
1463
|
|
|
1461
1464
|
// Elements is the one tool that takes all three modalities, and either a
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Validate only explicit, machine-readable declarations. Never infer creative
|
|
2
|
+
// intent from adjectives, require a template, or rewrite a user's prompt.
|
|
3
|
+
function validateVideoPrompt({ prompt, duration, aspect_ratio, multi_shots, multi_shot_count }) {
|
|
4
|
+
const totals = [...prompt.matchAll(/^\s*Total:\s*(\d+(?:\.\d+)?)s\s*\/\s*(\d+)\s*shots?\s*\/\s*(\d+:\d+)\s*$/gim)]
|
|
5
|
+
.map((m) => ({ duration: Number(m[1]), count: Number(m[2]), aspect: m[3] }));
|
|
6
|
+
if (!totals.length) return; // Existing free-form clients remain supported.
|
|
7
|
+
const total = totals[0];
|
|
8
|
+
const errors = [];
|
|
9
|
+
if (totals.some((t) => t.duration !== total.duration || t.count !== total.count || t.aspect !== total.aspect)) errors.push('Total declarations disagree');
|
|
10
|
+
if (duration != null && duration !== total.duration) errors.push('duration differs from prompt Total');
|
|
11
|
+
if (/^\d+:\d+$/.test(aspect_ratio || '') && aspect_ratio !== total.aspect) errors.push('aspect_ratio differs from prompt Total');
|
|
12
|
+
if (multi_shots === false && total.count > 1) errors.push('multi_shots=false conflicts with multiple declared shots');
|
|
13
|
+
if (multi_shots === true && total.count === 1) errors.push('multi_shots=true conflicts with one declared shot');
|
|
14
|
+
if (multi_shot_count != null && multi_shot_count !== total.count) errors.push('multi_shot_count differs from prompt Total');
|
|
15
|
+
const shots = [...prompt.matchAll(/^\s*SHOT\s+(\d+)\b/gim)].map((m) => Number(m[1]));
|
|
16
|
+
if (shots.length && (shots.length !== total.count || shots.some((n, i) => n !== i + 1))) errors.push('SHOT headings disagree with declared count or order');
|
|
17
|
+
if (errors.length) throw new Error(`Video prompt conflict before submission: ${errors.join('; ')}. Reconcile with the user's approved brief; no generation was submitted.`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { validateVideoPrompt };
|
package/src/tools/visual_dna.js
CHANGED
|
@@ -54,11 +54,13 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// Resolve all sources to buffers in parallel.
|
|
57
|
-
const [imageFiles, videoFile, audioFile] = await Promise.all([
|
|
57
|
+
const [imageFiles, videoFile, audioFile] = await Promise.all([
|
|
58
58
|
Promise.all(imageList.map(src => resolveToBuffer(src, 'image', options))),
|
|
59
59
|
video ? resolveToBuffer(video, 'video', options) : Promise.resolve(null),
|
|
60
60
|
audio ? resolveToBuffer(audio, 'audio', options) : Promise.resolve(null)
|
|
61
|
-
])
|
|
61
|
+
]).catch((cause) => {
|
|
62
|
+
throw new Error(`Visual DNA reference preparation failed before submission; this attempt did not create a profile. ${cause.message}`, { cause });
|
|
63
|
+
});
|
|
62
64
|
|
|
63
65
|
const form = new FormData();
|
|
64
66
|
form.append('name', name);
|