@nodaro/shared 2.8.0 → 2.11.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 +329 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +631 -94
- package/dist/index.d.ts +631 -94
- package/dist/index.js +297 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/catalog-projection.test.ts +14 -0
- package/src/__tests__/default-video-provider.test.ts +1 -1
- package/src/__tests__/llm-models.test.ts +3 -2
- package/src/__tests__/organizations-types.test.ts +61 -0
- package/src/__tests__/pack-sidecar-localization.test.ts +21 -0
- package/src/__tests__/parameter-node-value.test.ts +18 -1
- package/src/__tests__/producer-types.test.ts +11 -0
- package/src/__tests__/prompt-length-limits.test.ts +7 -3
- package/src/__tests__/resolve-pipeline-model.test.ts +4 -4
- package/src/__tests__/seedance2-continuation-ref.test.ts +2 -3
- package/src/__tests__/video-analysis-catalog-sync.test.ts +1 -1
- package/src/__tests__/video-analysis-pricing.test.ts +2 -2
- package/src/__tests__/video-mode-for-inputs.test.ts +74 -0
- package/src/animals.ts +10 -0
- package/src/catalog-projection.ts +48 -0
- package/src/combine-transitions.ts +38 -0
- package/src/credit-estimators/video-utils.ts +1 -1
- package/src/entity-node-fields.ts +147 -0
- package/src/featured-entities.ts +1 -1
- package/src/furniture.ts +10 -0
- package/src/i18n/index.ts +33 -3
- package/src/i18n/transitions.ar.ts +6 -0
- package/src/i18n/transitions.de.ts +6 -0
- package/src/i18n/transitions.es.ts +6 -0
- package/src/i18n/transitions.fr.ts +6 -0
- package/src/i18n/transitions.he.ts +6 -0
- package/src/i18n/transitions.hi.ts +6 -0
- package/src/i18n/transitions.ja.ts +6 -0
- package/src/i18n/transitions.ko.ts +6 -0
- package/src/i18n/transitions.pt-BR.ts +6 -0
- package/src/i18n/transitions.ru.ts +6 -0
- package/src/i18n/transitions.zh-CN.ts +6 -0
- package/src/i18n/types.ts +12 -14
- package/src/index.ts +30 -1
- package/src/llm-models.ts +4 -0
- package/src/model-catalog.ts +19 -0
- package/src/model-constants.ts +52 -7
- package/src/organizations/index.ts +2 -0
- package/src/organizations/types.ts +152 -0
- package/src/organizations/views.ts +220 -0
- package/src/parameter-node-value.ts +23 -3
- package/src/producer-types.ts +10 -0
- package/src/smart-cut-windows.ts +8 -17
- package/src/suno-track-sources.ts +23 -0
- package/src/surround.ts +10 -90
- package/src/vehicles.ts +11 -1
- package/src/video-analysis-pricing.ts +25 -36
- package/src/weapons.ts +11 -1
- package/src/workflow-export.ts +41 -0
package/package.json
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, it, expectTypeOf } from "vitest"
|
|
2
|
+
import type { ProjectedCatalog } from "../catalog-projection.js"
|
|
3
|
+
|
|
4
|
+
describe("ProjectedCatalog wire shape (Apache boundary)", () => {
|
|
5
|
+
it("is tag-free and policy-free (the deferred CatalogPolicy never crosses the wire)", () => {
|
|
6
|
+
// @ts-expect-error — no `tags` field may exist on the projection shape
|
|
7
|
+
const _t: ProjectedCatalog["tags"] = undefined
|
|
8
|
+
// @ts-expect-error — no policy field may exist either
|
|
9
|
+
const _p: ProjectedCatalog["policy"] = undefined
|
|
10
|
+
void _t
|
|
11
|
+
void _p
|
|
12
|
+
expectTypeOf<ProjectedCatalog>().toHaveProperty("catalogId")
|
|
13
|
+
})
|
|
14
|
+
})
|
|
@@ -23,7 +23,7 @@ describe("DEFAULT_VIDEO_PROVIDER", () => {
|
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
it("nothing-specified request resolves to the cheapest seeded composite", () => {
|
|
26
|
-
const sel = applyDefaultVideoSelection({})
|
|
26
|
+
const sel = applyDefaultVideoSelection<number>({})
|
|
27
27
|
const id = buildVideoCreditModelIdentifier(
|
|
28
28
|
sel.provider, sel.duration, undefined, "image-to-video", undefined, undefined, false,
|
|
29
29
|
)
|
|
@@ -392,10 +392,11 @@ describe("LLM_FEATURE_DEFAULTS", () => {
|
|
|
392
392
|
"translate",
|
|
393
393
|
"image-critic",
|
|
394
394
|
"pick-best-llm",
|
|
395
|
+
"workflow-copilot",
|
|
395
396
|
]
|
|
396
397
|
|
|
397
|
-
it("has entries for all
|
|
398
|
-
expect(Object.keys(LLM_FEATURE_DEFAULTS)).toHaveLength(
|
|
398
|
+
it("has entries for all 17 features", () => {
|
|
399
|
+
expect(Object.keys(LLM_FEATURE_DEFAULTS)).toHaveLength(17)
|
|
399
400
|
for (const feature of ALL_FEATURES) {
|
|
400
401
|
expect(LLM_FEATURE_DEFAULTS).toHaveProperty(feature)
|
|
401
402
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
ACCESS_LEVELS,
|
|
4
|
+
COLLABORATOR_ROLES,
|
|
5
|
+
MEMBER_STATUSES,
|
|
6
|
+
ORG_ERROR_CODES,
|
|
7
|
+
ORG_KINDS,
|
|
8
|
+
ORG_ROLES,
|
|
9
|
+
OrgSettingsSchema,
|
|
10
|
+
PRESET_SETTING_KEYS,
|
|
11
|
+
PresetSettingsSchema,
|
|
12
|
+
WORKSPACE_ROLES,
|
|
13
|
+
WorkspaceSettingsSchema,
|
|
14
|
+
} from "../organizations/index.js"
|
|
15
|
+
|
|
16
|
+
describe("organizations wire contract — enums", () => {
|
|
17
|
+
it("every enum is non-empty and duplicate-free", () => {
|
|
18
|
+
for (const list of [ORG_KINDS, ORG_ROLES, WORKSPACE_ROLES, MEMBER_STATUSES, COLLABORATOR_ROLES, ACCESS_LEVELS, ORG_ERROR_CODES]) {
|
|
19
|
+
expect(list.length).toBeGreaterThan(0)
|
|
20
|
+
expect(new Set(list).size).toBe(list.length)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("error codes are snake_case identifiers", () => {
|
|
25
|
+
for (const code of ORG_ERROR_CODES) expect(code).toMatch(/^[a-z][a-z_]*[a-z]$/)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe("organizations wire contract — settings request schemas", () => {
|
|
30
|
+
it("the preset shape has exactly the nine resolvable keys", () => {
|
|
31
|
+
expect([...PRESET_SETTING_KEYS].sort()).toEqual(
|
|
32
|
+
[
|
|
33
|
+
"admin_access",
|
|
34
|
+
"collaborators_can_invite",
|
|
35
|
+
"default_workflow_visibility",
|
|
36
|
+
"member_access_to_shared",
|
|
37
|
+
"member_caps_enabled",
|
|
38
|
+
"members_can_create_projects",
|
|
39
|
+
"personal_space_enabled",
|
|
40
|
+
"policy_survives_suspension",
|
|
41
|
+
"workspace_admins_can_invite",
|
|
42
|
+
].sort(),
|
|
43
|
+
)
|
|
44
|
+
expect(PresetSettingsSchema.safeParse({}).success).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("workspace settings are a partial of the preset; unknown keys are stripped; bad values rejected", () => {
|
|
48
|
+
expect(WorkspaceSettingsSchema.parse({ admin_access: "edit", rogue: 1 })).toEqual({ admin_access: "edit" })
|
|
49
|
+
expect(WorkspaceSettingsSchema.safeParse({}).success).toBe(true)
|
|
50
|
+
expect(WorkspaceSettingsSchema.safeParse({ admin_access: "owner" }).success).toBe(false)
|
|
51
|
+
expect(WorkspaceSettingsSchema.safeParse({ member_caps_enabled: "yes" }).success).toBe(false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("org settings accept lower-case domains only and string vocabulary overrides", () => {
|
|
55
|
+
expect(OrgSettingsSchema.safeParse({ allowed_email_domains: ["school.edu", "sub.school.ac.il"] }).success).toBe(true)
|
|
56
|
+
expect(OrgSettingsSchema.safeParse({ allowed_email_domains: ["School.edu"] }).success).toBe(false)
|
|
57
|
+
expect(OrgSettingsSchema.safeParse({ allowed_email_domains: ["not a domain"] }).success).toBe(false)
|
|
58
|
+
expect(OrgSettingsSchema.safeParse({ vocabulary_overrides: { workspace: "Cohort" } }).success).toBe(true)
|
|
59
|
+
expect(OrgSettingsSchema.safeParse({ vocabulary_overrides: { workspace: 3 } }).success).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest"
|
|
2
|
+
import { registerCatalogSidecars, resetCatalogSidecars, resolveLabel, entryMatchesQuery } from "../index.js"
|
|
3
|
+
|
|
4
|
+
afterEach(() => resetCatalogSidecars())
|
|
5
|
+
|
|
6
|
+
describe("pack sidecars resolve through the shared localizer (G10)", () => {
|
|
7
|
+
it("resolveLabel returns a pack-registered localized label", () => {
|
|
8
|
+
// English fallback before registration
|
|
9
|
+
expect(resolveLabel("person", "attire-x", "Modest Suit", "he")).toBe("Modest Suit")
|
|
10
|
+
registerCatalogSidecars("person", { he: { "attire-x": { label: "חליפה צנועה" } } })
|
|
11
|
+
expect(resolveLabel("person", "attire-x", "Modest Suit", "he")).toBe("חליפה צנועה")
|
|
12
|
+
})
|
|
13
|
+
it("english locale ignores sidecars", () => {
|
|
14
|
+
registerCatalogSidecars("person", { he: { "attire-x": { label: "חליפה צנועה" } } })
|
|
15
|
+
expect(resolveLabel("person", "attire-x", "Modest Suit", "en")).toBe("Modest Suit")
|
|
16
|
+
})
|
|
17
|
+
it("search matches the localized pack label", () => {
|
|
18
|
+
registerCatalogSidecars("person", { he: { "attire-x": { label: "חליפה צנועה" } } })
|
|
19
|
+
expect(entryMatchesQuery("person", "attire-x", "Modest Suit", "d", "he", "חליפה")).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest"
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest"
|
|
2
2
|
import {
|
|
3
3
|
PARAMETER_NODE_TYPES,
|
|
4
4
|
getParameterValue,
|
|
5
5
|
} from "../parameter-node-value.js"
|
|
6
|
+
import { setRegisteredPersonPackFields } from "../index.js"
|
|
6
7
|
|
|
7
8
|
describe("PARAMETER_NODE_TYPES", () => {
|
|
8
9
|
it("includes the existing parameter node types", () => {
|
|
@@ -135,3 +136,19 @@ describe("parameter-node-value — furniture", () => {
|
|
|
135
136
|
expect(getParameterValue({}, "furniture")).toBeUndefined()
|
|
136
137
|
})
|
|
137
138
|
})
|
|
139
|
+
|
|
140
|
+
describe("getParameterValue person pack-dimension fallback (G4)", () => {
|
|
141
|
+
afterEach(() => setRegisteredPersonPackFields([]))
|
|
142
|
+
|
|
143
|
+
it("returns undefined for an unregistered pack field", () => {
|
|
144
|
+
expect(getParameterValue({ sectorAttire: "attire-modest-suit" }, "person")).toBeUndefined()
|
|
145
|
+
})
|
|
146
|
+
it("resolves a registered pack field when no base dimension is set", () => {
|
|
147
|
+
setRegisteredPersonPackFields(["sectorAttire"])
|
|
148
|
+
expect(getParameterValue({ sectorAttire: "attire-modest-suit" }, "person")).toBe("attire-modest-suit")
|
|
149
|
+
})
|
|
150
|
+
it("base dimensions still win over pack fields", () => {
|
|
151
|
+
setRegisteredPersonPackFields(["sectorAttire"])
|
|
152
|
+
expect(getParameterValue({ type: "man", sectorAttire: "attire-modest-suit" }, "person")).toBe("man")
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -23,6 +23,17 @@ describe("producer-types", () => {
|
|
|
23
23
|
DYNAMIC_PRODUCER_TYPES,
|
|
24
24
|
} as const
|
|
25
25
|
|
|
26
|
+
// still-to-video (still image + audio → MP4 via local FFmpeg) has a video
|
|
27
|
+
// OUTPUT handle — absent from this set, every downstream video input would
|
|
28
|
+
// hard-reject its edges (the "cannot connect the outputs" bug class).
|
|
29
|
+
it("registers still-to-video as a video producer", () => {
|
|
30
|
+
expect(VIDEO_PRODUCER_TYPES.has("still-to-video")).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("registers slideshow as a video producer (still-to-video's N-image companion)", () => {
|
|
34
|
+
expect(VIDEO_PRODUCER_TYPES.has("slideshow")).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
26
37
|
it("registers voice-changer-pro as an audio producer (its default output)", () => {
|
|
27
38
|
expect(AUDIO_PRODUCER_TYPES.has("voice-changer-pro")).toBe(true)
|
|
28
39
|
})
|
|
@@ -102,9 +102,13 @@ describe("per-model prompt length limits", () => {
|
|
|
102
102
|
})
|
|
103
103
|
|
|
104
104
|
describe("Suno per-version caps", () => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
// Non-custom was 500 until 2026-08-19 — six times under the provider's
|
|
106
|
+
// documented 3000, and the route truncates rather than rejects, so a
|
|
107
|
+
// 950-char recast score brief reached Suno cut to exactly 500 mid-word.
|
|
108
|
+
it("prompt: 3000 non-custom (all models); 3000 V4 custom; 5000 V4.5+/V5 custom", () => {
|
|
109
|
+
expect(getMaxSunoPromptChars("V4", false)).toBe(3000)
|
|
110
|
+
expect(getMaxSunoPromptChars("V5", false)).toBe(3000)
|
|
111
|
+
expect(getMaxSunoPromptChars("V5_5", false)).toBe(3000)
|
|
108
112
|
expect(getMaxSunoPromptChars("V4", true)).toBe(3000)
|
|
109
113
|
expect(getMaxSunoPromptChars("V4_5", true)).toBe(5000)
|
|
110
114
|
expect(getMaxSunoPromptChars("V5", true)).toBe(5000)
|
|
@@ -28,8 +28,8 @@ describe("resolvePipelineModel", () => {
|
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
it("returns the global script_llm for the script_llm stage", () => {
|
|
31
|
-
const config: Partial<PipelineConfig> = { script_llm: "claude-opus-4-
|
|
32
|
-
expect(resolvePipelineModel(config, "script_llm")).toBe("claude-opus-4-
|
|
31
|
+
const config: Partial<PipelineConfig> = { script_llm: "claude-opus-4-7" }
|
|
32
|
+
expect(resolvePipelineModel(config, "script_llm")).toBe("claude-opus-4-7")
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
it("per-stage override beats the matching global field", () => {
|
|
@@ -52,12 +52,12 @@ describe("resolvePipelineModel", () => {
|
|
|
52
52
|
stage_models: {
|
|
53
53
|
scene_keyframes_image: "gpt-image",
|
|
54
54
|
shots_video: "veo3",
|
|
55
|
-
script_llm: "claude-opus-4-
|
|
55
|
+
script_llm: "claude-opus-4-7",
|
|
56
56
|
},
|
|
57
57
|
}
|
|
58
58
|
expect(resolvePipelineModel(config, "scene_keyframes_image")).toBe("gpt-image")
|
|
59
59
|
expect(resolvePipelineModel(config, "shots_video")).toBe("veo3")
|
|
60
|
-
expect(resolvePipelineModel(config, "script_llm")).toBe("claude-opus-4-
|
|
60
|
+
expect(resolvePipelineModel(config, "script_llm")).toBe("claude-opus-4-7")
|
|
61
61
|
// Entity image stages keep the global pick.
|
|
62
62
|
expect(resolvePipelineModel(config, "characters_image")).toBe("flux")
|
|
63
63
|
})
|
|
@@ -11,9 +11,8 @@ import {
|
|
|
11
11
|
* job dbf95612 — the original 1.0s tails made every generate-video-pro
|
|
12
12
|
* continuation segment fail deterministically). Everything this platform
|
|
13
13
|
* sends as a Seedance-2 video reference cuts SEEDANCE_2_CONTINUATION_REF_SEC
|
|
14
|
-
* seconds; do NOT "optimize" it back below the floor. The
|
|
15
|
-
* twin constants
|
|
16
|
-
* MIN_REF) are guarded by that repo's r2v-ref-floor.test.ts — keep in sync. */
|
|
14
|
+
* seconds; do NOT "optimize" it back below the floor. The plugin repo
|
|
15
|
+
* carries twin constants guarded by its own tests — keep the two in sync. */
|
|
17
16
|
describe("seedance-2 continuation-reference floor (do not regress)", () => {
|
|
18
17
|
it("the platform's continuation-ref length clears KIE's r2v minimum", () => {
|
|
19
18
|
expect(SEEDANCE_2_CONTINUATION_REF_SEC).toBeGreaterThanOrEqual(SEEDANCE_2_R2V_MIN_REF_VIDEO_SEC)
|
|
@@ -157,7 +157,7 @@ describe("bare video-analysis node-type credit id", () => {
|
|
|
157
157
|
// 24 and the schedule regenerated ~47% lower at the ceiling bucket, and 300
|
|
158
158
|
// wrote 2064: the V1 hybrid-smart reprice (task A3) moved `smart` to a
|
|
159
159
|
// multi-roll plan (native skeleton + donor rolls, always refined) and
|
|
160
|
-
// trued up every tier's judge/refine terms
|
|
160
|
+
// trued up every tier's judge/refine terms).
|
|
161
161
|
expect(ceiling).toBe(2064)
|
|
162
162
|
})
|
|
163
163
|
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
|
|
16
16
|
// The measured-rate constants and the $-derived `videoAnalysisBucketCredits`
|
|
17
17
|
// formula are PRIVATE, in @nodaroai/cloud-plugins
|
|
18
|
-
// (
|
|
18
|
+
// (in the plugin repo) — its tests (the worked-example
|
|
19
19
|
// bucket-credit values and the cross-check against VIDEO_ANALYSIS_BUCKET_CREDITS
|
|
20
20
|
// below) live in that private package's __tests__/cost.test.ts. This file
|
|
21
21
|
// covers only the NON-monetary duration-bucketing, window-batching, and
|
|
@@ -121,7 +121,7 @@ describe("video-analysis-pricing", () => {
|
|
|
121
121
|
})
|
|
122
122
|
|
|
123
123
|
// Full drift-detection against the live $-formula lives in the PRIVATE
|
|
124
|
-
// plugin repo
|
|
124
|
+
// plugin repo — the
|
|
125
125
|
// formula moved there in 2026-07 and the app-side test was deleted with it,
|
|
126
126
|
// so nothing in THIS repo can recompute these numbers. This is a lightweight
|
|
127
127
|
// shape check that the precomputed table covers every legal id.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
VIDEO_MODE_ALIASES,
|
|
4
|
+
VIDEO_REF_LIMITS_BY_PROVIDER,
|
|
5
|
+
resolveVideoModeForInputs,
|
|
6
|
+
resolveVideoProviderForMode,
|
|
7
|
+
} from "../model-constants.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* #861 — reference images wired to Grok Imagine 1 without a start frame were
|
|
11
|
+
* silently dropped: the run resolved to text-to-video, the t2v twin (`grok`)
|
|
12
|
+
* has no image parameter, and nothing said so. The mode must follow what the
|
|
13
|
+
* model can CARRY, derived from the catalog's reference caps — not a name.
|
|
14
|
+
*/
|
|
15
|
+
const I2V = "image-to-video"
|
|
16
|
+
const T2V = "text-to-video"
|
|
17
|
+
|
|
18
|
+
describe("resolveVideoModeForInputs", () => {
|
|
19
|
+
it("a start frame is image-to-video for every provider", () => {
|
|
20
|
+
for (const id of ["grok-i2v", "grok", "seedance-2", "kling", "veo3", "wan-i2v", undefined]) {
|
|
21
|
+
expect(resolveVideoModeForInputs(id, { hasStartFrame: true, hasImageRefs: false })).toBe(I2V)
|
|
22
|
+
expect(resolveVideoModeForInputs(id, { hasStartFrame: true, hasImageRefs: true })).toBe(I2V)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("nothing wired is text-to-video", () => {
|
|
27
|
+
expect(resolveVideoModeForInputs("grok-i2v", { hasStartFrame: false, hasImageRefs: false })).toBe(T2V)
|
|
28
|
+
expect(resolveVideoModeForInputs(undefined, { hasStartFrame: false, hasImageRefs: true })).toBe(T2V)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("refs alone route Grok Imagine 1 to its i2v twin — the only one that can carry them (#861)", () => {
|
|
32
|
+
// Whichever member id the node stored (base/i2v/t2v all appear in old workflows).
|
|
33
|
+
for (const id of ["grok-i2v", "grok"]) {
|
|
34
|
+
expect(resolveVideoModeForInputs(id, { hasStartFrame: false, hasImageRefs: true })).toBe(I2V)
|
|
35
|
+
}
|
|
36
|
+
// The premise, pinned: i2v carries refs, t2v does not.
|
|
37
|
+
expect(VIDEO_REF_LIMITS_BY_PROVIDER["grok-i2v"]?.images).toBeGreaterThan(0)
|
|
38
|
+
expect(VIDEO_REF_LIMITS_BY_PROVIDER["grok"]).toBeUndefined()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("single-id models are untouched: the same id serves both modes, so there is nothing to re-route", () => {
|
|
42
|
+
// seedance-2 / gemini-omni-video / veo forward refs on their t2v path
|
|
43
|
+
// (generate-video.md mode table). kling-3-omni is i2v-only and keeps its
|
|
44
|
+
// documented orchestrator behavior (image_required on the t2v path) —
|
|
45
|
+
// whether refs alone should satisfy it is a separate question from #861.
|
|
46
|
+
for (const id of ["seedance-2", "gemini-omni-video", "veo3.1", "kling-3-omni"]) {
|
|
47
|
+
expect(resolveVideoModeForInputs(id, { hasStartFrame: false, hasImageRefs: true })).toBe(T2V)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("a split-id model with no ref support on either twin is unaffected (wan, happyhorse)", () => {
|
|
52
|
+
for (const id of ["wan-i2v", "wan-2.7-i2v", "happyhorse-i2v"]) {
|
|
53
|
+
expect(resolveVideoModeForInputs(id, { hasStartFrame: false, hasImageRefs: true })).toBe(T2V)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("every alias group is classified — a new split-id model lands in one of the two pinned outcomes", () => {
|
|
58
|
+
// Explicit per-group expectations (not a re-derivation of the formula):
|
|
59
|
+
// grok is the one group whose twins differ on ref support today.
|
|
60
|
+
const expectedByBase: Record<string, "image-to-video" | "text-to-video"> = {
|
|
61
|
+
"grok-i2v": I2V,
|
|
62
|
+
"wan-i2v": T2V,
|
|
63
|
+
"wan-2.7-i2v": T2V,
|
|
64
|
+
"happyhorse-i2v": T2V,
|
|
65
|
+
}
|
|
66
|
+
expect(VIDEO_MODE_ALIASES.map((g) => g.base).sort()).toEqual(Object.keys(expectedByBase).sort())
|
|
67
|
+
for (const g of VIDEO_MODE_ALIASES) {
|
|
68
|
+
const expected = expectedByBase[g.base]!
|
|
69
|
+
expect(resolveVideoModeForInputs(g.base, { hasStartFrame: false, hasImageRefs: true }), g.base).toBe(expected)
|
|
70
|
+
// The mode it picks resolves to the twin that can actually run it.
|
|
71
|
+
if (expected === I2V) expect(resolveVideoProviderForMode(g.base, expected)).toBe(g.i2v)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
})
|
package/src/animals.ts
CHANGED
|
@@ -30,6 +30,16 @@ export interface Animal {
|
|
|
30
30
|
readonly label: string
|
|
31
31
|
readonly subcategory: AnimalSubcategory
|
|
32
32
|
readonly description: string
|
|
33
|
+
/**
|
|
34
|
+
* Optional authored compact term — the short phrase a professional would
|
|
35
|
+
* write in a prompt, as opposed to the user-facing `label` (see the `term`
|
|
36
|
+
* convention in `@nodaro/prompts`'s `term.ts`). Authored ONLY where the
|
|
37
|
+
* lowercased label is not that phrase — a UI compound naming two things at
|
|
38
|
+
* once ("Airship / Dirigible" -> "airship", "Plasma Sword / Lightsaber" ->
|
|
39
|
+
* "plasma sword"). Everywhere else the lowercased label IS the term for a
|
|
40
|
+
* concrete object ("golden retriever", "katana"), so nothing is authored.
|
|
41
|
+
*/
|
|
42
|
+
readonly term?: string
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
export const ANIMALS: ReadonlyArray<Animal> = [
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag-free, policy-free wire shape for the `GET /v1/catalogs` projection — the
|
|
3
|
+
* server-driven, pack-composed catalog view thin clients render their own
|
|
4
|
+
* pickers from. This is the ONLY catalog-related type in `@nodaro/shared`
|
|
5
|
+
* (Apache): catalog DATA stays in `@nodaro/prompts` (FSL), and the deferred
|
|
6
|
+
* `CatalogPolicy` (tags / deny-by-tag / per-read-kind filter) is deliberately
|
|
7
|
+
* NOT represented here — nothing tag- or policy-shaped may cross this boundary.
|
|
8
|
+
*/
|
|
9
|
+
export interface ProjectedCatalogOption {
|
|
10
|
+
id: string
|
|
11
|
+
label: string
|
|
12
|
+
description?: string
|
|
13
|
+
category?: string
|
|
14
|
+
/** The prompt fragment this id injects downstream. Present only when detail="full". */
|
|
15
|
+
promptHint?: string
|
|
16
|
+
/**
|
|
17
|
+
* Short professional term injected by compact hint mode; `label` is for
|
|
18
|
+
* display. Present at BOTH detail levels — a thin client renders `label`
|
|
19
|
+
* and injects `term`. Empty for a no-op ("auto"/"none") entry that injects
|
|
20
|
+
* nothing.
|
|
21
|
+
*/
|
|
22
|
+
term?: string
|
|
23
|
+
icon?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ProjectedCatalogDimension {
|
|
27
|
+
field: string
|
|
28
|
+
label: string
|
|
29
|
+
options: ProjectedCatalogOption[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ProjectedCatalog {
|
|
33
|
+
nodeType: string
|
|
34
|
+
label: string
|
|
35
|
+
catalogId: string
|
|
36
|
+
kind: "single" | "multi"
|
|
37
|
+
/** single only — the node-data field the chosen id writes to. */
|
|
38
|
+
valueField?: string
|
|
39
|
+
defaultValue?: string
|
|
40
|
+
categoryOrder?: readonly string[]
|
|
41
|
+
categoryLabels?: Readonly<Record<string, string>>
|
|
42
|
+
detail: "compact" | "full"
|
|
43
|
+
/** single-dim catalogs. */
|
|
44
|
+
options?: ProjectedCatalogOption[]
|
|
45
|
+
/** multi-dim catalogs. */
|
|
46
|
+
fields?: readonly string[]
|
|
47
|
+
dimensions?: ProjectedCatalogDimension[]
|
|
48
|
+
}
|
|
@@ -480,3 +480,41 @@ export function resolveXfadeName(id: string): string | null {
|
|
|
480
480
|
}
|
|
481
481
|
return entry.xfade
|
|
482
482
|
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Map a `transition` PARAMETER-NODE pick (the @nodaro/prompts creative
|
|
486
|
+
* catalog — prose-hint transitions for AI video prompts) onto the ffmpeg
|
|
487
|
+
* xfade vocabulary above. The two vocabularies are deliberately different
|
|
488
|
+
* (the picker describes shots, this table describes blends); only the
|
|
489
|
+
* visually-faithful subset maps — everything else falls back to `cut`.
|
|
490
|
+
*
|
|
491
|
+
* Consumed by the slideshow node (its transition TYPE comes from a wired
|
|
492
|
+
* transition parameter node; unwired defaults to cut). Kept here — not in
|
|
493
|
+
* @nodaro/prompts — because the dependency points prompts → shared, and the
|
|
494
|
+
* ids are plain strings on both sides.
|
|
495
|
+
*/
|
|
496
|
+
export const PICKER_TO_COMBINE_TRANSITION: Readonly<Record<string, string>> = {
|
|
497
|
+
"none": "cut",
|
|
498
|
+
"auto": "dissolve", // an explicitly wired "auto" wants A blend — the classic slideshow default
|
|
499
|
+
"cross-dissolve": "dissolve",
|
|
500
|
+
"fade-to-black": "dip-to-black",
|
|
501
|
+
"fade-to-white": "dip-to-white",
|
|
502
|
+
"iris": "circle-open",
|
|
503
|
+
"wipe": "wipe-left",
|
|
504
|
+
"pixelate-reform": "pixelize",
|
|
505
|
+
"dissolve-to-mist": "hblur",
|
|
506
|
+
"match-cut": "cut",
|
|
507
|
+
"smash-cut": "cut",
|
|
508
|
+
"freeze-frame-jump": "cut",
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Resolve ANY transition string a slideshow may receive — a combine id
|
|
513
|
+
* (already valid), a picker id (mapped), or anything else (→ cut).
|
|
514
|
+
*/
|
|
515
|
+
export function resolveSlideshowTransition(value: string | undefined | null): string {
|
|
516
|
+
if (!value) return "cut"
|
|
517
|
+
if (COMBINE_TRANSITION_IDS.includes(value)) return value
|
|
518
|
+
return PICKER_TO_COMBINE_TRANSITION[value] ?? "cut"
|
|
519
|
+
}
|
|
520
|
+
|
|
@@ -155,7 +155,7 @@ export function estimateLoopTrimAddonCredits(
|
|
|
155
155
|
Math.ceil(frames / VIDEO_UTIL_PRICING.FRAMES_PER_CREDIT)
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
/** BASE credits
|
|
158
|
+
/** BASE credits for assemble-narrated-video: 3 flat + 1 per 6
|
|
159
159
|
* blocks. 6→4, 24→7, 60→13. Single source of truth shared by the backend
|
|
160
160
|
* route/creditGuard (`backend/src/providers/video/narrated-block-fit.ts`
|
|
161
161
|
* re-exports this) and the frontend pre-run estimate
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which DB columns of a saved entity land on its canvas node, per kind.
|
|
3
|
+
*
|
|
4
|
+
* Four surfaces copy an entity row onto a node: the browser's load-time
|
|
5
|
+
* hydrator, the browser's library picker, the backend's run-time hydration, and
|
|
6
|
+
* (indirectly) anything that reads a node expecting those fields to be there.
|
|
7
|
+
* They used to be four hand-written lists, and they drifted exactly as you would
|
|
8
|
+
* expect — the load-time hydrator covers `character` and nothing else, so an
|
|
9
|
+
* object node bound by an agent stays media-less until someone rebinds it by
|
|
10
|
+
* hand.
|
|
11
|
+
*
|
|
12
|
+
* This is the field NAMES only — the structural vocabulary. Merge behaviour
|
|
13
|
+
* (defaults, `prev` fallbacks, type narrowing) stays with each caller, because
|
|
14
|
+
* a browser node and a server row disagree about nulls and it is not worth
|
|
15
|
+
* pretending otherwise.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Every entity kind, in the order surfaces present them.
|
|
20
|
+
*
|
|
21
|
+
* The list is the invariant: `Record<EntityNodeKind, …>` makes the compiler
|
|
22
|
+
* find every table below, every per-kind UI row in the `@` picker, and the
|
|
23
|
+
* picker’s kind→query map. What the compiler cannot see — that the MCP read
|
|
24
|
+
* tools for a kind exist and are reachable — is pinned by a test instead.
|
|
25
|
+
*
|
|
26
|
+
* Tool names are derivable from it too: `list_<kind>s` / `get_<kind>`.
|
|
27
|
+
*/
|
|
28
|
+
export const ENTITY_NODE_KINDS = ["character", "object", "creature", "location"] as const
|
|
29
|
+
|
|
30
|
+
export type EntityNodeKind = (typeof ENTITY_NODE_KINDS)[number]
|
|
31
|
+
|
|
32
|
+
/** The `data.*DbId` field that binds each entity node to its row. */
|
|
33
|
+
export const ENTITY_DB_ID_FIELD: Record<EntityNodeKind, string> = {
|
|
34
|
+
character: "characterDbId",
|
|
35
|
+
object: "objectDbId",
|
|
36
|
+
creature: "creatureDbId",
|
|
37
|
+
location: "locationDbId",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The `data.*Name` field each kind stores its display name under. */
|
|
41
|
+
export const ENTITY_NAME_FIELD: Record<EntityNodeKind, string> = {
|
|
42
|
+
character: "characterName",
|
|
43
|
+
object: "objectName",
|
|
44
|
+
creature: "creatureName",
|
|
45
|
+
location: "locationName",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Postgres table per kind. */
|
|
49
|
+
export const ENTITY_TABLE: Record<EntityNodeKind, string> = {
|
|
50
|
+
character: "characters",
|
|
51
|
+
object: "objects",
|
|
52
|
+
creature: "creatures",
|
|
53
|
+
location: "locations",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `{name,url}[]` buckets per kind, as `[db_column, nodeField]`.
|
|
58
|
+
*
|
|
59
|
+
* These are what a generation actually consumes — the variant a prompt
|
|
60
|
+
* `@mentions`, the extra references a user attaches. A node missing them is not
|
|
61
|
+
* visibly broken; it just quietly generates the wrong picture.
|
|
62
|
+
*/
|
|
63
|
+
export const ENTITY_BUCKET_FIELDS: Record<EntityNodeKind, ReadonlyArray<readonly [string, string]>> = {
|
|
64
|
+
character: [
|
|
65
|
+
["expressions", "expressions"],
|
|
66
|
+
["poses", "poses"],
|
|
67
|
+
["motions", "motions"],
|
|
68
|
+
["angles", "angles"],
|
|
69
|
+
["body_angles", "bodyAngles"],
|
|
70
|
+
["lighting_variations", "lightingVariations"],
|
|
71
|
+
["outfit_variations", "outfitVariations"],
|
|
72
|
+
["detail_closeups", "detailCloseups"],
|
|
73
|
+
["sheets", "sheets"],
|
|
74
|
+
],
|
|
75
|
+
object: [
|
|
76
|
+
["angles", "angles"],
|
|
77
|
+
["materials", "materials"],
|
|
78
|
+
["variations", "variations"],
|
|
79
|
+
["motion_clips", "motionClips"],
|
|
80
|
+
["detail_closeups", "detailCloseups"],
|
|
81
|
+
["sheets", "sheets"],
|
|
82
|
+
],
|
|
83
|
+
creature: [
|
|
84
|
+
["angles", "angles"],
|
|
85
|
+
["poses", "poses"],
|
|
86
|
+
["variations", "variations"],
|
|
87
|
+
["motion_clips", "motionClips"],
|
|
88
|
+
["detail_closeups", "detailCloseups"],
|
|
89
|
+
["sheets", "sheets"],
|
|
90
|
+
],
|
|
91
|
+
location: [
|
|
92
|
+
["time_of_day", "timeOfDay"],
|
|
93
|
+
["weather", "weather"],
|
|
94
|
+
["angles", "angles"],
|
|
95
|
+
["lighting", "lighting"],
|
|
96
|
+
["seasons", "seasons"],
|
|
97
|
+
["atmosphere_motions", "atmosphereMotions"],
|
|
98
|
+
["detail_closeups", "detailCloseups"],
|
|
99
|
+
["sheets", "sheets"],
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Scalars every kind shares, as `[db_column, nodeField]`.
|
|
105
|
+
*
|
|
106
|
+
* `source_image_url` is the load-bearing one: the run engine reads
|
|
107
|
+
* `defaultAssetUrl || sourceImageUrl` and SKIPS the reference entirely when
|
|
108
|
+
* both are empty — no error, no warning, just a generation of the wrong
|
|
109
|
+
* person. A node with an id but no image is the shape an agent produces.
|
|
110
|
+
*/
|
|
111
|
+
export const ENTITY_SCALAR_FIELDS: ReadonlyArray<readonly [string, string]> = [
|
|
112
|
+
["name", "__name"], // routed to the kind's own name field by the caller
|
|
113
|
+
["description", "description"],
|
|
114
|
+
["canonical_description", "canonicalDescription"],
|
|
115
|
+
["source_image_url", "sourceImageUrl"],
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Scalars only SOME kinds have.
|
|
120
|
+
*
|
|
121
|
+
* `style_lock` looks shared and is not: characters never grew the column,
|
|
122
|
+
* because a character's likeness is the lock. Selecting it from `characters`
|
|
123
|
+
* anyway is a PostgREST error, which the run-time hydrator swallows by design
|
|
124
|
+
* — so the whole kind would quietly stop hydrating and every test that mocks
|
|
125
|
+
* the database would still pass. That is why the column lists are checked
|
|
126
|
+
* against the migrations by `entity-hydration-columns.test.ts`.
|
|
127
|
+
*/
|
|
128
|
+
export const ENTITY_KIND_SCALAR_FIELDS: Record<EntityNodeKind, ReadonlyArray<readonly [string, string]>> = {
|
|
129
|
+
character: [],
|
|
130
|
+
object: [["style_lock", "styleLock"]],
|
|
131
|
+
creature: [["style_lock", "styleLock"]],
|
|
132
|
+
location: [["style_lock", "styleLock"]],
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Every DB column a full hydration of `kind` reads. */
|
|
136
|
+
export function entityHydrationColumns(kind: EntityNodeKind): string[] {
|
|
137
|
+
return [
|
|
138
|
+
"id",
|
|
139
|
+
...entityScalarFields(kind).map(([column]) => column),
|
|
140
|
+
...ENTITY_BUCKET_FIELDS[kind].map(([column]) => column),
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Every scalar `kind` actually has, shared plus its own. */
|
|
145
|
+
export function entityScalarFields(kind: EntityNodeKind): ReadonlyArray<readonly [string, string]> {
|
|
146
|
+
return [...ENTITY_SCALAR_FIELDS, ...ENTITY_KIND_SCALAR_FIELDS[kind]]
|
|
147
|
+
}
|
package/src/featured-entities.ts
CHANGED
package/src/furniture.ts
CHANGED
|
@@ -26,6 +26,16 @@ export interface Furniture {
|
|
|
26
26
|
readonly label: string
|
|
27
27
|
readonly subcategory: FurnitureSubcategory
|
|
28
28
|
readonly description: string
|
|
29
|
+
/**
|
|
30
|
+
* Optional authored compact term — the short phrase a professional would
|
|
31
|
+
* write in a prompt, as opposed to the user-facing `label` (see the `term`
|
|
32
|
+
* convention in `@nodaro/prompts`'s `term.ts`). Authored ONLY where the
|
|
33
|
+
* lowercased label is not that phrase — a UI compound naming two things at
|
|
34
|
+
* once ("Airship / Dirigible" -> "airship", "Plasma Sword / Lightsaber" ->
|
|
35
|
+
* "plasma sword"). Everywhere else the lowercased label IS the term for a
|
|
36
|
+
* concrete object ("golden retriever", "katana"), so nothing is authored.
|
|
37
|
+
*/
|
|
38
|
+
readonly term?: string
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
export const FURNITURE: ReadonlyArray<Furniture> = [
|