@nodaro/prompts 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +105 -0
- package/README.md +27 -0
- package/dist/index.cjs +21240 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3599 -0
- package/dist/index.d.ts +3599 -0
- package/dist/index.js +20854 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
- package/src/__tests__/action-fx.test.ts +155 -0
- package/src/__tests__/apply-picker-json.test.ts +50 -0
- package/src/__tests__/assemble-image-input.test.ts +248 -0
- package/src/__tests__/assemble-suno-input.test.ts +283 -0
- package/src/__tests__/brand-tokens.test.ts +81 -0
- package/src/__tests__/build-image-prompt-element-injection.test.ts +118 -0
- package/src/__tests__/build-image-prompt-hybrid-format.test.ts +131 -0
- package/src/__tests__/build-image-prompt-mentions.test.ts +809 -0
- package/src/__tests__/build-image-prompt-reference-cap.test.ts +57 -0
- package/src/__tests__/build-image-prompt-reference-numbering.test.ts +237 -0
- package/src/__tests__/build-image-prompt-reference-order.test.ts +313 -0
- package/src/__tests__/camera-motions-from-connections.test.ts +71 -0
- package/src/__tests__/catalog-gapfill.test.ts +53 -0
- package/src/__tests__/character-convergence-image.test.ts +217 -0
- package/src/__tests__/character-default-role-image.test.ts +167 -0
- package/src/__tests__/character-default-role-video.test.ts +168 -0
- package/src/__tests__/character-default-role.test.ts +70 -0
- package/src/__tests__/character-fx.test.ts +200 -0
- package/src/__tests__/entity-prompts-location.test.ts +97 -0
- package/src/__tests__/entity-prompts.test.ts +240 -0
- package/src/__tests__/expand-extra-refs-role.test.ts +50 -0
- package/src/__tests__/factory-presets.test.ts +1045 -0
- package/src/__tests__/factory-snippets.test.ts +68 -0
- package/src/__tests__/framing-multi.test.ts +71 -0
- package/src/__tests__/framing-vantage.test.ts +39 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +269 -0
- package/src/__tests__/identity-lock.test.ts +46 -0
- package/src/__tests__/instrumentation.test.ts +68 -0
- package/src/__tests__/lighting-multi.test.ts +68 -0
- package/src/__tests__/location-convergence-image.test.ts +124 -0
- package/src/__tests__/mention-lock-flag.test.ts +499 -0
- package/src/__tests__/multi-picker-spec.test.ts +71 -0
- package/src/__tests__/music-genre.test.ts +103 -0
- package/src/__tests__/music-mood.test.ts +87 -0
- package/src/__tests__/object-creature-convergence-image.test.ts +105 -0
- package/src/__tests__/parameter-prompt-hint.test.ts +270 -0
- package/src/__tests__/parameter-registry-sync.test.ts +243 -0
- package/src/__tests__/person-age.test.ts +80 -0
- package/src/__tests__/person-analyzer-invariants.test.ts +47 -0
- package/src/__tests__/person-body-axes.test.ts +67 -0
- package/src/__tests__/person-facial-geometry.test.ts +156 -0
- package/src/__tests__/person-regional-aesthetic.test.ts +161 -0
- package/src/__tests__/person-sections.test.ts +18 -0
- package/src/__tests__/picker-analyzer-registry.test.ts +108 -0
- package/src/__tests__/picker-catalogs-project.test.ts +85 -0
- package/src/__tests__/picker-catalogs.test.ts +53 -0
- package/src/__tests__/picker-limits.test.ts +20 -0
- package/src/__tests__/prompt-builder-segments.test.ts +183 -0
- package/src/__tests__/prompt-builder-structured-fields.test.ts +40 -0
- package/src/__tests__/prompt-builder.test.ts +1773 -0
- package/src/__tests__/prompt-wizard-categories.test.ts +31 -0
- package/src/__tests__/provider-prompt-doctrine.test.ts +49 -0
- package/src/__tests__/resolve-prompt-append.test.ts +58 -0
- package/src/__tests__/resolve-prompt.test.ts +44 -0
- package/src/__tests__/role-picker-shared.test.ts +208 -0
- package/src/__tests__/seedance-2-inputs.test.ts +173 -0
- package/src/__tests__/seedance-extend.test.ts +44 -0
- package/src/__tests__/sound-aggregator.test.ts +350 -0
- package/src/__tests__/style-presets.test.ts +34 -0
- package/src/__tests__/temporal-multi.test.ts +74 -0
- package/src/__tests__/transitions.test.ts +213 -0
- package/src/__tests__/video-reference-features.test.ts +41 -0
- package/src/__tests__/video-reference-leading-refs.test.ts +90 -0
- package/src/__tests__/video-reference-resolver.test.ts +233 -0
- package/src/__tests__/video-reference-roles.test.ts +96 -0
- package/src/__tests__/voice-character.test.ts +48 -0
- package/src/__tests__/voice-delivery.test.ts +41 -0
- package/src/__tests__/wardrobe.test.ts +25 -0
- package/src/action-fx.ts +255 -0
- package/src/aesthetic.ts +435 -0
- package/src/assemble-image-input.ts +236 -0
- package/src/assemble-suno-input.ts +147 -0
- package/src/atmosphere.ts +104 -0
- package/src/backdrop.ts +131 -0
- package/src/brand-tokens.ts +154 -0
- package/src/camera-format.ts +76 -0
- package/src/camera-motions.ts +615 -0
- package/src/character-fx.ts +274 -0
- package/src/color-look.ts +103 -0
- package/src/composition-effects.ts +67 -0
- package/src/entity-prompts.ts +231 -0
- package/src/era.ts +292 -0
- package/src/exposure-settings.ts +142 -0
- package/src/factory-presets/generate-image.ts +1644 -0
- package/src/factory-presets/generate-video.ts +1116 -0
- package/src/factory-presets/index.ts +46 -0
- package/src/factory-presets/lottie-overlay.ts +166 -0
- package/src/factory-presets/motion-graphics.ts +350 -0
- package/src/factory-presets/music.ts +734 -0
- package/src/factory-presets/sfx.ts +136 -0
- package/src/factory-presets/shared-image.ts +207 -0
- package/src/factory-presets/switchx.ts +65 -0
- package/src/factory-presets/text.ts +292 -0
- package/src/factory-presets/types.ts +51 -0
- package/src/factory-presets/video-edit.ts +136 -0
- package/src/factory-presets/voice.ts +172 -0
- package/src/factory-presets.ts +2 -0
- package/src/factory-snippets/catalog.ts +105 -0
- package/src/factory-snippets/index.ts +16 -0
- package/src/factory-snippets/types.ts +33 -0
- package/src/framing.ts +634 -0
- package/src/held-prop.ts +187 -0
- package/src/identity-lock.ts +213 -0
- package/src/index.ts +66 -0
- package/src/instrumentation.ts +337 -0
- package/src/lens.ts +59 -0
- package/src/lighting.ts +229 -0
- package/src/loop-subject.ts +276 -0
- package/src/materials.ts +184 -0
- package/src/mood.ts +186 -0
- package/src/music-genre.ts +662 -0
- package/src/music-mood.ts +137 -0
- package/src/object-asset-presets.ts +81 -0
- package/src/parameter-prompt-hint.ts +281 -0
- package/src/person.ts +1368 -0
- package/src/photo-genre.ts +151 -0
- package/src/photographer.ts +612 -0
- package/src/picker-analyzer-registry.ts +374 -0
- package/src/picker-catalogs.ts +858 -0
- package/src/pose.ts +246 -0
- package/src/post-process-effects.ts +94 -0
- package/src/prompt-builder-structured-fields.ts +116 -0
- package/src/prompt-builder.ts +2964 -0
- package/src/prompt-templates.ts +50 -0
- package/src/prompt-wizard-categories.ts +334 -0
- package/src/provider-prompt-doctrine.ts +85 -0
- package/src/render-quality.ts +89 -0
- package/src/resolve-prompt.ts +120 -0
- package/src/seedance-2-inputs.ts +100 -0
- package/src/setting.ts +130 -0
- package/src/sound-aggregator.ts +241 -0
- package/src/style-presets.ts +162 -0
- package/src/style.ts +99 -0
- package/src/styling.ts +585 -0
- package/src/temporal.ts +151 -0
- package/src/transitions.ts +333 -0
- package/src/video-reference-resolver.ts +823 -0
- package/src/voice-character.ts +237 -0
- package/src/voice-delivery.ts +142 -0
- package/src/wardrobe.ts +179 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { modelsWithFeature } from "@nodaro/shared"
|
|
3
|
+
|
|
4
|
+
// Guards the catalog `reference-image` feature against the actual provider
|
|
5
|
+
// transport. A video model that advertises `reference-image` lets the editor
|
|
6
|
+
// attach reference images AND suppresses the "references will be ignored"
|
|
7
|
+
// warning — so advertising it without a working backend path silently drops
|
|
8
|
+
// the user's references (the grok-imagine-video-1.5 bug, 2026-06-28 audit).
|
|
9
|
+
describe("video reference-image capability (catalog)", () => {
|
|
10
|
+
const refModels = modelsWithFeature("reference-image")
|
|
11
|
+
|
|
12
|
+
// grok-imagine-video-1.5 takes a single image_url (the i2v start frame) at
|
|
13
|
+
// KIE — there is no reference transport (no `maxRefImages`, no special path),
|
|
14
|
+
// so it must NOT advertise the feature.
|
|
15
|
+
it("excludes grok-imagine-video-1.5 (no reference transport)", () => {
|
|
16
|
+
expect(refModels).not.toContain("grok-imagine-video-1.5")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// The video models that genuinely forward references — verified against the
|
|
20
|
+
// backend provider paths in the 2026-06-28 audit (Seedance resolver,
|
|
21
|
+
// gemini-omni image_urls merge, VEO REFERENCE_2_VIDEO endpoint, kling-3-omni
|
|
22
|
+
// Replicate reference_images, and the `maxRefImages` merge for grok-i2v /
|
|
23
|
+
// happyhorse-ref2v) — must keep advertising it. Subset check, so wiring a
|
|
24
|
+
// NEW ref-capable model doesn't fail here; only breaking a wired one does.
|
|
25
|
+
it("includes the wired reference-capable video models", () => {
|
|
26
|
+
for (const id of [
|
|
27
|
+
"veo3",
|
|
28
|
+
"veo3.1",
|
|
29
|
+
"veo3_lite",
|
|
30
|
+
"gemini-omni-video",
|
|
31
|
+
"kling-3-omni",
|
|
32
|
+
"grok-i2v",
|
|
33
|
+
"happyhorse-ref2v",
|
|
34
|
+
"seedance-2",
|
|
35
|
+
"seedance-2-fast",
|
|
36
|
+
"seedance-2-mini",
|
|
37
|
+
]) {
|
|
38
|
+
expect(refModels).toContain(id)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D5 unified-numbering invariant for `resolveVideoReferenceCore`'s `leadingRefUrls`
|
|
3
|
+
* (unified-asset-references spec, 2026-06-29). Leading plain image-refs occupy
|
|
4
|
+
* `@image_1 … @image_offset`; every asset URL the core attaches (canonical /
|
|
5
|
+
* extras) is numbered AFTER them, prepended to `additionalUrls` so the worker
|
|
6
|
+
* payload position == the `@image_N` ordinal == the `{image:N}` token the editor
|
|
7
|
+
* offers. Frames are tail-appended elsewhere and out of scope here.
|
|
8
|
+
*
|
|
9
|
+
* The opt-in contract: WITHOUT `leadingRefUrls` the core behaves exactly as
|
|
10
|
+
* before (covered by the existing suite); these cases exercise the WITH path.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect } from "vitest"
|
|
13
|
+
import { resolveVideoReferenceCore } from "../video-reference-resolver.js"
|
|
14
|
+
|
|
15
|
+
const A = "https://cdn/a.png"
|
|
16
|
+
const B = "https://cdn/b.png"
|
|
17
|
+
const E1 = "https://cdn/e1.png"
|
|
18
|
+
const E2 = "https://cdn/e2.png"
|
|
19
|
+
|
|
20
|
+
describe("resolveVideoReferenceCore — leadingRefUrls (D5)", () => {
|
|
21
|
+
it("leading refs only: tokens number 1..offset, URLs returned in order", () => {
|
|
22
|
+
const r = resolveVideoReferenceCore({
|
|
23
|
+
prompt: "start {image:1:hero} mid {image:2:car} end",
|
|
24
|
+
wiredCharRefs: [],
|
|
25
|
+
leadingRefUrls: [A, B],
|
|
26
|
+
})
|
|
27
|
+
expect(r.additionalUrls).toEqual([A, B])
|
|
28
|
+
expect(r.prompt).toContain("the hero from @image_1")
|
|
29
|
+
expect(r.prompt).toContain("the car from @image_2")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("leading + extra asset: extra is numbered AFTER the leading refs (offset)", () => {
|
|
33
|
+
const r = resolveVideoReferenceCore({
|
|
34
|
+
prompt: "a scene of {image:2:object}",
|
|
35
|
+
wiredCharRefs: [],
|
|
36
|
+
// one plain ref leads → offset 1 → the extra is @image_2
|
|
37
|
+
leadingRefUrls: [A],
|
|
38
|
+
extraRefs: [{ url: E1, description: "object" }],
|
|
39
|
+
})
|
|
40
|
+
// worker payload: leading first, then the asset URL
|
|
41
|
+
expect(r.additionalUrls).toEqual([A, E1])
|
|
42
|
+
// the extra's directive bullet is numbered @image_2 (offset+1), NOT @image_1
|
|
43
|
+
expect(r.prompt).toContain("@image_2")
|
|
44
|
+
expect(r.prompt).not.toMatch(/@image_1\b/) // @image_1 is the (un-bulleted) leading ref
|
|
45
|
+
// the {image:2} body token resolves against the unified count (2)
|
|
46
|
+
expect(r.prompt).toContain("the object from @image_2")
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("out-of-range token (N > leading+assets) drops to the bare label", () => {
|
|
50
|
+
const r = resolveVideoReferenceCore({
|
|
51
|
+
prompt: "x {image:5:ghost} y",
|
|
52
|
+
wiredCharRefs: [],
|
|
53
|
+
leadingRefUrls: [A],
|
|
54
|
+
extraRefs: [{ url: E1, description: "obj" }],
|
|
55
|
+
})
|
|
56
|
+
// count is 2 → {image:5} is out of range → bare label, never raw token
|
|
57
|
+
expect(r.prompt).toContain("x ghost y")
|
|
58
|
+
expect(r.prompt).not.toContain("{image:5")
|
|
59
|
+
expect(r.prompt).not.toContain("@image_5")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("leading + 2 extras + referenceOrder: leading stays @image_1, assets renumber from offset+1", () => {
|
|
63
|
+
// assets e1,e2 are @image_2,@image_3; reorder swaps them → e2 first.
|
|
64
|
+
const r = resolveVideoReferenceCore({
|
|
65
|
+
prompt: "scene {image:2:first} and {image:3:second}",
|
|
66
|
+
wiredCharRefs: [],
|
|
67
|
+
leadingRefUrls: [A],
|
|
68
|
+
extraRefs: [
|
|
69
|
+
{ url: E1, description: "first" },
|
|
70
|
+
{ url: E2, description: "second" },
|
|
71
|
+
],
|
|
72
|
+
referenceOrder: [`wired:${E2}`, `wired:${E1}`], // manual-extra tile-id = `wired:<id>`
|
|
73
|
+
})
|
|
74
|
+
// leading ref stays first; the two assets are swapped by the reorder
|
|
75
|
+
expect(r.additionalUrls[0]).toBe(A)
|
|
76
|
+
expect(r.additionalUrls.slice(1)).toEqual([E2, E1])
|
|
77
|
+
// leading ordinal 1 is never remapped; the asset ordinals stay within 2..3
|
|
78
|
+
expect(r.prompt).not.toMatch(/@image_(0|4|5)\b/)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("no leadingRefUrls → unchanged legacy numbering (asset from @image_1)", () => {
|
|
82
|
+
const r = resolveVideoReferenceCore({
|
|
83
|
+
prompt: "a {image:1:object}",
|
|
84
|
+
wiredCharRefs: [],
|
|
85
|
+
extraRefs: [{ url: E1, description: "object" }],
|
|
86
|
+
})
|
|
87
|
+
expect(r.additionalUrls).toEqual([E1])
|
|
88
|
+
expect(r.prompt).toContain("the object from @image_1")
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { resolveVideoReferenceCore, REF_BINDING, resolveReferenceTokens } from "../video-reference-resolver.js"
|
|
3
|
+
import type { ConnectedReference } from "@nodaro/shared"
|
|
4
|
+
|
|
5
|
+
const charRef = (over: Partial<ConnectedReference>): ConnectedReference => ({
|
|
6
|
+
id: "n1", defaultName: "Kira", source: "wired-character", url: "https://r2/kira.png",
|
|
7
|
+
characterSlug: "kira", variantSlug: undefined, characterCanonicalDescription: null,
|
|
8
|
+
variantDescription: null, variantDisplayName: "canonical", ...over,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe("resolveVideoReferenceCore — character canonical fallback", () => {
|
|
12
|
+
// NOTE: the brief's draft asserted `toContain("Image 1")` here, but that is
|
|
13
|
+
// wrong for the DEFAULT "identical" usage mode: the canonical fallback emits a
|
|
14
|
+
// name-based bullet (`- Kira. <directive>`), never `Image N` numbering (only
|
|
15
|
+
// "name"/"none" modes or extras are positionally numbered). This lift-and-shift
|
|
16
|
+
// is behavior-preserving, so the assertion is corrected to the directive the
|
|
17
|
+
// resolver actually emits — identical to the backend mirror test
|
|
18
|
+
// (`payload-builder-video-mentions.test.ts`: "falls back to canonical URL when
|
|
19
|
+
// character is wired but not @-mentioned"). The `Image N` numbering path is
|
|
20
|
+
// still covered by the manual-extras test below.
|
|
21
|
+
it("emits a 'Use these characters' block with the identity directive for an unmentioned wired character", () => {
|
|
22
|
+
const out = resolveVideoReferenceCore({
|
|
23
|
+
prompt: "a woman dancing",
|
|
24
|
+
wiredCharRefs: [charRef({})],
|
|
25
|
+
})
|
|
26
|
+
expect(out.prompt).toContain("Use these characters:")
|
|
27
|
+
expect(out.prompt).toContain("The subject must remain exactly the same person")
|
|
28
|
+
expect(out.additionalUrls).toEqual(["https://r2/kira.png"])
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe("resolveVideoReferenceCore — manual extras", () => {
|
|
33
|
+
it("numbers a manual extra after the characters and emits its bullet", () => {
|
|
34
|
+
const out = resolveVideoReferenceCore({
|
|
35
|
+
prompt: "scene",
|
|
36
|
+
wiredCharRefs: [charRef({})],
|
|
37
|
+
extraRefs: [{ url: "https://r2/x.png", description: "a red car" }],
|
|
38
|
+
})
|
|
39
|
+
expect(out.additionalUrls).toEqual(["https://r2/kira.png", "https://r2/x.png"])
|
|
40
|
+
expect(out.prompt).toContain("@image_2 (reference): a red car")
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// FE/BE PARITY GOLDEN CASES (Phase 1, Task 1.4).
|
|
45
|
+
//
|
|
46
|
+
// The frontend (`video-prompt-assembly.ts`) and backend (`payload-builder.ts`)
|
|
47
|
+
// video-reference resolvers now BOTH delegate to `resolveVideoReferenceCore`,
|
|
48
|
+
// so they cannot diverge by construction. These three cases pin the behaviors
|
|
49
|
+
// that previously lived duplicated across both layers: same-character pair-back,
|
|
50
|
+
// `referenceOrder` reorder + `@image_N` renumber, and the `usageMode: "none"`
|
|
51
|
+
// "URL attached, no bullet" contract. Each expected string was derived by
|
|
52
|
+
// reading the core's loops (and the `applyReferenceOrderToVideo` →
|
|
53
|
+
// `buildTileIdForUrl` tile-id scheme in prompt-builder.ts) and cross-checked
|
|
54
|
+
// against the backend mirror suite (`payload-builder-video-mentions.test.ts`).
|
|
55
|
+
describe("resolveVideoReferenceCore — FE/BE parity golden cases", () => {
|
|
56
|
+
// Case 1 — pair-back. A wired character (canonical Kira at Image 1, default
|
|
57
|
+
// "identical" mode) plus a SAME-character extra (`characterSlug: "kira"`).
|
|
58
|
+
// The extras loop sees `positionsByChar.get("kira") === 1`, so instead of a
|
|
59
|
+
// fresh identity directive it emits the pair-back bullet "@image_2 is the same
|
|
60
|
+
// subject as @image_1." (no per-ref description → no trailing ", <desc>").
|
|
61
|
+
// Mirrors the backend's "pairs character-sourced extras back…" test, which
|
|
62
|
+
// asserts the byte-identical wording.
|
|
63
|
+
it("pairs a same-character extra back to the canonical character (@image_2 is the same subject as @image_1)", () => {
|
|
64
|
+
const out = resolveVideoReferenceCore({
|
|
65
|
+
prompt: "scene",
|
|
66
|
+
wiredCharRefs: [charRef({})],
|
|
67
|
+
extraRefs: [{ url: "https://r2/kira-extra.png", characterSlug: "kira" }],
|
|
68
|
+
})
|
|
69
|
+
// Canonical Kira first, then the paired-back extra — both URLs attached.
|
|
70
|
+
expect(out.additionalUrls).toEqual([
|
|
71
|
+
"https://r2/kira.png",
|
|
72
|
+
"https://r2/kira-extra.png",
|
|
73
|
+
])
|
|
74
|
+
// The extra's bullet is the same-subject pair-back, not a fresh directive.
|
|
75
|
+
expect(out.prompt).toContain("- @image_2 is the same subject as @image_1.")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Case 2 — referenceOrder reorder + `@image_N` renumber. Two DISTINCT wired
|
|
79
|
+
// characters in "name" mode (the canonical-fallback branch that emits
|
|
80
|
+
// positional `- @image_N (Name)` bullets; the default "identical" mode emits
|
|
81
|
+
// a name-only bullet with NO ordinal, so it can't exercise renumbering).
|
|
82
|
+
// `referenceOrder` keys on the stable tile ids from `buildTileIdForUrl`: an
|
|
83
|
+
// unmentioned wired-character canonical URL → `char-canonical:<slug>`.
|
|
84
|
+
// Reversing the two ids must reverse `additionalUrls` AND renumber the
|
|
85
|
+
// bullets so each `@image_N` tracks its new slot.
|
|
86
|
+
it("applies referenceOrder: reverses additionalUrls AND renumbers the @image_N bullets to follow the new order", () => {
|
|
87
|
+
const kira = charRef({ defaultUsageMode: "name" }) // → "- @image_1 (Kira)", char-canonical:kira
|
|
88
|
+
const mira = charRef({
|
|
89
|
+
id: "n2",
|
|
90
|
+
defaultName: "Mira",
|
|
91
|
+
url: "https://r2/mira.png",
|
|
92
|
+
characterSlug: "mira",
|
|
93
|
+
defaultUsageMode: "name", // → "- @image_2 (Mira)", char-canonical:mira
|
|
94
|
+
})
|
|
95
|
+
const out = resolveVideoReferenceCore({
|
|
96
|
+
prompt: "two people talking",
|
|
97
|
+
wiredCharRefs: [kira, mira],
|
|
98
|
+
// Reverse: Mira's tile first, Kira's second.
|
|
99
|
+
referenceOrder: ["char-canonical:mira", "char-canonical:kira"],
|
|
100
|
+
})
|
|
101
|
+
// URL list reversed to match referenceOrder.
|
|
102
|
+
expect(out.additionalUrls).toEqual([
|
|
103
|
+
"https://r2/mira.png",
|
|
104
|
+
"https://r2/kira.png",
|
|
105
|
+
])
|
|
106
|
+
// Bullets renumbered to follow the new order: Mira → @image_1, Kira → @image_2.
|
|
107
|
+
expect(out.prompt).toContain("- @image_1 (Mira)")
|
|
108
|
+
expect(out.prompt).toContain("- @image_2 (Kira)")
|
|
109
|
+
// The pre-reorder numbering is gone — proves the renumber pass actually ran
|
|
110
|
+
// (not just an unchanged prompt riding along with reordered URLs).
|
|
111
|
+
expect(out.prompt).not.toContain("- @image_1 (Kira)")
|
|
112
|
+
expect(out.prompt).not.toContain("- @image_2 (Mira)")
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Case 3 — usageMode "none" extra. A wired Kira (so the slug is known and
|
|
116
|
+
// canonically attached at @image_1) plus a same-character extra with
|
|
117
|
+
// `usageMode: "none"`. The extra's URL IS attached, but the pair-back branch's
|
|
118
|
+
// `effectiveMode !== "none"` guard suppresses any bullet for it — the
|
|
119
|
+
// minimal-intervention contract (image speaks for itself, no textual bias).
|
|
120
|
+
it("attaches a usageMode:'none' extra's URL but emits NO bullet for it", () => {
|
|
121
|
+
const out = resolveVideoReferenceCore({
|
|
122
|
+
prompt: "scene",
|
|
123
|
+
wiredCharRefs: [charRef({})],
|
|
124
|
+
extraRefs: [
|
|
125
|
+
{ url: "https://r2/kira-extra.png", characterSlug: "kira", usageMode: "none" },
|
|
126
|
+
],
|
|
127
|
+
})
|
|
128
|
+
// The extra's URL is still attached to the worker payload.
|
|
129
|
+
expect(out.additionalUrls).toEqual([
|
|
130
|
+
"https://r2/kira.png",
|
|
131
|
+
"https://r2/kira-extra.png",
|
|
132
|
+
])
|
|
133
|
+
// Exactly one bullet (Kira's canonical) — the "none" extra adds none.
|
|
134
|
+
const bulletLines = (out.prompt ?? "").split("\n").filter((l) => l.startsWith("- "))
|
|
135
|
+
expect(bulletLines).toHaveLength(1)
|
|
136
|
+
// Neither a pair-back ("same subject") nor the extra's positional marker.
|
|
137
|
+
expect(out.prompt).not.toContain("same subject")
|
|
138
|
+
expect(out.prompt).not.toContain("@image_2")
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// REF_BINDING — the single swap-point for the binding surface-string (D1/D7).
|
|
143
|
+
// This is purely additive in Task 2.1: defined + exported + tested here, but NOT
|
|
144
|
+
// yet wired into the core's logic (that is Task 2.2). The default form is
|
|
145
|
+
// `@image_N`; flipping to the legacy `Image N` form is editing the five arrows
|
|
146
|
+
// in `video-reference-resolver.ts` only.
|
|
147
|
+
describe("REF_BINDING", () => {
|
|
148
|
+
it("formats subject + ordinal + frame in @image_N form by default", () => {
|
|
149
|
+
expect(REF_BINDING.image("person", 2)).toBe("the person from @image_2")
|
|
150
|
+
expect(REF_BINDING.ordinal(1)).toBe("@image_1")
|
|
151
|
+
expect(REF_BINDING.frame(3, "opening")).toBe("Use @image_3 as the opening (first) frame of the video.")
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// resolveReferenceTokens — rewrites editor `{image|video|audio:N:label}` tokens
|
|
156
|
+
// into `@image_N`-style subject bindings (Task 2.3). Positional, 1-based against
|
|
157
|
+
// the corresponding count. Out-of-range / missing count → drop to the bare label
|
|
158
|
+
// (legacy `stripVideoImageTokens` strip behavior). Label-less in-range token →
|
|
159
|
+
// "the subject in @kind_N" so the binding still lands. Purely additive in 2.3 —
|
|
160
|
+
// defined + exported + tested here, NOT yet called by the core (that is Task 2.4).
|
|
161
|
+
describe("resolveReferenceTokens", () => {
|
|
162
|
+
const counts = { image: 4, video: 0, audio: 0 }
|
|
163
|
+
it("resolves repeated labels distinctly by slot", () => {
|
|
164
|
+
expect(resolveReferenceTokens(
|
|
165
|
+
"{image:1:person} wearing {image:2:clothes} and {image:3:person} wearing {image:4:clothes}", counts))
|
|
166
|
+
.toBe("the person from @image_1 wearing the clothes from @image_2 and the person from @image_3 wearing the clothes from @image_4")
|
|
167
|
+
})
|
|
168
|
+
it("drops out-of-range tokens to bare label", () => {
|
|
169
|
+
expect(resolveReferenceTokens("a {image:9:ghost} here", counts)).toBe("a ghost here")
|
|
170
|
+
})
|
|
171
|
+
it("handles a label-less token", () => {
|
|
172
|
+
expect(resolveReferenceTokens("{image:1}", counts)).toBe("the subject in @image_1")
|
|
173
|
+
})
|
|
174
|
+
it("resolves video + audio tokens against their own counts", () => {
|
|
175
|
+
expect(resolveReferenceTokens("dance like {video:1:clip} to {audio:1:song}", { image: 0, video: 1, audio: 1 }))
|
|
176
|
+
.toBe("dance like the clip from @video_1 to the song from @audio_1")
|
|
177
|
+
})
|
|
178
|
+
// The docstring's stated intent is "runs of 2+ SPACES" — horizontal cleanup
|
|
179
|
+
// after a dropped label-less token. The collapse must NOT eat newline block
|
|
180
|
+
// separators (`\n\n` between the "Use these characters:" block and the body),
|
|
181
|
+
// which is exactly the surface this function is applied to once Task 2.4 wires
|
|
182
|
+
// it into the FULLY-ASSEMBLED core prompt. Guards against a regression back to
|
|
183
|
+
// `\s{2,}` (which also matches `\n`).
|
|
184
|
+
it("preserves \\n\\n block separators (collapses horizontal whitespace only)", () => {
|
|
185
|
+
expect(resolveReferenceTokens("Use these characters:\n- Kira.\n\n{image:1:person} runs", { image: 1, video: 0, audio: 0 }))
|
|
186
|
+
.toBe("Use these characters:\n- Kira.\n\nthe person from @image_1 runs")
|
|
187
|
+
})
|
|
188
|
+
it("still collapses the horizontal gap left by a dropped label-less token", () => {
|
|
189
|
+
expect(resolveReferenceTokens("walk past {image:9} slowly", { image: 1, video: 0, audio: 0 }))
|
|
190
|
+
.toBe("walk past slowly")
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// Task 2.4 — the core resolves the editor's curly `{image:N}` / `{video:N}` /
|
|
195
|
+
// `{audio:N}` body tokens into `@image_N`-style subject bindings via
|
|
196
|
+
// resolveReferenceTokens, applied LAST (after mention/canonical/extras assembly
|
|
197
|
+
// AND after the optional `referenceOrder` reorder, so the reorder's `@image_N`
|
|
198
|
+
// renumber never touches a freshly-resolved body token). The positional counts
|
|
199
|
+
// come from the caller-declared reference-handle totals (`imageRefCount` /
|
|
200
|
+
// `videoRefCount` / `audioRefCount`); image falls back to the core's own merged
|
|
201
|
+
// URL count when the caller hasn't wired it through yet (pre-Tasks 3.2/4.1).
|
|
202
|
+
describe("resolveVideoReferenceCore — {image:N} body-token resolution (Task 2.4)", () => {
|
|
203
|
+
it("resolves {image:N} subject tokens in the body against the reference count", () => {
|
|
204
|
+
const out = resolveVideoReferenceCore({
|
|
205
|
+
prompt: "camera circles {image:1:person} for a 360 spin",
|
|
206
|
+
wiredCharRefs: [charRef({})],
|
|
207
|
+
imageRefCount: 1,
|
|
208
|
+
})
|
|
209
|
+
expect(out.prompt).toContain("camera circles the person from @image_1 for a 360 spin")
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("resolves {image:N} on the early-return path (no wired chars, no extras) using imageRefCount", () => {
|
|
213
|
+
const out = resolveVideoReferenceCore({
|
|
214
|
+
prompt: "a slow dolly toward {image:1:object} on the table",
|
|
215
|
+
wiredCharRefs: [],
|
|
216
|
+
imageRefCount: 1,
|
|
217
|
+
})
|
|
218
|
+
// Early return (no chars, no extras) — the whole prompt is the resolved body.
|
|
219
|
+
expect(out.prompt).toBe("a slow dolly toward the object from @image_1 on the table")
|
|
220
|
+
expect(out.additionalUrls).toEqual([])
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it("preserves the canonical block's \\n\\n separator while resolving the body token (no \\s-collapse regression)", () => {
|
|
224
|
+
const out = resolveVideoReferenceCore({
|
|
225
|
+
prompt: "circle {image:1:person}",
|
|
226
|
+
wiredCharRefs: [charRef({})],
|
|
227
|
+
imageRefCount: 1,
|
|
228
|
+
})
|
|
229
|
+
// The block + body stay separated by the blank line; only the curly token
|
|
230
|
+
// changed. Proves the assembled `\n\n` survives the token-resolution pass.
|
|
231
|
+
expect(out.prompt).toContain("\n\ncircle the person from @image_1")
|
|
232
|
+
})
|
|
233
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { resolveVideoReferenceCore } from "../video-reference-resolver.js"
|
|
3
|
+
import type { ConnectedReference } from "@nodaro/shared"
|
|
4
|
+
|
|
5
|
+
const kira = (over: Partial<ConnectedReference> = {}): ConnectedReference => ({
|
|
6
|
+
id: "k", defaultName: "Kira", source: "wired-character", url: "u-kira", characterSlug: "kira", ...over,
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
describe("video character convergence (hybridRoles)", () => {
|
|
10
|
+
it("hybridRoles: @-mention → 'the face from @image_1', no legacy block", () => {
|
|
11
|
+
const out = resolveVideoReferenceCore({
|
|
12
|
+
prompt: "@kira:1:face runs", wiredCharRefs: [kira()], hybridRoles: true,
|
|
13
|
+
})
|
|
14
|
+
expect(out.prompt).toContain("the face from @image_1 runs")
|
|
15
|
+
expect(out.prompt).not.toContain("Use these characters:")
|
|
16
|
+
expect(out.additionalUrls).toContain("u-kira")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("hybridRoles: unmentioned character → canonical 'the person from @image_1', no block, no auto-lock", () => {
|
|
20
|
+
const out = resolveVideoReferenceCore({
|
|
21
|
+
prompt: "a slow dolly", wiredCharRefs: [kira()], hybridRoles: true,
|
|
22
|
+
})
|
|
23
|
+
expect(out.prompt).toContain("the person from @image_1")
|
|
24
|
+
expect(out.prompt).not.toContain("Lock the exact identity")
|
|
25
|
+
expect(out.prompt).not.toContain("Use these characters:")
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("opt-in lock appears when enabled", () => {
|
|
29
|
+
const out = resolveVideoReferenceCore({
|
|
30
|
+
prompt: "a portrait", wiredCharRefs: [kira({ identityLock: { enabled: true } })], hybridRoles: true,
|
|
31
|
+
})
|
|
32
|
+
expect(out.prompt).toContain("Lock the exact identity of the person in @image_1")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("elementInjection preserved (unmentioned/canonical path)", () => {
|
|
36
|
+
const out = resolveVideoReferenceCore({
|
|
37
|
+
prompt: "a cafe", wiredCharRefs: [kira({ elementInjection: "holding a smartphone" })], hybridRoles: true,
|
|
38
|
+
})
|
|
39
|
+
expect(out.prompt?.toLowerCase()).toContain("holding a smartphone")
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("elementInjection preserved on the MENTIONED path (with role phrase)", () => {
|
|
43
|
+
const out = resolveVideoReferenceCore({
|
|
44
|
+
prompt: "@kira:1 walks in", wiredCharRefs: [kira({ elementInjection: "wearing a red scarf" })], hybridRoles: true,
|
|
45
|
+
})
|
|
46
|
+
expect(out.prompt?.toLowerCase()).toContain("red scarf")
|
|
47
|
+
expect(out.prompt).toContain("the person from @image_1")
|
|
48
|
+
expect(out.prompt).not.toContain("Use these characters:")
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("hybridRoles: manual extra surfaces description tied to @image_N, URL attached, no block, no legacy bullet", () => {
|
|
52
|
+
const out = resolveVideoReferenceCore({
|
|
53
|
+
prompt: "a still life",
|
|
54
|
+
wiredCharRefs: [],
|
|
55
|
+
extraRefs: [{ url: "u-lantern", description: "a brass lantern" }],
|
|
56
|
+
hybridRoles: true,
|
|
57
|
+
})
|
|
58
|
+
expect(out.prompt?.toLowerCase()).toContain("a brass lantern")
|
|
59
|
+
expect(out.prompt).toContain("@image_1")
|
|
60
|
+
expect(out.prompt).not.toContain("Use these characters:")
|
|
61
|
+
expect(out.prompt).not.toContain("(reference):") // legacy manual shape gone
|
|
62
|
+
expect(out.additionalUrls).toContain("u-lantern")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("hybridRoles: same-character extra pairs back to the earlier @image_M", () => {
|
|
66
|
+
const out = resolveVideoReferenceCore({
|
|
67
|
+
prompt: "a duet",
|
|
68
|
+
wiredCharRefs: [kira()], // canonical → @image_1
|
|
69
|
+
extraRefs: [{ url: "u-kira-2", characterSlug: "kira", description: "mid-action" }],
|
|
70
|
+
lookupCharacterBySlug: () => ({ characterName: "Kira" }),
|
|
71
|
+
hybridRoles: true,
|
|
72
|
+
})
|
|
73
|
+
expect(out.prompt).toContain("@image_2 is the same subject as @image_1")
|
|
74
|
+
expect(out.prompt?.toLowerCase()).toContain("mid-action")
|
|
75
|
+
expect(out.additionalUrls).toEqual(["u-kira", "u-kira-2"])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("hybridRoles: first-sight character extra → mapped role phrase + elementInjection, no block", () => {
|
|
79
|
+
const out = resolveVideoReferenceCore({
|
|
80
|
+
prompt: "a reveal",
|
|
81
|
+
wiredCharRefs: [],
|
|
82
|
+
extraRefs: [{ url: "u-vex", characterSlug: "vex", usageMode: "face", elementInjection: "wearing a red scarf" }],
|
|
83
|
+
lookupCharacterBySlug: () => ({ characterName: "Vex" }),
|
|
84
|
+
hybridRoles: true,
|
|
85
|
+
})
|
|
86
|
+
expect(out.prompt).toContain("the face from @image_1")
|
|
87
|
+
expect(out.prompt?.toLowerCase()).toContain("red scarf")
|
|
88
|
+
expect(out.prompt).not.toContain("Use these characters:")
|
|
89
|
+
expect(out.additionalUrls).toContain("u-vex")
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("LEGACY (hybridRoles false/default) unchanged → still the block", () => {
|
|
93
|
+
const out = resolveVideoReferenceCore({ prompt: "@kira:1 runs", wiredCharRefs: [kira()] })
|
|
94
|
+
expect(out.prompt).toContain("Use these characters:")
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
VOICE_AGES,
|
|
4
|
+
VOICE_GENDERS,
|
|
5
|
+
VOICE_ACCENTS,
|
|
6
|
+
VOICE_TIMBRES,
|
|
7
|
+
buildVoiceCharacterHints,
|
|
8
|
+
VOICE_CHARACTER_DEFAULT_DATA,
|
|
9
|
+
} from "../voice-character.js"
|
|
10
|
+
|
|
11
|
+
describe("voice-character catalogs", () => {
|
|
12
|
+
it("all four catalogs are non-empty with unique ids", () => {
|
|
13
|
+
for (const list of [VOICE_AGES, VOICE_GENDERS, VOICE_ACCENTS, VOICE_TIMBRES]) {
|
|
14
|
+
expect(list.length).toBeGreaterThan(0)
|
|
15
|
+
const ids = new Set(list.map((x) => x.id))
|
|
16
|
+
expect(ids.size).toBe(list.length)
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe("buildVoiceCharacterHints", () => {
|
|
22
|
+
it("returns empty for empty data", () => {
|
|
23
|
+
expect(buildVoiceCharacterHints({})).toBe("")
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("returns single field hint as bare phrase", () => {
|
|
27
|
+
const t = VOICE_TIMBRES[0]
|
|
28
|
+
expect(buildVoiceCharacterHints({ timbre: t.id })).toContain(t.promptHint)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("composes [age] [gender] voice with [timbre] timbre and [accent] accent", () => {
|
|
32
|
+
const age = VOICE_AGES[0]
|
|
33
|
+
const gender = VOICE_GENDERS[0]
|
|
34
|
+
const accent = VOICE_ACCENTS[0]
|
|
35
|
+
const timbre = VOICE_TIMBRES[0]
|
|
36
|
+
const out = buildVoiceCharacterHints({
|
|
37
|
+
age: age.id, gender: gender.id, accent: accent.id, timbre: timbre.id,
|
|
38
|
+
})
|
|
39
|
+
expect(out).toContain(age.promptHint)
|
|
40
|
+
expect(out).toContain(gender.promptHint)
|
|
41
|
+
expect(out).toContain(accent.promptHint)
|
|
42
|
+
expect(out).toContain(timbre.promptHint)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("falls back gracefully on unknown ids", () => {
|
|
46
|
+
expect(buildVoiceCharacterHints({ age: "not-real" })).toBe("")
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
VOICE_PACES,
|
|
4
|
+
VOICE_EMOTIONS,
|
|
5
|
+
VOICE_ARCHETYPES,
|
|
6
|
+
buildVoiceDeliveryHints,
|
|
7
|
+
VOICE_DELIVERY_DEFAULT_DATA,
|
|
8
|
+
} from "../voice-delivery.js"
|
|
9
|
+
|
|
10
|
+
describe("voice-delivery catalogs", () => {
|
|
11
|
+
it("all three catalogs are non-empty with unique ids", () => {
|
|
12
|
+
for (const list of [VOICE_PACES, VOICE_EMOTIONS, VOICE_ARCHETYPES]) {
|
|
13
|
+
expect(list.length).toBeGreaterThan(0)
|
|
14
|
+
const ids = new Set(list.map((x) => x.id))
|
|
15
|
+
expect(ids.size).toBe(list.length)
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe("buildVoiceDeliveryHints", () => {
|
|
21
|
+
it("returns empty for empty data", () => {
|
|
22
|
+
expect(buildVoiceDeliveryHints({})).toBe("")
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("composes [pace] [archetype]-style delivery, [emotion] tone", () => {
|
|
26
|
+
const pace = VOICE_PACES[0]
|
|
27
|
+
const emotion = VOICE_EMOTIONS[0]
|
|
28
|
+
const archetype = VOICE_ARCHETYPES[0]
|
|
29
|
+
const out = buildVoiceDeliveryHints({
|
|
30
|
+
pace: pace.id, emotion: emotion.id, archetype: archetype.id,
|
|
31
|
+
})
|
|
32
|
+
expect(out).toContain(pace.promptHint)
|
|
33
|
+
expect(out).toContain(emotion.promptHint)
|
|
34
|
+
expect(out).toContain(archetype.promptHint)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("handles partial fields gracefully", () => {
|
|
38
|
+
const e = VOICE_EMOTIONS[0]
|
|
39
|
+
expect(buildVoiceDeliveryHints({ emotion: e.id })).toContain(e.promptHint)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { buildWardrobeHints, getWardrobePromptHint, WARDROBE_DIMENSION_ORDER } from "../wardrobe.js"
|
|
3
|
+
|
|
4
|
+
describe("buildWardrobeHints", () => {
|
|
5
|
+
it("returns [] for empty value", () => {
|
|
6
|
+
expect(buildWardrobeHints({})).toEqual([])
|
|
7
|
+
})
|
|
8
|
+
it("emits single-pick hints in dimension order", () => {
|
|
9
|
+
const hints = buildWardrobeHints({ archetype: "wd-formal", top: "wd-blouse", colorPalette: "wd-all-black" })
|
|
10
|
+
expect(hints[0]).toBe(getWardrobePromptHint("wd-formal"))
|
|
11
|
+
expect(hints).toContain(getWardrobePromptHint("wd-blouse"))
|
|
12
|
+
expect(hints).toContain(getWardrobePromptHint("wd-all-black"))
|
|
13
|
+
})
|
|
14
|
+
it("suppresses the 'none' outerwear sentinel", () => {
|
|
15
|
+
expect(buildWardrobeHints({ outerwear: "wd-outer-none" })).toEqual([])
|
|
16
|
+
})
|
|
17
|
+
it("emits multi-pick accessories independently", () => {
|
|
18
|
+
const hints = buildWardrobeHints({ accessories: ["wd-glasses", "wd-scarf"] })
|
|
19
|
+
expect(hints).toContain(getWardrobePromptHint("wd-glasses"))
|
|
20
|
+
expect(hints).toContain(getWardrobePromptHint("wd-scarf"))
|
|
21
|
+
})
|
|
22
|
+
it("WARDROBE_DIMENSION_ORDER has no duplicates", () => {
|
|
23
|
+
expect(new Set(WARDROBE_DIMENSION_ORDER).size).toBe(WARDROBE_DIMENSION_ORDER.length)
|
|
24
|
+
})
|
|
25
|
+
})
|