@nodaro/prompts 1.47.0 → 1.49.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/{catalog-packs-D7NJpo4k.d.ts → catalog-packs-BzzZCjbz.d.ts} +1 -1
- package/dist/{catalog-packs-C9CXEWfl.d.cts → catalog-packs-C--EzV3K.d.cts} +1 -1
- package/dist/{chunk-PHR4UETI.cjs → chunk-3AVLIA4B.cjs} +18 -18
- package/dist/{chunk-PHR4UETI.cjs.map → chunk-3AVLIA4B.cjs.map} +1 -1
- package/dist/{chunk-OSKABA64.cjs → chunk-3HMMG6W4.cjs} +8 -8
- package/dist/{chunk-OSKABA64.cjs.map → chunk-3HMMG6W4.cjs.map} +1 -1
- package/dist/{chunk-NN4FL7SB.js → chunk-3J52HGBL.js} +4 -4
- package/dist/{chunk-NN4FL7SB.js.map → chunk-3J52HGBL.js.map} +1 -1
- package/dist/{chunk-TXHFL725.js → chunk-4V4JJUEH.js} +102 -54
- package/dist/chunk-4V4JJUEH.js.map +1 -0
- package/dist/{chunk-CPKYIVWP.js → chunk-7C7XLZPU.js} +12 -9
- package/dist/chunk-7C7XLZPU.js.map +1 -0
- package/dist/{chunk-YCCQXX3U.cjs → chunk-7VVVHIMH.cjs} +39 -39
- package/dist/{chunk-YCCQXX3U.cjs.map → chunk-7VVVHIMH.cjs.map} +1 -1
- package/dist/{chunk-2OIKQ4WB.js → chunk-FDTRN7OE.js} +5 -5
- package/dist/{chunk-2OIKQ4WB.js.map → chunk-FDTRN7OE.js.map} +1 -1
- package/dist/{chunk-JHEZJKTX.cjs → chunk-GTN3UO74.cjs} +113 -56
- package/dist/chunk-GTN3UO74.cjs.map +1 -0
- package/dist/{chunk-C4FDHCEZ.cjs → chunk-KBIA4R6X.cjs} +125 -125
- package/dist/{chunk-C4FDHCEZ.cjs.map → chunk-KBIA4R6X.cjs.map} +1 -1
- package/dist/{chunk-2HJJBXGA.js → chunk-MJ5OXUDF.js} +6 -6
- package/dist/{chunk-2HJJBXGA.js.map → chunk-MJ5OXUDF.js.map} +1 -1
- package/dist/{chunk-OY7XOPDG.cjs → chunk-NBJR7JS4.cjs} +57 -57
- package/dist/chunk-NBJR7JS4.cjs.map +1 -0
- package/dist/{chunk-VAK4RUNU.cjs → chunk-NET25TEQ.cjs} +13 -13
- package/dist/{chunk-VAK4RUNU.cjs.map → chunk-NET25TEQ.cjs.map} +1 -1
- package/dist/{chunk-SMKIVT7P.js → chunk-NRBO4I4G.js} +4 -4
- package/dist/{chunk-SMKIVT7P.js.map → chunk-NRBO4I4G.js.map} +1 -1
- package/dist/{chunk-FCTK7RGO.cjs → chunk-OXJ5GN57.cjs} +42 -39
- package/dist/chunk-OXJ5GN57.cjs.map +1 -0
- package/dist/{chunk-IKHRZ5UQ.js → chunk-PJPCNKEJ.js} +3 -3
- package/dist/{chunk-IKHRZ5UQ.js.map → chunk-PJPCNKEJ.js.map} +1 -1
- package/dist/{chunk-XZXUU3YH.js → chunk-QN3OP6TG.js} +8 -3
- package/dist/chunk-QN3OP6TG.js.map +1 -0
- package/dist/{chunk-UNR34GYU.cjs → chunk-WW7IHRIE.cjs} +8 -3
- package/dist/chunk-WW7IHRIE.cjs.map +1 -0
- package/dist/{chunk-57PTYJJX.js → chunk-XVAVCHPJ.js} +13 -13
- package/dist/chunk-XVAVCHPJ.js.map +1 -0
- package/dist/entries/assembly.cjs +29 -29
- package/dist/entries/assembly.d.cts +1 -2
- package/dist/entries/assembly.d.ts +1 -2
- package/dist/entries/assembly.js +6 -6
- package/dist/entries/catalog-packs.d.cts +3 -3
- package/dist/entries/catalog-packs.d.ts +3 -3
- package/dist/entries/person.d.cts +2 -2
- package/dist/entries/person.d.ts +2 -2
- package/dist/entries/picker-art.cjs +19 -19
- package/dist/entries/picker-art.d.cts +5 -0
- package/dist/entries/picker-art.d.ts +5 -0
- package/dist/entries/picker-art.js +1 -1
- package/dist/entries/picker-catalogs.cjs +57 -57
- package/dist/entries/picker-catalogs.d.cts +26 -4
- package/dist/entries/picker-catalogs.d.ts +26 -4
- package/dist/entries/picker-catalogs.js +5 -5
- package/dist/entries/pickers.cjs +410 -374
- package/dist/entries/pickers.d.cts +524 -3
- package/dist/entries/pickers.d.ts +524 -3
- package/dist/entries/pickers.js +3 -3
- package/dist/entries/prompt-builder.cjs +76 -76
- package/dist/entries/prompt-builder.d.cts +2 -3
- package/dist/entries/prompt-builder.d.ts +2 -3
- package/dist/entries/prompt-builder.js +5 -5
- package/dist/index.cjs +580 -544
- package/dist/index.d.cts +5 -6
- package/dist/index.d.ts +5 -6
- package/dist/index.js +9 -9
- package/dist/{picker-catalogs-C3utGMRB.d.cts → picker-catalogs-BylSIFKL.d.cts} +17 -0
- package/dist/{picker-catalogs-BM8IphF1.d.ts → picker-catalogs-DHRyjPXb.d.ts} +17 -0
- package/dist/{subject-registry-DFiGx49v.d.ts → subject-registry-Cf802LQ5.d.ts} +8 -4
- package/dist/{subject-registry-CQi_CZfJ.d.cts → subject-registry-D2uMBVpP.d.cts} +8 -4
- package/package.json +2 -2
- package/src/__tests__/assemble-video-input.test.ts +13 -11
- package/src/__tests__/direction-registry.test.ts +5 -5
- package/src/__tests__/prompt-style-section.test.ts +2 -2
- package/src/__tests__/style-media.test.ts +141 -0
- package/src/__tests__/style-video-wording.test.ts +273 -0
- package/src/catalog-overlay.ts +15 -1
- package/src/direction-registry.ts +25 -7
- package/src/parameter-prompt-hint.ts +23 -7
- package/src/picker-art/look-preview-sets.ts +5 -0
- package/src/picker-catalogs.ts +30 -0
- package/src/style.ts +163 -50
- package/dist/chunk-57PTYJJX.js.map +0 -1
- package/dist/chunk-CPKYIVWP.js.map +0 -1
- package/dist/chunk-FCTK7RGO.cjs.map +0 -1
- package/dist/chunk-JHEZJKTX.cjs.map +0 -1
- package/dist/chunk-OY7XOPDG.cjs.map +0 -1
- package/dist/chunk-TXHFL725.js.map +0 -1
- package/dist/chunk-UNR34GYU.cjs.map +0 -1
- package/dist/chunk-XZXUU3YH.js.map +0 -1
- package/dist/transitions-BLFfAJUH.d.cts +0 -450
- package/dist/transitions-BgwWM37i.d.ts +0 -450
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A style's medium, tones and media (`Style.medium` / `tones` / `media`):
|
|
3
|
+
* - every style classifies itself; the 49 styles offered for images and video
|
|
4
|
+
* keep exactly the medium and tones Nodaro Motion's art-style line has read
|
|
5
|
+
* them with so far (its own id lists), so moving them into the catalog
|
|
6
|
+
* changes no prompt;
|
|
7
|
+
* - the video-only styles are rendered 3D looks, offered for video alone;
|
|
8
|
+
* - image style lists leave them out, and the wire carries `media` so a thin
|
|
9
|
+
* client can do the same.
|
|
10
|
+
*/
|
|
11
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
12
|
+
import {
|
|
13
|
+
STYLES,
|
|
14
|
+
STYLE_MEDIUMS,
|
|
15
|
+
getStyleMedium,
|
|
16
|
+
getStyleTones,
|
|
17
|
+
styleOffersMedia,
|
|
18
|
+
stylesForMedia,
|
|
19
|
+
styleOffersRestyle,
|
|
20
|
+
stylesForRestyle,
|
|
21
|
+
getPickerCatalog,
|
|
22
|
+
projectPickerCatalog,
|
|
23
|
+
registerCatalogPack,
|
|
24
|
+
resetCatalogPacks,
|
|
25
|
+
} from "../index.js"
|
|
26
|
+
|
|
27
|
+
afterEach(() => resetCatalogPacks())
|
|
28
|
+
|
|
29
|
+
/** Nodaro Motion's lists as they read the 49 styles before the catalog carried a medium. */
|
|
30
|
+
const PHOTOGRAPHIC = ["photorealistic", "cinematic", "noir", "early-color-photo", "retro-vintage", "cyberpunk", "steampunk", "glitch-art"]
|
|
31
|
+
const RENDERED = ["3d-render", "low-poly", "isometric", "pixar-3d", "claymation"]
|
|
32
|
+
const BLACK_AND_WHITE = ["noir", "manga", "pencil-sketch", "charcoal", "etching", "woodcut", "ink-wash"]
|
|
33
|
+
const ONE_COLOUR = ["blueprint", "ascii-art", "chalkboard"]
|
|
34
|
+
const VIDEO_ONLY = ["felt-puppet", "knitted-yarn", "toy-figure-3d", "painterly-3d", "painted-clay"]
|
|
35
|
+
/** Left out of restyle lists: in tests each came out as the same plain 2D cartoon, without its own look. */
|
|
36
|
+
const NOT_FOR_RESTYLE = ["fantasy", "concept-art", "surrealism"]
|
|
37
|
+
|
|
38
|
+
const both = STYLES.filter((s) => s.media === undefined)
|
|
39
|
+
|
|
40
|
+
describe("medium and tones", () => {
|
|
41
|
+
it("every style declares a known medium", () => {
|
|
42
|
+
for (const s of STYLES) expect(STYLE_MEDIUMS, s.id).toContain(s.medium)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("the 49 styles for images and video keep the medium Nodaro Motion read them with", () => {
|
|
46
|
+
expect(both).toHaveLength(49)
|
|
47
|
+
for (const s of both) {
|
|
48
|
+
const expected = PHOTOGRAPHIC.includes(s.id) ? "photo" : RENDERED.includes(s.id) ? "3d" : "2d"
|
|
49
|
+
expect(getStyleMedium(s.id), s.id).toBe(expected)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("tones: black and white, one colour, or none", () => {
|
|
54
|
+
for (const s of STYLES) {
|
|
55
|
+
const expected = BLACK_AND_WHITE.includes(s.id) ? "black-and-white" : ONE_COLOUR.includes(s.id) ? "one-colour" : undefined
|
|
56
|
+
expect(getStyleTones(s.id), s.id).toBe(expected)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("an unknown id has no medium and no tones", () => {
|
|
61
|
+
expect(getStyleMedium("no-such-style")).toBeUndefined()
|
|
62
|
+
expect(getStyleMedium(undefined)).toBeUndefined()
|
|
63
|
+
expect(getStyleTones("no-such-style")).toBeUndefined()
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe("media", () => {
|
|
68
|
+
it("the video-only styles are rendered 3D looks, offered for video alone", () => {
|
|
69
|
+
const videoOnly = STYLES.filter((s) => s.media !== undefined)
|
|
70
|
+
expect(videoOnly.map((s) => s.id)).toEqual(VIDEO_ONLY)
|
|
71
|
+
for (const s of videoOnly) {
|
|
72
|
+
expect(s.media, s.id).toEqual(["video"])
|
|
73
|
+
expect(getStyleMedium(s.id), s.id).toBe("3d")
|
|
74
|
+
expect(styleOffersMedia(s, "video"), s.id).toBe(true)
|
|
75
|
+
expect(styleOffersMedia(s, "image"), s.id).toBe(false)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("an image list leaves out the video-only styles; a video list has every style, in catalog order", () => {
|
|
80
|
+
expect(stylesForMedia(STYLES, "image").map((s) => s.id)).toEqual(both.map((s) => s.id))
|
|
81
|
+
expect(stylesForMedia(STYLES, "video").map((s) => s.id)).toEqual(STYLES.map((s) => s.id))
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("a style with no media is offered for both", () => {
|
|
85
|
+
expect(styleOffersMedia({}, "image")).toBe(true)
|
|
86
|
+
expect(styleOffersMedia({}, "video")).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe("restyle", () => {
|
|
91
|
+
it("exactly the styles that do not hold up as a restyle say so", () => {
|
|
92
|
+
expect(STYLES.filter((s) => !styleOffersRestyle(s)).map((s) => s.id)).toEqual(NOT_FOR_RESTYLE)
|
|
93
|
+
for (const id of NOT_FOR_RESTYLE) expect(STYLES.find((s) => s.id === id)!.restyle, id).toBe(false)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("a restyle list is the video list without them, in catalog order", () => {
|
|
97
|
+
expect(stylesForRestyle(STYLES).map((s) => s.id)).toEqual(STYLES.filter((s) => !NOT_FOR_RESTYLE.includes(s.id)).map((s) => s.id))
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("video generation still offers them", () => {
|
|
101
|
+
for (const id of NOT_FOR_RESTYLE) expect(stylesForMedia(STYLES, "video").some((s) => s.id === id), id).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("a style that says nothing is offered for restyle", () => {
|
|
105
|
+
expect(styleOffersRestyle({})).toBe(true)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe("the style catalog", () => {
|
|
110
|
+
const catalog = () => getPickerCatalog("style")!
|
|
111
|
+
|
|
112
|
+
it("carries media on the video-only options only", () => {
|
|
113
|
+
for (const o of catalog().options!) {
|
|
114
|
+
expect(o.media, o.id).toEqual(VIDEO_ONLY.includes(o.id) ? ["video"] : undefined)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("the wire carries media at both detail levels", () => {
|
|
119
|
+
for (const detail of ["compact", "full"] as const) {
|
|
120
|
+
const options = projectPickerCatalog(catalog(), { detail }).options!
|
|
121
|
+
const felt = options.find((o) => o.id === "felt-puppet")!
|
|
122
|
+
expect(felt.media, detail).toEqual(["video"])
|
|
123
|
+
expect(options.find((o) => o.id === "anime")!.media, detail).toBeUndefined()
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("the catalog and the wire carry restyle: false on exactly those options, at both detail levels", () => {
|
|
128
|
+
for (const o of catalog().options!) expect(o.restyle, o.id).toBe(NOT_FOR_RESTYLE.includes(o.id) ? false : undefined)
|
|
129
|
+
for (const detail of ["compact", "full"] as const) {
|
|
130
|
+
const options = projectPickerCatalog(catalog(), { detail }).options!
|
|
131
|
+
expect(options.find((o) => o.id === "fantasy")!.restyle, detail).toBe(false)
|
|
132
|
+
expect(options.find((o) => o.id === "vaporwave")!.restyle, detail).toBeUndefined()
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("a catalog pack that removes a video-only style takes away its medium too", () => {
|
|
137
|
+
registerCatalogPack({ id: "t-deny", catalogId: "style", mode: "deny", denyIds: ["felt-puppet"] })
|
|
138
|
+
expect(getStyleMedium("felt-puppet")).toBeUndefined()
|
|
139
|
+
expect(getPickerCatalog("style")!.options!.some((o) => o.id === "felt-puppet")).toBe(false)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every style carries a VIDEO-only wording (`videoPromptHint`): what the medium
|
|
3
|
+
* is, then its concrete techniques (the pattern that styled every shot in video
|
|
4
|
+
* tests). Every video surface reads it through `getStyleVideoPromptHint`
|
|
5
|
+
* (falling back to `promptHint` for a pack style without one); every image
|
|
6
|
+
* surface keeps `promptHint`. These tests pin:
|
|
7
|
+
* - stills render exactly the image wording for every style;
|
|
8
|
+
* - video renders each style's own video wording, the three tested on video
|
|
9
|
+
* and the two chosen for 3D Render and Charcoal word for word, the
|
|
10
|
+
* photographic ones still photographic;
|
|
11
|
+
* - a deployment's catalog packs reach the video wording the same way they
|
|
12
|
+
* reach the image wording, and a rewrite of the image wording never ships
|
|
13
|
+
* beside the stock video wording.
|
|
14
|
+
*/
|
|
15
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
16
|
+
import {
|
|
17
|
+
STYLES,
|
|
18
|
+
getStyle,
|
|
19
|
+
getStylePromptHint,
|
|
20
|
+
getStyleVideoPromptHint,
|
|
21
|
+
getPickerCatalog,
|
|
22
|
+
projectPickerCatalog,
|
|
23
|
+
registerCatalogPack,
|
|
24
|
+
resetCatalogPacks,
|
|
25
|
+
renderDirectionHints,
|
|
26
|
+
composeVideoPromptText,
|
|
27
|
+
assembleImageInput,
|
|
28
|
+
getParameterPromptHint,
|
|
29
|
+
type PickerCatalogInput,
|
|
30
|
+
} from "../index.js"
|
|
31
|
+
|
|
32
|
+
afterEach(() => resetCatalogPacks())
|
|
33
|
+
|
|
34
|
+
const LOW_POLY_VIDEO =
|
|
35
|
+
"Stylised low poly 3D animation, computer-generated: every person, face, vehicle and room built from large flat-shaded triangles, visible facets on every surface, flat untextured colours, crisp shadow edges between facets."
|
|
36
|
+
/** The wordings tested on video (Seedance 2.5), word for word. */
|
|
37
|
+
const TESTED_ON_VIDEO: Readonly<Record<string, string>> = {
|
|
38
|
+
"felt-puppet": "Needle-felted stop-motion puppet animation, handmade: every person a felt puppet with soft fuzzy wool-felt skin, simple stitched features, small bead-like eyes and hair of felted wool strands, clothes sewn from real fabric with visible weave. The whole set is a handmade felt miniature: every car a fuzzy felt-covered model with felt seats, every wall, floor, curtain, table, chair and plant made of felt or wool fabric with visible fibres, the street and sky felt too; warm soft studio light on a tabletop miniature set, a slight stop-motion step to the motion.",
|
|
39
|
+
"knitted-yarn": "Knitted yarn stop-motion animation, handmade: every person a knitted doll with yarn skin in visible knit loops, hair of chunky yarn strands, clothes knitted in cable and rib stitch; every car, building, road, tree and sky knitted from wool yarn with visible stitches; soft warm studio light on a tabletop miniature set, a slight stop-motion step to the motion.",
|
|
40
|
+
"toy-figure-3d": "Stylised 3D toy animation, computer-generated: every person a rounded vinyl toy figure with smooth glossy plastic skin, a simplified oversized head, small dot eyes and moulded plastic hair, clothes as moulded painted plastic; every car, room and prop a chunky rounded toy model with soft bevelled edges; bright clean colours, soft studio lighting.",
|
|
41
|
+
"painterly-3d": "Painterly 3D animation, computer-generated with a hand-painted finish: every person a stylised 3D character with soft rounded features and expressive eyes, skin, hair and clothes textured with visible painted brush strokes; every room and landscape a 3D set painted like a storybook illustration; warm glowing light, rich soft colours.",
|
|
42
|
+
"painted-clay": "Sculpted clay animation with a painted finish, handmade: every person sculpted from matte clay with visible thumbprints and tool marks, painted in soft gouache colour; every car, room and prop sculpted and hand-painted the same way; warm painterly light, a slight stop-motion step to the motion.",
|
|
43
|
+
"pop-art": "Flat 2D cel-shaded comic illustration, hand-drawn: thick uniform black ink outlines, flat colour fills, Ben-Day dot shading, bold saturated primary colours, high contrast.",
|
|
44
|
+
claymation: "Stop-motion claymation, handmade: every person, vehicle and set sculpted from matte plasticine clay with visible fingerprints and tool marks, rounded simplified forms, miniature handcrafted diorama sets, soft warm studio lighting.",
|
|
45
|
+
anime: "2D Japanese anime, hand-drawn cel animation: clean thin ink line art, flat cel shading with one hard shadow tone, large expressive eyes, hair drawn as solid shapes, saturated colours, painted backgrounds.",
|
|
46
|
+
}
|
|
47
|
+
/** The 3D Render and Charcoal video wordings as chosen, word for word, beside their unchanged image wordings. */
|
|
48
|
+
const CHOSEN_FOR_VIDEO: Readonly<Record<string, { readonly video: string; readonly image: string }>> = {
|
|
49
|
+
"3d-render": {
|
|
50
|
+
video: "Stylised 3D render animation, computer-generated: every person, vehicle and room modelled in smooth matte CG surfaces, simplified clean forms, soft studio lighting, gentle ambient occlusion, bright clean colours.",
|
|
51
|
+
image: "3D rendered style, modern CGI aesthetic with clean shading, subtle ambient occlusion and crisp materials",
|
|
52
|
+
},
|
|
53
|
+
charcoal: {
|
|
54
|
+
video: "Rough 2D charcoal sketch, hand-drawn on white paper: every person and every face drawn in heavy black charcoal outlines and loose hatched strokes, white paper showing through, smudged edges, bold gestural lines.",
|
|
55
|
+
image: "rendered as a charcoal drawing on toothy paper, soft black and grey tonal masses with smudged blended midtones, sharp accented darks, expressive gestural strokes and the raw immediacy of a fine-art figure study",
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
/** The photographic styles: their video wording keeps them photographed, never drawn. */
|
|
59
|
+
const PHOTOGRAPHIC = ["photorealistic", "cinematic", "noir", "retro-vintage", "early-color-photo"]
|
|
60
|
+
const videoOf = (id: string) => STYLES.find((s) => s.id === id)!.videoPromptHint!
|
|
61
|
+
const LOW_POLY_IMAGE =
|
|
62
|
+
"low poly style, faceted geometric 3D aesthetic with visible flat triangular faces, simplified forms and crisp shadow edges between facets"
|
|
63
|
+
|
|
64
|
+
const styleNode = (style: string) => ({ id: "s1", type: "style", data: { style } })
|
|
65
|
+
|
|
66
|
+
/** The styles offered for video alone (`media: ["video"]`). */
|
|
67
|
+
const VIDEO_ONLY = ["felt-puppet", "knitted-yarn", "toy-figure-3d", "painterly-3d", "painted-clay"]
|
|
68
|
+
/**
|
|
69
|
+
* A video wording's length cap. A style offered for images too names its medium and its techniques in one short line;
|
|
70
|
+
* a video-only style also names what the whole set is made of (cars, rooms, props), and the felt line that felted
|
|
71
|
+
* the whole set of a live-action clip on Seedance 2.5 (people, car, street and curtains) is 562 characters.
|
|
72
|
+
*/
|
|
73
|
+
const capOf = (id: string) => (VIDEO_ONLY.includes(id) ? 600 : 240)
|
|
74
|
+
|
|
75
|
+
describe("the catalog", () => {
|
|
76
|
+
it("has 54 styles (49 for images and video, 5 for video alone), and every one carries its own video wording, different from its image wording", () => {
|
|
77
|
+
expect(STYLES).toHaveLength(54)
|
|
78
|
+
expect(STYLES.filter((s) => s.media?.length === 1 && s.media[0] === "video").map((s) => s.id)).toEqual(VIDEO_ONLY)
|
|
79
|
+
for (const s of STYLES) {
|
|
80
|
+
expect(s.videoPromptHint, s.id).toEqual(expect.any(String))
|
|
81
|
+
expect(s.videoPromptHint, s.id).not.toBe(s.promptHint)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("each video wording names the medium, then its techniques after a colon: one sentence, one line", () => {
|
|
86
|
+
for (const s of STYLES) {
|
|
87
|
+
const v = s.videoPromptHint!
|
|
88
|
+
expect(v, s.id).toMatch(/^[^:]+: \S[^\n]*\.$/)
|
|
89
|
+
expect(v.length, s.id).toBeLessThanOrEqual(capOf(s.id))
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("the wordings tested on video are word for word", () => {
|
|
94
|
+
for (const [id, wording] of Object.entries(TESTED_ON_VIDEO)) expect(getStyleVideoPromptHint(id), id).toBe(wording)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("the 3D Render and Charcoal video wordings are word for word, and their image wordings unchanged", () => {
|
|
98
|
+
for (const [id, { video, image }] of Object.entries(CHOSEN_FOR_VIDEO)) {
|
|
99
|
+
expect(getStyleVideoPromptHint(id), id).toBe(video)
|
|
100
|
+
expect(getStylePromptHint(id), id).toBe(image)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("the photographic styles stay photographed on video", () => {
|
|
105
|
+
for (const id of PHOTOGRAPHIC) expect(getStyleVideoPromptHint(id), id).toMatch(/live-action|photograph/i)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("Low Poly keeps its image wording and carries the video wording", () => {
|
|
109
|
+
expect(getStylePromptHint("low-poly")).toBe(LOW_POLY_IMAGE)
|
|
110
|
+
expect(getStyleVideoPromptHint("low-poly")).toBe(LOW_POLY_VIDEO)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("every style's video wording is its own, read through getStyleVideoPromptHint", () => {
|
|
114
|
+
for (const s of STYLES) expect(getStyleVideoPromptHint(s.id), s.id).toBe(s.videoPromptHint)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("an unknown or empty id has no wording on either surface", () => {
|
|
118
|
+
expect(getStyleVideoPromptHint("no-such-style")).toBe("")
|
|
119
|
+
expect(getStyleVideoPromptHint(undefined)).toBe("")
|
|
120
|
+
expect(getStyleVideoPromptHint("")).toBe("")
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe("image surfaces read only promptHint, for every style (a video-only one wired into an image included)", () => {
|
|
125
|
+
it("the direction fold on image", () => {
|
|
126
|
+
for (const s of STYLES) {
|
|
127
|
+
expect(renderDirectionHints({ style: s.id }, { surface: "image" }), s.id).toEqual([s.promptHint])
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("the assembled image prompt", () => {
|
|
132
|
+
for (const s of STYLES) {
|
|
133
|
+
const { prompt } = assembleImageInput({ userPrompt: "a man walks", provider: "flux-2-max", direction: { style: s.id } })
|
|
134
|
+
expect(prompt, s.id).toBe(`a man walks\n\n[style]:\n${s.promptHint}`)
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it("a Style node's own hint (no surface, and surface image)", () => {
|
|
139
|
+
for (const s of STYLES) {
|
|
140
|
+
expect(getParameterPromptHint(styleNode(s.id)), s.id).toBe(s.promptHint)
|
|
141
|
+
expect(getParameterPromptHint(styleNode(s.id), undefined, { surface: "image" }), s.id).toBe(s.promptHint)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe("video surfaces read the video wording", () => {
|
|
147
|
+
it("the direction fold on video: each style's video wording", () => {
|
|
148
|
+
for (const s of STYLES) expect(renderDirectionHints({ style: s.id }, { surface: "video" }), s.id).toEqual([videoOf(s.id)])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("the composed video prompt", () => {
|
|
152
|
+
for (const s of STYLES) {
|
|
153
|
+
expect(composeVideoPromptText("a man walks", { style: s.id }), s.id).toBe(`a man walks\n\n[style]:\n${videoOf(s.id)}`)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it("a Style node folded into a video consumer", () => {
|
|
158
|
+
for (const s of STYLES) {
|
|
159
|
+
expect(getParameterPromptHint(styleNode(s.id), undefined, { surface: "video" }), s.id).toBe(videoOf(s.id))
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("keeps the node's own pre/post text around the video wording", () => {
|
|
164
|
+
const node = { id: "s1", type: "style", data: { style: "low-poly", preText: "before", postText: "after" } }
|
|
165
|
+
// The video wording ends a sentence, so what follows it is joined with a space (joinHintFragments).
|
|
166
|
+
expect(getParameterPromptHint(node, undefined, { surface: "video" })).toBe(`before, ${LOW_POLY_VIDEO} after`)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it("compact mode injects the same term on both surfaces", () => {
|
|
170
|
+
expect(renderDirectionHints({ style: "low-poly" }, { surface: "video", mode: "compact" })).toEqual(["low poly"])
|
|
171
|
+
const node = { id: "s1", type: "style", data: { style: "low-poly", hintMode: "compact" } }
|
|
172
|
+
expect(getParameterPromptHint(node, undefined, { surface: "video" })).toBe("low poly")
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it("a Style node on a camera move's start state takes the video surface down with it", () => {
|
|
176
|
+
const nodes = [
|
|
177
|
+
{ id: "cm", type: "camera-motion", data: { cameraMotion: "dolly-in" } },
|
|
178
|
+
styleNode("low-poly"),
|
|
179
|
+
]
|
|
180
|
+
const edges = [{ id: "e1", source: "s1", target: "cm", targetHandle: "startState" }]
|
|
181
|
+
const video = getParameterPromptHint(nodes[0], { nodes, edges }, { surface: "video" })
|
|
182
|
+
const image = getParameterPromptHint(nodes[0], { nodes, edges })
|
|
183
|
+
expect(video).toContain(LOW_POLY_VIDEO)
|
|
184
|
+
expect(image).toContain(LOW_POLY_IMAGE)
|
|
185
|
+
expect(image).not.toContain(LOW_POLY_VIDEO)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe("catalog packs", () => {
|
|
190
|
+
const styleCatalog = () => getPickerCatalog("style")!
|
|
191
|
+
const replaceWith = (mapOption: (o: NonNullable<PickerCatalogInput["options"]>[number]) => object) => {
|
|
192
|
+
const base = styleCatalog()
|
|
193
|
+
registerCatalogPack({
|
|
194
|
+
id: "t-style-replace",
|
|
195
|
+
catalogId: "style",
|
|
196
|
+
mode: "replace",
|
|
197
|
+
catalog: { ...base, options: base.options!.map((o) => ({ ...o, ...mapOption(o) })) },
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
it("the composed catalog carries every style's video wording", () => {
|
|
202
|
+
const options = styleCatalog().options!
|
|
203
|
+
expect(options).toHaveLength(54)
|
|
204
|
+
for (const o of options) expect(o.videoPromptHint, o.id).toBe(videoOf(o.id))
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it("the wire projection does not carry it", () => {
|
|
208
|
+
const projected = projectPickerCatalog(styleCatalog(), { detail: "full" })
|
|
209
|
+
expect(JSON.stringify(projected)).not.toContain("videoPromptHint")
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("a deny pack: a denied style has no video wording either", () => {
|
|
213
|
+
registerCatalogPack({ id: "t-deny", catalogId: "style", mode: "deny", denyIds: ["low-poly"] })
|
|
214
|
+
expect(getStyleVideoPromptHint("low-poly")).toBe("")
|
|
215
|
+
expect(renderDirectionHints({ style: "low-poly" }, { surface: "video" })).toEqual([])
|
|
216
|
+
// A sibling the pack did not touch still resolves on both surfaces.
|
|
217
|
+
expect(getStyleVideoPromptHint("anime")).toBe(TESTED_ON_VIDEO.anime)
|
|
218
|
+
expect(getStylePromptHint("anime")).toBe(STYLES.find((s) => s.id === "anime")!.promptHint)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("a deny pack on ANOTHER style keeps Low Poly's video wording", () => {
|
|
222
|
+
registerCatalogPack({ id: "t-deny", catalogId: "style", mode: "deny", denyIds: ["anime"] })
|
|
223
|
+
expect(getStyleVideoPromptHint("low-poly")).toBe(LOW_POLY_VIDEO)
|
|
224
|
+
expect(getStylePromptHint("low-poly")).toBe(LOW_POLY_IMAGE)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it("the browser's copy of the server catalog (full detail, no video wording) keeps the stock video wording", () => {
|
|
228
|
+
// What `catalog-bootstrap` registers on a curated deployment: the server's
|
|
229
|
+
// composed catalog read back from `/v1/catalogs?detail=full`.
|
|
230
|
+
const wire = projectPickerCatalog(styleCatalog(), { detail: "full" }) as unknown as PickerCatalogInput
|
|
231
|
+
registerCatalogPack({ id: "wire:style", catalogId: "style", mode: "replace", catalog: wire })
|
|
232
|
+
expect(getStyleVideoPromptHint("low-poly")).toBe(LOW_POLY_VIDEO)
|
|
233
|
+
expect(getStylePromptHint("low-poly")).toBe(LOW_POLY_IMAGE)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it("a pack that rewrites the image wording drops the stock video wording (video follows the rewrite)", () => {
|
|
237
|
+
replaceWith((o) => (o.id === "low-poly" ? { promptHint: "curated low poly wording", videoPromptHint: undefined } : {}))
|
|
238
|
+
expect(getStylePromptHint("low-poly")).toBe("curated low poly wording")
|
|
239
|
+
expect(getStyleVideoPromptHint("low-poly")).toBe("curated low poly wording")
|
|
240
|
+
expect(getStyle("low-poly")?.videoPromptHint).toBeUndefined()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it("a pack may carry its own video wording", () => {
|
|
244
|
+
replaceWith((o) => (o.id === "low-poly" ? { promptHint: "curated still", videoPromptHint: "curated motion" } : {}))
|
|
245
|
+
expect(getStylePromptHint("low-poly")).toBe("curated still")
|
|
246
|
+
expect(getStyleVideoPromptHint("low-poly")).toBe("curated motion")
|
|
247
|
+
expect(renderDirectionHints({ style: "low-poly" }, { surface: "video" })).toEqual(["curated motion"])
|
|
248
|
+
expect(renderDirectionHints({ style: "low-poly" }, { surface: "image" })).toEqual(["curated still"])
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it("a pack may replace a stock style's video wording and keep its image wording", () => {
|
|
252
|
+
replaceWith((o) => (o.id === "anime" ? { videoPromptHint: "anime video wording" } : {}))
|
|
253
|
+
expect(getStylePromptHint("anime")).toBe(STYLES.find((s) => s.id === "anime")!.promptHint)
|
|
254
|
+
expect(getStyleVideoPromptHint("anime")).toBe("anime video wording")
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it("a pack-ADDED style resolves its own video wording, else its image wording", () => {
|
|
258
|
+
registerCatalogPack({
|
|
259
|
+
id: "t-extend",
|
|
260
|
+
catalogId: "style",
|
|
261
|
+
mode: "extend",
|
|
262
|
+
options: [
|
|
263
|
+
{ id: "claymation-x", label: "Clay X", promptHint: "clay still", videoPromptHint: "clay motion" },
|
|
264
|
+
{ id: "inkwash-x", label: "Ink X", promptHint: "ink still" },
|
|
265
|
+
],
|
|
266
|
+
})
|
|
267
|
+
expect(getStyleVideoPromptHint("claymation-x")).toBe("clay motion")
|
|
268
|
+
expect(getStylePromptHint("claymation-x")).toBe("clay still")
|
|
269
|
+
expect(getStyleVideoPromptHint("inkwash-x")).toBe("ink still")
|
|
270
|
+
expect(getParameterPromptHint(styleNode("claymation-x"), undefined, { surface: "video" })).toBe("clay motion")
|
|
271
|
+
expect(getParameterPromptHint(styleNode("claymation-x"))).toBe("clay still")
|
|
272
|
+
})
|
|
273
|
+
})
|
package/src/catalog-overlay.ts
CHANGED
|
@@ -26,6 +26,16 @@ import { catalogPacksVersion, hasCatalogPacksFor } from "./catalog-packs.js"
|
|
|
26
26
|
* carry (typed extras a specific builder reads) survive from the base.
|
|
27
27
|
* - pack-ADDED id (no base entry) → the composed option itself.
|
|
28
28
|
*
|
|
29
|
+
* THE VIDEO WORDING (`videoPromptHint`, styles only) pairs with the image
|
|
30
|
+
* wording it was written beside, so it cannot simply survive from the base
|
|
31
|
+
* like a typed extra: a pack that rewrites a style's `promptHint` would then
|
|
32
|
+
* still inject the stock video text. The composed option's own
|
|
33
|
+
* `videoPromptHint` wins when it carries one; otherwise the base's is kept
|
|
34
|
+
* only while the composed `promptHint` is the stock one (a deny / extend pack,
|
|
35
|
+
* or the browser's `replace` copy of the server's composed catalog, which
|
|
36
|
+
* carries `promptHint` but not the video wording); a rewritten `promptHint`
|
|
37
|
+
* drops it, so video falls back to the pack's own wording.
|
|
38
|
+
*
|
|
29
39
|
* LATE-BOUND RESOLVER, ON PURPOSE. The catalog modules cannot import
|
|
30
40
|
* `getPickerCatalog`: picker-catalogs.ts imports every one of them to build
|
|
31
41
|
* `PICKER_CATALOGS` at module scope, and a module-scope cycle is a TDZ error
|
|
@@ -91,7 +101,7 @@ export function overlayEntry<T extends { readonly id: string }>(
|
|
|
91
101
|
const opt = composedOptionIndex(catalogId).get(id)
|
|
92
102
|
if (!opt) return undefined
|
|
93
103
|
if (!base) return opt as unknown as T
|
|
94
|
-
|
|
104
|
+
const out: Record<string, unknown> = {
|
|
95
105
|
...base,
|
|
96
106
|
label: opt.label,
|
|
97
107
|
promptHint: opt.promptHint,
|
|
@@ -99,6 +109,10 @@ export function overlayEntry<T extends { readonly id: string }>(
|
|
|
99
109
|
...(opt.description !== undefined ? { description: opt.description } : {}),
|
|
100
110
|
...(opt.category !== undefined ? { category: opt.category } : {}),
|
|
101
111
|
}
|
|
112
|
+
const baseFields = base as { readonly promptHint?: unknown; readonly videoPromptHint?: unknown }
|
|
113
|
+
if (opt.videoPromptHint !== undefined) out.videoPromptHint = opt.videoPromptHint
|
|
114
|
+
else if (baseFields.videoPromptHint !== undefined && opt.promptHint !== baseFields.promptHint) delete out.videoPromptHint
|
|
115
|
+
return out as unknown as T
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
/**
|
|
@@ -56,7 +56,7 @@ import { getExposurePromptHint, getExposureTerm } from "./exposure-settings.js"
|
|
|
56
56
|
import { getColorLookPromptHint, getColorLookTerm } from "./color-look.js"
|
|
57
57
|
import { buildAtmosphereHints } from "./atmosphere.js"
|
|
58
58
|
import { buildPostProcessHints } from "./post-process-effects.js"
|
|
59
|
-
import { getStylePromptHint, getStyleTerm } from "./style.js"
|
|
59
|
+
import { getStylePromptHint, getStyleTerm, getStyleVideoPromptHint } from "./style.js"
|
|
60
60
|
import { buildMoodHints } from "./mood.js"
|
|
61
61
|
import { buildAestheticHints } from "./aesthetic.js"
|
|
62
62
|
import { getPhotoGenrePromptHint, getPhotoGenreTerm } from "./photo-genre.js"
|
|
@@ -125,8 +125,13 @@ export interface DirectionFieldSpec {
|
|
|
125
125
|
* Render selected ids in this catalog's own doctrine: per-id for independent
|
|
126
126
|
* catalogs, ONE blended clause for Mood / Aesthetic / Photographer, the
|
|
127
127
|
* catalog's own multi builder where it has one. `[]` when nothing resolves.
|
|
128
|
+
*
|
|
129
|
+
* `surface` is the stage being folded. Nearly every row ignores it (one
|
|
130
|
+
* wording serves stills and video); a row whose catalog authors a video-only
|
|
131
|
+
* wording (`style`, via `getStyleVideoPromptHint`) reads it. Absent reads as
|
|
132
|
+
* `"image"`.
|
|
128
133
|
*/
|
|
129
|
-
readonly render: (ids: readonly string[], mode: PickerHintMode) => string[]
|
|
134
|
+
readonly render: (ids: readonly string[], mode: PickerHintMode, surface?: "image" | "video") => string[]
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
// ── Render adapters (module-private) ────────────────────────────────────────
|
|
@@ -163,6 +168,16 @@ const viaStringBuilder =
|
|
|
163
168
|
return s.length > 0 ? [s] : []
|
|
164
169
|
}
|
|
165
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Per-id emission whose FULL wording depends on the surface: the image getter
|
|
173
|
+
* on a still, the video getter on video (which falls back to the image wording
|
|
174
|
+
* for an id that authors none). The compact term is the same on both.
|
|
175
|
+
*/
|
|
176
|
+
const perIdBySurface =
|
|
177
|
+
(image: (id: string) => string, video: (id: string) => string, compact: (id: string) => string) =>
|
|
178
|
+
(ids: readonly string[], mode: PickerHintMode, surface?: "image" | "video"): string[] =>
|
|
179
|
+
perId(surface === "video" ? video : image, compact)(ids, mode)
|
|
180
|
+
|
|
166
181
|
/** Mood's builder takes a RECORD and returns a list — and blends multi picks. */
|
|
167
182
|
const viaMood = (ids: readonly string[], mode: PickerHintMode): string[] =>
|
|
168
183
|
buildMoodHints({ mood: ids.length === 1 ? ids[0] : [...ids] }, mode).filter(
|
|
@@ -242,7 +257,7 @@ export const DIRECTION_FIELDS = [
|
|
|
242
257
|
{ key: "postProcess", surface: "image", family: "look", maxPicks: 2, render: viaListBuilder(buildPostProcessHints) },
|
|
243
258
|
|
|
244
259
|
// Style.
|
|
245
|
-
{ key: "style", surface: "both", family: "look", styleGroup: "film", maxPicks: 1, userRef: "moodboard", render:
|
|
260
|
+
{ key: "style", surface: "both", family: "look", styleGroup: "film", maxPicks: 1, userRef: "moodboard", render: perIdBySurface(getStylePromptHint, getStyleVideoPromptHint, getStyleTerm) },
|
|
246
261
|
{ key: "mood", surface: "both", family: "look", maxPicks: 2, render: viaMood },
|
|
247
262
|
{ key: "aesthetic", surface: "both", family: "look", maxPicks: 2, render: viaStringBuilder(buildAestheticHints) },
|
|
248
263
|
{ key: "photoGenre", surface: "image", family: "look", maxPicks: 1, render: perId(getPhotoGenrePromptHint, getPhotoGenreTerm) },
|
|
@@ -260,7 +275,9 @@ export const DIRECTION_FIELDS = [
|
|
|
260
275
|
{ key: "temporalFreeze", surface: "video", family: "motion", maxPicks: 1, render: temporal },
|
|
261
276
|
{ key: "temporalDirection", surface: "video", family: "motion", maxPicks: 1, render: temporal },
|
|
262
277
|
{ key: "temporalShutter", surface: "video", family: "motion", maxPicks: 1, render: temporal },
|
|
263
|
-
|
|
278
|
+
// Wrapped: `renderTransitionBases`' third parameter is a row's option values,
|
|
279
|
+
// not the surface `render` is handed.
|
|
280
|
+
{ key: "transition", surface: "video", family: "motion", maxPicks: 2, render: (ids, mode) => renderTransitionBases(ids, mode) },
|
|
264
281
|
{ key: "loopSubject", surface: "video", family: "motion", maxPicks: 1, render: perId(getLoopSubjectPromptHint, getLoopSubjectTerm) },
|
|
265
282
|
|
|
266
283
|
// ── LEGACY BLOCK — see the table doc above. Placed LAST, in today's exact
|
|
@@ -408,14 +425,15 @@ function renderRow(
|
|
|
408
425
|
spec: DirectionFieldSpec,
|
|
409
426
|
ids: readonly string[],
|
|
410
427
|
mode: PickerHintMode,
|
|
428
|
+
surface: "image" | "video",
|
|
411
429
|
refs: ReadonlyMap<string, string> | undefined,
|
|
412
430
|
): string[] {
|
|
413
431
|
if (spec.userRef === undefined || !ids.some((id) => parseUserDirectionRef(id) !== null)) {
|
|
414
|
-
return spec.render(ids, mode)
|
|
432
|
+
return spec.render(ids, mode, surface)
|
|
415
433
|
}
|
|
416
434
|
return ids.flatMap((id) => {
|
|
417
435
|
const ref = parseUserDirectionRef(id)
|
|
418
|
-
if (ref === null) return spec.render([id], mode)
|
|
436
|
+
if (ref === null) return spec.render([id], mode, surface)
|
|
419
437
|
const clause = ref.kind === spec.userRef ? refs?.get(id) : undefined
|
|
420
438
|
return clause ? [clause] : []
|
|
421
439
|
})
|
|
@@ -457,7 +475,7 @@ export function renderDirectionHintClauses(
|
|
|
457
475
|
spec.maxPicks,
|
|
458
476
|
)
|
|
459
477
|
if (ids.length === 0) continue
|
|
460
|
-
for (const hint of renderRow(spec, ids, modeForFamily(mode, spec.family), opts.refs)) {
|
|
478
|
+
for (const hint of renderRow(spec, ids, modeForFamily(mode, spec.family), opts.surface, opts.refs)) {
|
|
461
479
|
if (hint.length > 0 && !seen.has(hint)) {
|
|
462
480
|
seen.add(hint)
|
|
463
481
|
out.push({
|
|
@@ -26,7 +26,7 @@ import { getCameraFormatPromptHint, getCameraFormatTerm } from "./camera-format.
|
|
|
26
26
|
import { getColorLookPromptHint, getColorLookTerm } from "./color-look.js"
|
|
27
27
|
import { buildAtmosphereHints } from "./atmosphere.js"
|
|
28
28
|
import { buildActionFxHints } from "./action-fx.js"
|
|
29
|
-
import { getStylePromptHint, getStyleTerm } from "./style.js"
|
|
29
|
+
import { getStylePromptHint, getStyleTerm, getStyleVideoPromptHint } from "./style.js"
|
|
30
30
|
import { getSettingPromptHint, getSettingTerm } from "./setting.js"
|
|
31
31
|
import { getLoopSubjectPromptHint, getLoopSubjectTerm } from "./loop-subject.js"
|
|
32
32
|
import { buildPersonHints } from "./person.js"
|
|
@@ -179,12 +179,26 @@ const MOTION_INTENSITY_HINTS: Readonly<Record<string, { readonly full: string; r
|
|
|
179
179
|
* transition / camera-motion / character-fx timing and start-state/end-state
|
|
180
180
|
* clauses, multi-pick joining and multi-dimension composition are emitted the
|
|
181
181
|
* same way in both modes.
|
|
182
|
+
*
|
|
183
|
+
* SURFACE — `opts.surface: "video"` is for a caller folding the hint into ONE
|
|
184
|
+
* video consumer's prompt (the canvas's video nodes): a Style node then injects
|
|
185
|
+
* its video wording (`getStyleVideoPromptHint`). Absent (or `"image"`) is the
|
|
186
|
+
* output this function always returned — what a still, a `{Label}` reference
|
|
187
|
+
* and every preview of the node's own text read, since a node's own output
|
|
188
|
+
* text has no single consumer. Every other node type ignores it.
|
|
182
189
|
*/
|
|
183
190
|
export function getParameterPromptHint(
|
|
184
191
|
node: HintNodeLike | undefined,
|
|
185
192
|
ctx?: HintGraphContext,
|
|
193
|
+
opts?: ParameterHintOptions,
|
|
186
194
|
): string {
|
|
187
|
-
return resolveParameterHint(node, ctx)
|
|
195
|
+
return resolveParameterHint(node, ctx, undefined, opts?.surface === "video" ? "video" : "image")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** How `getParameterPromptHint` resolves a node. */
|
|
199
|
+
export interface ParameterHintOptions {
|
|
200
|
+
/** The stage the hint is folded into. Absent = `"image"`. */
|
|
201
|
+
readonly surface?: "image" | "video"
|
|
188
202
|
}
|
|
189
203
|
|
|
190
204
|
/**
|
|
@@ -198,6 +212,7 @@ function resolveParameterHint(
|
|
|
198
212
|
node: HintNodeLike | undefined,
|
|
199
213
|
ctx?: HintGraphContext,
|
|
200
214
|
inherited?: PickerHintMode,
|
|
215
|
+
surface: "image" | "video" = "image",
|
|
201
216
|
): string {
|
|
202
217
|
if (!node?.type) return ""
|
|
203
218
|
const data = (node.data ?? {}) as Record<string, unknown>
|
|
@@ -216,7 +231,7 @@ function resolveParameterHint(
|
|
|
216
231
|
// themselves parameter nodes (framing/tone/etc.) that don't need graph
|
|
217
232
|
// context, and avoiding recursion keeps the walk cycle-safe. The mode
|
|
218
233
|
// rides down so the composed clause stays at one level of detail.
|
|
219
|
-
const hint = resolveParameterHint(src, undefined, mode)
|
|
234
|
+
const hint = resolveParameterHint(src, undefined, mode, surface)
|
|
220
235
|
if (!hint) continue
|
|
221
236
|
if (edge.targetHandle === "startState") startHints.push(hint)
|
|
222
237
|
else if (edge.targetHandle === "endState") endHints.push(hint)
|
|
@@ -247,7 +262,7 @@ function resolveParameterHint(
|
|
|
247
262
|
if (edge.target !== node.id) continue
|
|
248
263
|
const src = ctx.nodes.find((n) => n.id === edge.source)
|
|
249
264
|
if (!src) continue
|
|
250
|
-
const hint = resolveParameterHint(src, undefined, mode) // no ctx — cycle-safe
|
|
265
|
+
const hint = resolveParameterHint(src, undefined, mode, surface) // no ctx — cycle-safe
|
|
251
266
|
if (!hint) continue
|
|
252
267
|
if (edge.targetHandle === "startState") startHints.push(hint)
|
|
253
268
|
else if (edge.targetHandle === "endState") endHints.push(hint)
|
|
@@ -301,7 +316,7 @@ function resolveParameterHint(
|
|
|
301
316
|
)
|
|
302
317
|
}
|
|
303
318
|
|
|
304
|
-
const base = resolveBaseHint(node.type, data, mode)
|
|
319
|
+
const base = resolveBaseHint(node.type, data, mode, surface)
|
|
305
320
|
if (base) return base
|
|
306
321
|
// Pack-extend fallback: a single-dim pack entry the per-catalog getter (which
|
|
307
322
|
// reads the frozen base array) can't resolve. Resolve it against the
|
|
@@ -313,7 +328,7 @@ function resolveParameterHint(
|
|
|
313
328
|
if (cat?.kind === "single" && cat.valueField) {
|
|
314
329
|
const id = typeof data[cat.valueField] === "string" ? (data[cat.valueField] as string) : ""
|
|
315
330
|
const opt = cat.options?.find((o) => o.id === id)
|
|
316
|
-
if (opt) return byMode(mode, opt.promptHint, resolveTerm(opt))
|
|
331
|
+
if (opt) return byMode(mode, surface === "video" ? (opt.videoPromptHint ?? opt.promptHint) : opt.promptHint, resolveTerm(opt))
|
|
317
332
|
}
|
|
318
333
|
return base
|
|
319
334
|
}
|
|
@@ -333,6 +348,7 @@ function resolveBaseHint(
|
|
|
333
348
|
type: string,
|
|
334
349
|
data: Record<string, unknown>,
|
|
335
350
|
mode: PickerHintMode = "full",
|
|
351
|
+
surface: "image" | "video" = "image",
|
|
336
352
|
): string {
|
|
337
353
|
switch (type) {
|
|
338
354
|
case "framing":
|
|
@@ -373,7 +389,7 @@ function resolveBaseHint(
|
|
|
373
389
|
case "action-fx":
|
|
374
390
|
return withCustomText(data, buildActionFxHints(data.actionFx, mode).join(", "))
|
|
375
391
|
case "style":
|
|
376
|
-
return withCustomText(data, byMode(mode, getStylePromptHint, getStyleTerm)(asStr(data.style)))
|
|
392
|
+
return withCustomText(data, byMode(mode, surface === "video" ? getStyleVideoPromptHint : getStylePromptHint, getStyleTerm)(asStr(data.style)))
|
|
377
393
|
case "setting":
|
|
378
394
|
return withCustomText(data, byMode(mode, getSettingPromptHint, getSettingTerm)(asStr(data.setting)))
|
|
379
395
|
case "loop-subject":
|
|
@@ -70,6 +70,11 @@ export const LOOK_PREVIEW_SETS = {
|
|
|
70
70
|
"mixed-media": "https://cdn.nodaro.ai/picker-renders/d36ab150-9cd6-47ac-bdd9-3b26990a4fa9.png",
|
|
71
71
|
"early-color-photo": "https://cdn.nodaro.ai/picker-renders/1bf61f3d-c174-466f-9092-5de72dc7abea.png",
|
|
72
72
|
manga: "https://cdn.nodaro.ai/picker-renders/3d6bbfd8-8c62-4769-b62e-11d2a7e43b4b.png",
|
|
73
|
+
"felt-puppet": "https://cdn.nodaro.ai/picker-renders/31ad3b39-d501-4b76-9c1c-2f87fc374333.png",
|
|
74
|
+
"knitted-yarn": "https://cdn.nodaro.ai/picker-renders/b80b6a96-2221-4c68-8e0c-4beaa5a6ce56.png",
|
|
75
|
+
"toy-figure-3d": "https://cdn.nodaro.ai/picker-renders/add81127-a005-4f9a-a002-780768238715.png",
|
|
76
|
+
"painterly-3d": "https://cdn.nodaro.ai/picker-renders/6aec1d80-be9f-4f2e-a1b7-fa38a61131d1.png",
|
|
77
|
+
"painted-clay": "https://cdn.nodaro.ai/picker-renders/daedee18-c034-4b98-80bb-7bef7e05c547.png",
|
|
73
78
|
},
|
|
74
79
|
"color-look": {
|
|
75
80
|
warm: "https://cdn.nodaro.ai/picker-renders/8511a1bc-3d3b-46a8-adc0-26fcc9e93a4b.png",
|