@nodaro/shared 2.12.2 → 2.13.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/dist/index.cjs +42 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -2
- package/dist/index.d.ts +91 -2
- package/dist/index.js +39 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/node-preset-extract.test.ts +59 -0
- package/src/__tests__/prompt-affixes.test.ts +32 -0
- package/src/__tests__/video-analysis-shot-craft.test.ts +8 -2
- package/src/__tests__/video-analysis.test.ts +12 -0
- package/src/catalog-projection.ts +3 -0
- package/src/i18n/composition-effects.ar.ts +1 -1
- package/src/i18n/composition-effects.de.ts +1 -1
- package/src/i18n/composition-effects.es.ts +1 -1
- package/src/i18n/composition-effects.fr.ts +1 -1
- package/src/i18n/composition-effects.he.ts +1 -1
- package/src/i18n/composition-effects.hi.ts +1 -1
- package/src/i18n/composition-effects.ja.ts +1 -1
- package/src/i18n/composition-effects.ko.ts +1 -1
- package/src/i18n/composition-effects.pt-BR.ts +1 -1
- package/src/i18n/composition-effects.ru.ts +1 -4
- package/src/i18n/composition-effects.zh-CN.ts +1 -1
- package/src/index.ts +3 -1
- package/src/node-preset-extract.ts +38 -0
- package/src/prompt-affixes.ts +43 -0
- package/src/video-analysis.ts +13 -1
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
extractPresetData,
|
|
5
5
|
PRESET_EXCLUDED_KEYS,
|
|
6
6
|
PRESET_APPLY_CLEAR_KEYS,
|
|
7
|
+
presetApplyClearKeys,
|
|
7
8
|
presetDataMatches,
|
|
8
9
|
} from "../node-preset-extract.js"
|
|
9
10
|
|
|
@@ -164,3 +165,61 @@ describe("presetDataMatches", () => {
|
|
|
164
165
|
expect(presetDataMatches({ prompt: "a" }, {})).toBe(true)
|
|
165
166
|
})
|
|
166
167
|
})
|
|
168
|
+
|
|
169
|
+
describe("presetApplyClearKeys", () => {
|
|
170
|
+
const staticKeys = [...PRESET_APPLY_CLEAR_KEYS]
|
|
171
|
+
const hasAffixes = (keys: readonly string[]) =>
|
|
172
|
+
keys.includes("promptPrefix") && keys.includes("promptSuffix")
|
|
173
|
+
|
|
174
|
+
it("always includes every static clear key, whatever the preset ships", () => {
|
|
175
|
+
for (const data of [
|
|
176
|
+
{},
|
|
177
|
+
{ prompt: "a cat" },
|
|
178
|
+
{ promptPrefix: "Cinematic 35mm still of" },
|
|
179
|
+
{ promptSuffix: ", golden hour" },
|
|
180
|
+
{ prompt: "a cat", promptPrefix: "PRE", promptSuffix: "POST" },
|
|
181
|
+
{ provider: "flux", aspectRatio: "16:9" },
|
|
182
|
+
]) {
|
|
183
|
+
const keys = presetApplyClearKeys(data)
|
|
184
|
+
for (const k of staticKeys) expect(keys, `${k} missing for ${JSON.stringify(data)}`).toContain(k)
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it("owns no prompt content -> does NOT clear the affixes (settings-only preset)", () => {
|
|
189
|
+
expect(hasAffixes(presetApplyClearKeys({}))).toBe(false)
|
|
190
|
+
expect(hasAffixes(presetApplyClearKeys({ provider: "flux", seed: 3 }))).toBe(false)
|
|
191
|
+
// exactly the static set, nothing added
|
|
192
|
+
expect([...presetApplyClearKeys({ provider: "flux" })].sort()).toEqual([...staticKeys].sort())
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it("ships `prompt` -> clears BOTH affixes (the stale-prefix hazard)", () => {
|
|
196
|
+
expect(hasAffixes(presetApplyClearKeys({ prompt: "a cat" }))).toBe(true)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it("ships an affix -> clears both affixes (the other one is then re-set by the spread)", () => {
|
|
200
|
+
expect(hasAffixes(presetApplyClearKeys({ promptPrefix: "PRE" }))).toBe(true)
|
|
201
|
+
expect(hasAffixes(presetApplyClearKeys({ promptSuffix: "POST" }))).toBe(true)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it("ships prompt AND affixes -> clears both affixes", () => {
|
|
205
|
+
expect(hasAffixes(presetApplyClearKeys({ prompt: "a cat", promptPrefix: "PRE", promptSuffix: "POST" }))).toBe(true)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it("never lists `prompt` itself — it is overwritten only when the preset ships it", () => {
|
|
209
|
+
for (const data of [{}, { prompt: "a cat" }, { promptPrefix: "PRE" }]) {
|
|
210
|
+
expect(presetApplyClearKeys(data)).not.toContain("prompt")
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it("treats an explicit `undefined` value as NOT owning prompt content", () => {
|
|
215
|
+
expect(hasAffixes(presetApplyClearKeys({ prompt: undefined }))).toBe(false)
|
|
216
|
+
expect(hasAffixes(presetApplyClearKeys({ promptPrefix: undefined }))).toBe(false)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it("does not mutate PRESET_APPLY_CLEAR_KEYS", () => {
|
|
220
|
+
presetApplyClearKeys({ prompt: "a cat" })
|
|
221
|
+
expect([...PRESET_APPLY_CLEAR_KEYS].sort()).toEqual([...staticKeys].sort())
|
|
222
|
+
expect(PRESET_APPLY_CLEAR_KEYS).not.toContain("promptPrefix")
|
|
223
|
+
expect(PRESET_APPLY_CLEAR_KEYS).not.toContain("promptSuffix")
|
|
224
|
+
})
|
|
225
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { PROMPT_PREFIX_KEY, PROMPT_SUFFIX_KEY, readPromptAffixes } from "../prompt-affixes.js"
|
|
3
|
+
|
|
4
|
+
describe("prompt-affixes keys", () => {
|
|
5
|
+
it("names the node-data keys", () => {
|
|
6
|
+
expect(PROMPT_PREFIX_KEY).toBe("promptPrefix")
|
|
7
|
+
expect(PROMPT_SUFFIX_KEY).toBe("promptSuffix")
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe("readPromptAffixes", () => {
|
|
12
|
+
it("returns both affixes when set", () =>
|
|
13
|
+
expect(readPromptAffixes({ promptPrefix: "PRE", promptSuffix: "POST" })).toEqual({ prefix: "PRE", suffix: "POST" }))
|
|
14
|
+
it("omits blank / non-string values", () =>
|
|
15
|
+
expect(readPromptAffixes({ promptPrefix: " ", promptSuffix: 42 })).toEqual({}))
|
|
16
|
+
it("omits absent keys", () => expect(readPromptAffixes({ prompt: "x" })).toEqual({}))
|
|
17
|
+
it("keeps internal/trailing whitespace of a non-blank affix verbatim", () =>
|
|
18
|
+
expect(readPromptAffixes({ promptPrefix: "RULES:\n\n" })).toEqual({ prefix: "RULES:\n\n" }))
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Presets capture the affixes for free: `extractPresetData` strips only
|
|
23
|
+
* PRESET_EXCLUDED_KEYS + EXECUTION_DATA_KEYS, and the affix keys are in
|
|
24
|
+
* neither. This test pins that contract so a future addition to either
|
|
25
|
+
* exclusion set can't silently drop pre/post text from saved presets.
|
|
26
|
+
*/
|
|
27
|
+
import { extractPresetData } from "../node-preset-extract.js"
|
|
28
|
+
describe("presets capture prompt affixes", () => {
|
|
29
|
+
it("extractPresetData keeps promptPrefix/promptSuffix", () =>
|
|
30
|
+
expect(extractPresetData({ prompt: "p", promptPrefix: "PRE", promptSuffix: "POST", label: "x", executionStatus: "running" }))
|
|
31
|
+
.toEqual({ prompt: "p", promptPrefix: "PRE", promptSuffix: "POST" }))
|
|
32
|
+
})
|
|
@@ -8,10 +8,16 @@ const scene = { startSec: 0, endSec: 4, label: "hook", shotType: "Wide", camera:
|
|
|
8
8
|
const slot = { slotId: "a", label: "A", source: "wired-character", role: "person", description: "d" }
|
|
9
9
|
|
|
10
10
|
describe("shot-craft fields (keys only)", () => {
|
|
11
|
-
it("adds a text-kind vocabulary and a clip transition-in vocabulary
|
|
11
|
+
it("adds a text-kind vocabulary and a clip transition-in vocabulary; the transition enum is v2 (rev 1.5)", () => {
|
|
12
12
|
expect(VIDEO_ANALYSIS_TEXT_KINDS).toEqual(["title", "caption", "lower-third", "subtitle", "logo", "other"])
|
|
13
13
|
expect(VIDEO_ANALYSIS_CLIP_TRANSITIONS_IN).toEqual(["fade"])
|
|
14
|
-
|
|
14
|
+
// Stage 1 shipped "keys only, no enum widened" (a lagging client rejects an
|
|
15
|
+
// unknown VALUE). Rev 1.5 (2026-08-30) widens it on purpose — sequenced
|
|
16
|
+
// behind the client bump — so this pin now tracks the v2 vocabulary.
|
|
17
|
+
expect(VIDEO_ANALYSIS_TRANSITIONS).toEqual([
|
|
18
|
+
"cut", "fade", "dissolve", "wipe", "whip",
|
|
19
|
+
"zoom", "slide", "white-flash", "digital-glitch", "morph", "match", "jump",
|
|
20
|
+
])
|
|
15
21
|
})
|
|
16
22
|
it("window and result accept transitionIn at the CLIP level and onScreenTextKind on a scene", () => {
|
|
17
23
|
const w = windowAnalysisSchema.parse({ transitionIn: "fade", slots: [slot], scenes: [{ ...scene, onScreenText: "HELLO", onScreenTextKind: "title", transitionOut: "cut" }] })
|
|
@@ -275,6 +275,18 @@ describe("cinematography fields (angle / speed / onScreenText / look)", () => {
|
|
|
275
275
|
}
|
|
276
276
|
})
|
|
277
277
|
|
|
278
|
+
it("vocabulary v2 carries the twelve common edits, and the wire neutral is not a member", () => {
|
|
279
|
+
// rev 1.5 Appendix F: absent = nothing asserted; `none` is wire-local to the
|
|
280
|
+
// analyser's roll schema and must never reach the canonical enum.
|
|
281
|
+
expect([...VIDEO_ANALYSIS_TRANSITIONS]).toEqual([
|
|
282
|
+
"cut", "fade", "dissolve", "wipe", "whip",
|
|
283
|
+
"zoom", "slide", "white-flash", "digital-glitch", "morph", "match", "jump",
|
|
284
|
+
])
|
|
285
|
+
expect(VIDEO_ANALYSIS_TRANSITIONS).not.toContain("none" as never)
|
|
286
|
+
expect(windowAnalysisSchema.safeParse({ slots: [slot], scenes: [{ ...baseScene, transitionOut: "none" }] }).success).toBe(false)
|
|
287
|
+
expect(windowAnalysisSchema.safeParse({ slots: [slot], scenes: [{ ...baseScene }] }).success).toBe(true)
|
|
288
|
+
})
|
|
289
|
+
|
|
278
290
|
it("marks the viewpoints where no face is visible — auto-cast reads this", () => {
|
|
279
291
|
// A reference frame shot from behind cannot cast a face, however well framed.
|
|
280
292
|
expect([...VIDEO_ANALYSIS_FACELESS_ANGLES].sort()).toEqual(["from-behind", "over-the-shoulder"])
|
|
@@ -44,5 +44,8 @@ export interface ProjectedCatalog {
|
|
|
44
44
|
options?: ProjectedCatalogOption[]
|
|
45
45
|
/** multi-dim catalogs. */
|
|
46
46
|
fields?: readonly string[]
|
|
47
|
+
/** Per-field option lists. Multi-dim catalogs always carry these; a
|
|
48
|
+
* single-dim catalog carries them when it has secondary parameter
|
|
49
|
+
* fields beside its main picker (transition position/duration/intensity). */
|
|
47
50
|
dimensions?: ProjectedCatalogDimension[]
|
|
48
51
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LocaleCatalogMap } from "./types.js"
|
|
2
2
|
|
|
3
3
|
const map: LocaleCatalogMap = {
|
|
4
|
+
"none": { label: "بدون", description: "بدون تأثير تركيبي" },
|
|
4
5
|
"bursting-through-frame": { label: "اختراق الإطار", description: "تمزق ورقي ثلاثي الأبعاد يكسر الإطار" },
|
|
5
6
|
"breaking-out-of-frame": { label: "الخروج من الإطار", description: "طرف يمتد خارج حدود اللوحة" },
|
|
6
7
|
"pixel-disintegration": { label: "تفكك بكسلي", description: "الموضوع يتحلل إلى جسيمات" },
|
|
@@ -14,7 +15,6 @@ const map: LocaleCatalogMap = {
|
|
|
14
15
|
"floating-fragments": { label: "شظايا عائمة", description: "أجزاء من الجسم تنجرف بعيدا" },
|
|
15
16
|
"silhouette-outline": { label: "ظل خارجي", description: "ظل أسود نقي على خلفية مسطحة" },
|
|
16
17
|
"exploding-particles": { label: "جسيمات منفجرة", description: "الخط الخارجي يتطاير إلى جسيمات" },
|
|
17
|
-
"3x3-grid-collage": { label: "كولاج شبكة 3×3", description: "مونتاج تسع وضعيات على ورقة اتصال" },
|
|
18
18
|
|
|
19
19
|
"matte-painting": { label: "رسم Matte", description: "خلفية مرسومة بـ Matte مدمجة مع Live Action" },
|
|
20
20
|
"double-exposure": { label: "تعريض مزدوج", description: "تعريضان فوتوغرافيان متراكبان مدمجان" },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LocaleCatalogMap } from "./types.js"
|
|
2
2
|
|
|
3
3
|
const map: LocaleCatalogMap = {
|
|
4
|
+
"none": { label: "Keiner", description: "Kein Kompositionseffekt" },
|
|
4
5
|
"bursting-through-frame": { label: "Durch den Rahmen brechen", description: "3D-Papierriss durchbricht den Rahmen" },
|
|
5
6
|
"breaking-out-of-frame": { label: "Aus dem Rahmen ausbrechen", description: "Glied erstreckt sich über den Bildrand" },
|
|
6
7
|
"pixel-disintegration": { label: "Pixel-Zerfall", description: "Motiv löst sich in Partikel auf" },
|
|
@@ -14,7 +15,6 @@ const map: LocaleCatalogMap = {
|
|
|
14
15
|
"floating-fragments": { label: "Schwebende Fragmente", description: "Körper schwebt teilweise davon" },
|
|
15
16
|
"silhouette-outline": { label: "Silhouetten-Umriss", description: "Saubere schwarze Silhouette auf flachem Hintergrund" },
|
|
16
17
|
"exploding-particles": { label: "Explodierende Partikel", description: "Umriss zerstreut sich in Partikel" },
|
|
17
|
-
"3x3-grid-collage": { label: "3x3-Raster-Collage", description: "Kontaktbogen mit 9 Posen-Montage" },
|
|
18
18
|
"matte-painting": { label: "Matte Painting", description: "Kompositiertes Matte-Painting-Hintergrund mit Live-Action verbunden" },
|
|
19
19
|
"double-exposure": { label: "Doppelbelichtung", description: "Zwei geschichtete fotografische Belichtungen verschmolzen" },
|
|
20
20
|
"multiple-exposure": { label: "Mehrfachbelichtung", description: "Drei oder mehr übereinander gestapelte Belichtungen" },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LocaleCatalogMap } from "./types.js"
|
|
2
2
|
|
|
3
3
|
const map: LocaleCatalogMap = {
|
|
4
|
+
"none": { label: "Ninguno", description: "Sin efecto de composición" },
|
|
4
5
|
"bursting-through-frame": { label: "Atravesando el Marco", description: "Rasgado de papel 3D rompiendo el marco" },
|
|
5
6
|
"breaking-out-of-frame": { label: "Rompiendo el Marco", description: "Extremidad se extiende más allá del borde del lienzo" },
|
|
6
7
|
"pixel-disintegration": { label: "Desintegración de Píxeles", description: "Sujeto disolviéndose en partículas" },
|
|
@@ -14,7 +15,6 @@ const map: LocaleCatalogMap = {
|
|
|
14
15
|
"floating-fragments": { label: "Fragmentos Flotantes", description: "Cuerpo derivando parcialmente" },
|
|
15
16
|
"silhouette-outline": { label: "Silueta Contorno", description: "Silueta negra limpia sobre fondo plano" },
|
|
16
17
|
"exploding-particles": { label: "Partículas Explotando", description: "Contorno disipándose en partículas" },
|
|
17
|
-
"3x3-grid-collage": { label: "Collage en Cuadrícula 3x3", description: "Montaje tipo hoja de contactos de 9 poses" },
|
|
18
18
|
|
|
19
19
|
// -------------------- Round 2 --------------------
|
|
20
20
|
"matte-painting": { label: "Matte Painting", description: "Fondo de matte painting compuesto y mezclado con acción real" },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LocaleCatalogMap } from "./types.js"
|
|
2
2
|
|
|
3
3
|
const map: LocaleCatalogMap = {
|
|
4
|
+
"none": { label: "Aucun", description: "Aucun effet de composition" },
|
|
4
5
|
"bursting-through-frame": { label: "Surgissant à travers le cadre", description: "Déchirure de papier 3D brisant le cadre" },
|
|
5
6
|
"breaking-out-of-frame": { label: "Sortant du cadre", description: "Membre s'étendant au-delà de la bordure du tableau" },
|
|
6
7
|
"pixel-disintegration": { label: "Désintégration en pixels", description: "Sujet se dissolvant en particules" },
|
|
@@ -14,7 +15,6 @@ const map: LocaleCatalogMap = {
|
|
|
14
15
|
"floating-fragments": { label: "Fragments flottants", description: "Corps se dispersant partiellement" },
|
|
15
16
|
"silhouette-outline": { label: "Silhouette en contour", description: "Silhouette noire nette sur fond uni" },
|
|
16
17
|
"exploding-particles": { label: "Particules explosives", description: "Contour se dispersant en particules" },
|
|
17
|
-
"3x3-grid-collage": { label: "Collage en grille 3x3", description: "Montage planche-contact de 9 poses" },
|
|
18
18
|
"matte-painting": { label: "Matte painting", description: "Fond peint en matte painting fondu avec prise de vue réelle" },
|
|
19
19
|
"double-exposure": { label: "Double exposition", description: "Deux expositions photographiques superposées et fondues" },
|
|
20
20
|
"multiple-exposure": { label: "Exposition multiple", description: "Trois expositions ou plus empilées" },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LocaleCatalogMap } from "./types.js"
|
|
2
2
|
|
|
3
3
|
const map: LocaleCatalogMap = {
|
|
4
|
+
"none": { label: "ללא", description: "ללא אפקט קומפוזיציה" },
|
|
4
5
|
"bursting-through-frame": { label: "פורץ דרך הפריים", description: "קריעת נייר תלת-ממדית הפורצת את הפריים" },
|
|
5
6
|
"breaking-out-of-frame": { label: "פורץ מחוץ לפריים", description: "גף נמתח מעבר לגבול הקנבס" },
|
|
6
7
|
"pixel-disintegration": { label: "פירוק לפיקסלים", description: "סובייקט מתפרק לחלקיקים" },
|
|
@@ -14,7 +15,6 @@ const map: LocaleCatalogMap = {
|
|
|
14
15
|
"floating-fragments": { label: "שברים מרחפים", description: "גוף נסחף חלקית" },
|
|
15
16
|
"silhouette-outline": { label: "מתאר צללית", description: "צללית שחורה נקייה על רקע שטוח" },
|
|
16
17
|
"exploding-particles": { label: "חלקיקים מתפוצצים", description: "מתאר מתפזר לחלקיקים" },
|
|
17
|
-
"3x3-grid-collage": { label: "קולאז' רשת 3x3", description: "מונטז' של 9 פוזות בסגנון contact sheet" },
|
|
18
18
|
"matte-painting": { label: "Matte Painting", description: "רקע matte-painting מורכב מעורב עם צילום חי" },
|
|
19
19
|
"double-exposure": { label: "חשיפה כפולה", description: "שתי חשיפות צילום שכבתיות מוזגות" },
|
|
20
20
|
"multiple-exposure": { label: "חשיפה מרובה", description: "שלוש חשיפות או יותר מוערמות" },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LocaleCatalogMap } from "./types.js"
|
|
2
2
|
|
|
3
3
|
const map: LocaleCatalogMap = {
|
|
4
|
+
"none": { label: "कोई नहीं", description: "कोई composition effect नहीं" },
|
|
4
5
|
"bursting-through-frame": { label: "Frame से बाहर निकलता हुआ", description: "Frame को तोड़ता 3D paper-tear" },
|
|
5
6
|
"breaking-out-of-frame": { label: "Frame से बाहर निकलना", description: "अंग canvas की सीमा से बाहर निकलता है" },
|
|
6
7
|
"pixel-disintegration": { label: "Pixel Disintegration", description: "Subject कणों में घुलता हुआ" },
|
|
@@ -14,7 +15,6 @@ const map: LocaleCatalogMap = {
|
|
|
14
15
|
"floating-fragments": { label: "तैरते टुकड़े", description: "बदन आंशिक रूप से दूर बहता हुआ" },
|
|
15
16
|
"silhouette-outline": { label: "Silhouette की रेखा", description: "Flat BG पर साफ़ काला silhouette" },
|
|
16
17
|
"exploding-particles": { label: "विस्फोटक कण", description: "रेखा कणों में बिखर रही है" },
|
|
17
|
-
"3x3-grid-collage": { label: "3x3 Grid Collage", description: "Contact-sheet 9-pose montage" },
|
|
18
18
|
|
|
19
19
|
// -------------------- Round 2 --------------------
|
|
20
20
|
"matte-painting": { label: "Matte Painting", description: "Live action के साथ blended composite matte-painted background" },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LocaleCatalogMap } from "./types.js"
|
|
2
2
|
|
|
3
3
|
const map: LocaleCatalogMap = {
|
|
4
|
+
"none": { label: "なし", description: "構図エフェクトなし" },
|
|
4
5
|
"bursting-through-frame": { label: "フレームを突き破る", description: "フレームを破る3D紙破り効果" },
|
|
5
6
|
"breaking-out-of-frame": { label: "フレームから飛び出す", description: "手足がキャンバスの境界を越える" },
|
|
6
7
|
"pixel-disintegration": { label: "ピクセル崩壊", description: "粒子に溶け込む被写体" },
|
|
@@ -14,7 +15,6 @@ const map: LocaleCatalogMap = {
|
|
|
14
15
|
"floating-fragments": { label: "浮かぶ断片", description: "体が部分的に漂い去る" },
|
|
15
16
|
"silhouette-outline": { label: "シルエットの輪郭", description: "フラットな背景上の真っ黒なシルエット" },
|
|
16
17
|
"exploding-particles": { label: "爆発する粒子", description: "粒子に飛散する輪郭" },
|
|
17
|
-
"3x3-grid-collage": { label: "3×3グリッド・コラージュ", description: "コンタクトシート風の9ポーズモンタージュ" },
|
|
18
18
|
|
|
19
19
|
// Additional composition effects
|
|
20
20
|
"matte-painting": { label: "マットペインティング", description: "実写と合成されたマットペイントの背景" },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LocaleCatalogMap } from "./types.js"
|
|
2
2
|
|
|
3
3
|
const map: LocaleCatalogMap = {
|
|
4
|
+
"none": { label: "없음", description: "구도 효과 없음" },
|
|
4
5
|
"bursting-through-frame": { label: "프레임을 뚫고 나오기", description: "프레임을 찢고 나오는 3D 페이퍼-티어 효과입니다" },
|
|
5
6
|
"breaking-out-of-frame": { label: "프레임 밖으로 나오기", description: "사지가 캔버스 경계 너머로 뻗어 나옵니다" },
|
|
6
7
|
"pixel-disintegration": { label: "픽셀 디스인테그레이션", description: "피사체가 입자로 분해되어 흩어집니다" },
|
|
@@ -14,7 +15,6 @@ const map: LocaleCatalogMap = {
|
|
|
14
15
|
"floating-fragments": { label: "플로팅 프래그먼츠", description: "몸이 부분적으로 떠다니며 흩어집니다" },
|
|
15
16
|
"silhouette-outline": { label: "실루엣 아웃라인", description: "평면 배경 위의 깔끔한 검은 실루엣입니다" },
|
|
16
17
|
"exploding-particles": { label: "익스플로딩 파티클", description: "윤곽이 입자로 흩뿌려집니다" },
|
|
17
|
-
"3x3-grid-collage": { label: "3x3 그리드 콜라주", description: "9 포즈 콘택트 시트 몽타주입니다" },
|
|
18
18
|
|
|
19
19
|
// Additional composition effects
|
|
20
20
|
"matte-painting": { label: "매트 페인팅", description: "라이브 액션과 합성된 매트 페인팅 배경입니다" },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LocaleCatalogMap } from "./types.js"
|
|
2
2
|
|
|
3
3
|
const map: LocaleCatalogMap = {
|
|
4
|
+
"none": { label: "Nenhum", description: "Sem efeito de composição" },
|
|
4
5
|
"bursting-through-frame": { label: "Rompendo o Quadro", description: "Rasgo de papel 3D quebrando o quadro" },
|
|
5
6
|
"breaking-out-of-frame": { label: "Saindo do Quadro", description: "Membro se estende além da borda do quadro" },
|
|
6
7
|
"pixel-disintegration": { label: "Desintegração em Pixels", description: "Sujeito se dissolvendo em partículas" },
|
|
@@ -14,7 +15,6 @@ const map: LocaleCatalogMap = {
|
|
|
14
15
|
"floating-fragments": { label: "Fragmentos Flutuando", description: "Corpo se afastando parcialmente em pedaços" },
|
|
15
16
|
"silhouette-outline": { label: "Contorno em Silhueta", description: "Silhueta preta limpa sobre fundo plano" },
|
|
16
17
|
"exploding-particles": { label: "Partículas Explodindo", description: "Contorno se desfazendo em partículas" },
|
|
17
|
-
"3x3-grid-collage": { label: "Colagem em Grade 3x3", description: "Folha-contato com 9 poses" },
|
|
18
18
|
"matte-painting": { label: "Matte Painting", description: "Fundo composto em matte painting integrado a captação real" },
|
|
19
19
|
"double-exposure": { label: "Dupla Exposição", description: "Duas exposições fotográficas sobrepostas e fundidas" },
|
|
20
20
|
"multiple-exposure": { label: "Múltipla Exposição", description: "Três ou mais exposições empilhadas" },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LocaleCatalogMap } from "./types.js"
|
|
2
2
|
|
|
3
3
|
const map: LocaleCatalogMap = {
|
|
4
|
+
"none": { label: "Нет", description: "Без композиционного эффекта" },
|
|
4
5
|
"bursting-through-frame": {
|
|
5
6
|
label: "Прорыв сквозь рамку",
|
|
6
7
|
description: "3D-эффект разорванной бумаги, ломающий рамку",
|
|
@@ -53,10 +54,6 @@ const map: LocaleCatalogMap = {
|
|
|
53
54
|
label: "Взрыв частиц",
|
|
54
55
|
description: "Контур разлетается на частицы",
|
|
55
56
|
},
|
|
56
|
-
"3x3-grid-collage": {
|
|
57
|
-
label: "Коллаж 3×3",
|
|
58
|
-
description: "Контактный лист из 9 поз",
|
|
59
|
-
},
|
|
60
57
|
"matte-painting": {
|
|
61
58
|
label: "Маттовая живопись",
|
|
62
59
|
description: "Композитный нарисованный матовый фон, смешанный с живой съёмкой",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LocaleCatalogMap } from "./types.js"
|
|
2
2
|
|
|
3
3
|
const map: LocaleCatalogMap = {
|
|
4
|
+
"none": { label: "无", description: "无构图效果" },
|
|
4
5
|
"bursting-through-frame": { label: "撕纸破框", description: "3D 撕纸效果冲破画框" },
|
|
5
6
|
"breaking-out-of-frame": { label: "破出画框", description: "肢体伸出画布边缘" },
|
|
6
7
|
"pixel-disintegration": { label: "像素崩解", description: "主体崩解为颗粒" },
|
|
@@ -14,7 +15,6 @@ const map: LocaleCatalogMap = {
|
|
|
14
15
|
"floating-fragments": { label: "漂浮碎片", description: "身体部分飘散远去" },
|
|
15
16
|
"silhouette-outline": { label: "剪影轮廓", description: "纯色背景上的干净黑剪影" },
|
|
16
17
|
"exploding-particles": { label: "粒子爆裂", description: "轮廓向外炸成颗粒" },
|
|
17
|
-
"3x3-grid-collage": { label: "3x3 网格拼贴", description: "9 格姿势接触印样拼贴" },
|
|
18
18
|
"matte-painting": { label: "接景绘画", description: "接景背景与实拍画面合成" },
|
|
19
19
|
"double-exposure": { label: "二次曝光", description: "两层摄影曝光叠合" },
|
|
20
20
|
"multiple-exposure": { label: "多重曝光", description: "三次或更多次曝光叠合" },
|
package/src/index.ts
CHANGED
|
@@ -912,7 +912,9 @@ export {
|
|
|
912
912
|
describeNodeAdjustments,
|
|
913
913
|
} from "./normalize-node-params.js"
|
|
914
914
|
export type { NodeParamAdjustment, NormalizedNodes } from "./normalize-node-params.js"
|
|
915
|
-
export { extractPresetData, PRESET_EXCLUDED_KEYS, PRESET_APPLY_CLEAR_KEYS, presetDataMatches } from "./node-preset-extract.js"
|
|
915
|
+
export { extractPresetData, PRESET_EXCLUDED_KEYS, PRESET_APPLY_CLEAR_KEYS, presetApplyClearKeys, presetDataMatches } from "./node-preset-extract.js"
|
|
916
|
+
export { PROMPT_PREFIX_KEY, PROMPT_SUFFIX_KEY, readPromptAffixes } from "./prompt-affixes.js"
|
|
917
|
+
export type { PromptAffixFields, PromptAffixes } from "./prompt-affixes.js"
|
|
916
918
|
|
|
917
919
|
// --- Factory prompt-snippets (reusable inline prompt fragments) ---
|
|
918
920
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EXECUTION_DATA_KEYS } from "./node-runtime-keys.js"
|
|
2
2
|
import { COMPOSER_PLAN_FIELDS } from "./model-constants.js"
|
|
3
|
+
import { PROMPT_PREFIX_KEY, PROMPT_SUFFIX_KEY } from "./prompt-affixes.js"
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Generated-plan state on composer nodes that a preset must NEITHER capture NOR
|
|
@@ -26,6 +27,43 @@ import { COMPOSER_PLAN_FIELDS } from "./model-constants.js"
|
|
|
26
27
|
*/
|
|
27
28
|
export const PRESET_APPLY_CLEAR_KEYS: readonly string[] = [...COMPOSER_PLAN_FIELDS, "lottieUrl"]
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The three keys that carry a preset's PROMPT CONTENT: the prompt itself plus the pre/post text
|
|
32
|
+
* wrapped around it at run time. A preset "owns prompt content" iff its data defines any of them.
|
|
33
|
+
*/
|
|
34
|
+
const PROMPT_CONTENT_KEYS: readonly string[] = ["prompt", PROMPT_PREFIX_KEY, PROMPT_SUFFIX_KEY]
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The keys applying THIS preset must clear on the node: always `PRESET_APPLY_CLEAR_KEYS`, plus
|
|
38
|
+
* `promptPrefix` / `promptSuffix` when the preset owns prompt content. The dropdown spreads
|
|
39
|
+
* `{ ...clearPatch, ...preset.data }`, so the preset's own affixes (if any) land back on top of
|
|
40
|
+
* the clear — the clear only decides what happens to the keys the preset does NOT set.
|
|
41
|
+
*
|
|
42
|
+
* The hazard this closes: with a STATIC clear set the affixes are named by no patch, so applying
|
|
43
|
+
* preset B (which ships only `prompt`) over a node carrying preset A's `promptPrefix` leaves A's
|
|
44
|
+
* prefix in place, silently wrapping B's prompt. The node then runs text no preset ever authored,
|
|
45
|
+
* and nothing in the UI attributes it — the prefix lives in a collapsed settings section.
|
|
46
|
+
*
|
|
47
|
+
* The rule is ASYMMETRIC on purpose, because the two directions have opposite promises:
|
|
48
|
+
* - A preset that owns prompt content is authoring the whole prompt, affixes included, so any
|
|
49
|
+
* leftover affix from a previous preset is stale — clear both.
|
|
50
|
+
* - A settings-only preset (provider / aspect ratio / seed …) promises to touch nothing about the
|
|
51
|
+
* prompt, so it leaves prompt AND affixes exactly as the user has them.
|
|
52
|
+
* - `prompt` is NEVER cleared, only overwritten when the preset ships it. That is the feature's
|
|
53
|
+
* core promise: apply a preset that ships only affixes and your typed prompt is kept, wrapped.
|
|
54
|
+
*
|
|
55
|
+
* Pure; `PRESET_APPLY_CLEAR_KEYS` itself stays a fixed set (it is also spread into
|
|
56
|
+
* `PRESET_EXCLUDED_KEYS`, and affixes must remain capture-INCLUDED for presets to ship them).
|
|
57
|
+
*/
|
|
58
|
+
export function presetApplyClearKeys(
|
|
59
|
+
presetData: Readonly<Record<string, unknown>>,
|
|
60
|
+
): readonly string[] {
|
|
61
|
+
const ownsPromptContent = PROMPT_CONTENT_KEYS.some((k) => presetData[k] !== undefined)
|
|
62
|
+
return ownsPromptContent
|
|
63
|
+
? [...PRESET_APPLY_CLEAR_KEYS, PROMPT_PREFIX_KEY, PROMPT_SUFFIX_KEY]
|
|
64
|
+
: PRESET_APPLY_CLEAR_KEYS
|
|
65
|
+
}
|
|
66
|
+
|
|
29
67
|
/**
|
|
30
68
|
* Config fields that are intentionally NOT part of a preset, because they are not portable
|
|
31
69
|
* configuration — they identify the node on the canvas, wire it into one specific graph, or point
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt pre/post text ("affixes") — the PUBLIC node-data contract.
|
|
3
|
+
*
|
|
4
|
+
* Every AI prompt node may carry two optional strings that are wrapped around
|
|
5
|
+
* its run-time prompt: `promptPrefix` goes before, `promptSuffix` after. Both
|
|
6
|
+
* support `{Node Label}` references and plain-text snippets exactly like the
|
|
7
|
+
* main prompt. They are settings (edited in the node's config panel, captured
|
|
8
|
+
* by presets, settable through workflow JSON / `inputOverrides`), never shown
|
|
9
|
+
* on the node face or to published-app end users.
|
|
10
|
+
*
|
|
11
|
+
* Only the KEYS + TYPE live here (what the SDK/API contract needs). The wrap
|
|
12
|
+
* behaviour (`applyPromptAffixes`) lives in `@nodaro/prompts` next to
|
|
13
|
+
* `resolvePrompt` — see the spec's IP-placement note.
|
|
14
|
+
*/
|
|
15
|
+
export const PROMPT_PREFIX_KEY = "promptPrefix" as const
|
|
16
|
+
export const PROMPT_SUFFIX_KEY = "promptSuffix" as const
|
|
17
|
+
|
|
18
|
+
export interface PromptAffixFields {
|
|
19
|
+
/** Text placed BEFORE the node's prompt at run time. Supports {Node Label} refs. */
|
|
20
|
+
readonly promptPrefix?: string
|
|
21
|
+
/** Text placed AFTER the node's prompt at run time. Supports {Node Label} refs. */
|
|
22
|
+
readonly promptSuffix?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The affix pair read off node data (blank / non-string values are omitted). */
|
|
26
|
+
export interface PromptAffixes {
|
|
27
|
+
readonly prefix?: string
|
|
28
|
+
readonly suffix?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function nonBlank(v: unknown): string | undefined {
|
|
32
|
+
return typeof v === "string" && v.trim().length > 0 ? v : undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Read `{ prefix, suffix }` off a node's `data`. Never throws; unknown shapes → `{}`. */
|
|
36
|
+
export function readPromptAffixes(data: Record<string, unknown>): PromptAffixes {
|
|
37
|
+
const prefix = nonBlank(data[PROMPT_PREFIX_KEY])
|
|
38
|
+
const suffix = nonBlank(data[PROMPT_SUFFIX_KEY])
|
|
39
|
+
return {
|
|
40
|
+
...(prefix !== undefined ? { prefix } : {}),
|
|
41
|
+
...(suffix !== undefined ? { suffix } : {}),
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/video-analysis.ts
CHANGED
|
@@ -84,8 +84,20 @@ export type VideoAnalysisVisualEffect = (typeof VIDEO_ANALYSIS_VISUAL_EFFECTS)[n
|
|
|
84
84
|
* `dissolve` (a cross-fade from one image to the other) is distinct from `fade`
|
|
85
85
|
* (through black or white). Collapsing both onto `fade` — as this enum did — makes
|
|
86
86
|
* a recreation render the wrong edit, and the two look nothing alike.
|
|
87
|
+
*
|
|
88
|
+
* ABSENT means NOTHING ASSERTED (since v2): the analyser emits a value only
|
|
89
|
+
* when it can see the device; an unknown or uncertain edit is left out so the
|
|
90
|
+
* video model chooses. `cut` is itself an assertion (an instant change between
|
|
91
|
+
* two clearly different shots), never a default.
|
|
87
92
|
*/
|
|
88
|
-
export const VIDEO_ANALYSIS_TRANSITIONS = [
|
|
93
|
+
export const VIDEO_ANALYSIS_TRANSITIONS = [
|
|
94
|
+
"cut", "fade", "dissolve", "wipe", "whip",
|
|
95
|
+
// v2 (2026-08-30, recast shot craft rev 1.5 Appendix F): the common edits a
|
|
96
|
+
// creator actually cuts with. `white-flash` / `digital-glitch` are spelled
|
|
97
|
+
// out because `flash` / `glitch` already name in-shot EFFECTS (the axis test
|
|
98
|
+
// below) — a device is the edit BETWEEN two shots, an effect is on one shot.
|
|
99
|
+
"zoom", "slide", "white-flash", "digital-glitch", "morph", "match", "jump",
|
|
100
|
+
] as const
|
|
89
101
|
export type VideoAnalysisTransition = (typeof VIDEO_ANALYSIS_TRANSITIONS)[number]
|
|
90
102
|
|
|
91
103
|
/**
|