@nodaro/prompts 1.48.0 → 1.50.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.
Files changed (100) hide show
  1. package/dist/{catalog-packs-BDsrv8St.d.cts → catalog-packs-DeMcSVCR.d.cts} +1 -1
  2. package/dist/{catalog-packs-CAK42voB.d.ts → catalog-packs-DjqvicAR.d.ts} +1 -1
  3. package/dist/{chunk-RRHU6MD5.js → chunk-34JK2G34.js} +4 -4
  4. package/dist/{chunk-RRHU6MD5.js.map → chunk-34JK2G34.js.map} +1 -1
  5. package/dist/{chunk-LD47QOKS.js → chunk-3XSB5YDY.js} +92 -53
  6. package/dist/chunk-3XSB5YDY.js.map +1 -0
  7. package/dist/{chunk-XZXUU3YH.js → chunk-5XPELUES.js} +10 -5
  8. package/dist/chunk-5XPELUES.js.map +1 -0
  9. package/dist/{chunk-XBRW75LM.js → chunk-BWDQEAX2.js} +4 -4
  10. package/dist/{chunk-XBRW75LM.js.map → chunk-BWDQEAX2.js.map} +1 -1
  11. package/dist/{chunk-JHRVNWBS.js → chunk-C5LP7DAQ.js} +6 -6
  12. package/dist/{chunk-JHRVNWBS.js.map → chunk-C5LP7DAQ.js.map} +1 -1
  13. package/dist/{chunk-BTV2I7EN.js → chunk-DB6A5I6V.js} +3 -3
  14. package/dist/{chunk-BTV2I7EN.js.map → chunk-DB6A5I6V.js.map} +1 -1
  15. package/dist/{chunk-GZVCBSFG.js → chunk-FEOVUTFC.js} +10 -7
  16. package/dist/{chunk-GZVCBSFG.js.map → chunk-FEOVUTFC.js.map} +1 -1
  17. package/dist/{chunk-UNR34GYU.cjs → chunk-FFPFBNXH.cjs} +10 -5
  18. package/dist/chunk-FFPFBNXH.cjs.map +1 -0
  19. package/dist/chunk-GZ5SL2BI.js +30 -0
  20. package/dist/chunk-GZ5SL2BI.js.map +1 -0
  21. package/dist/{chunk-IMQB7GIK.cjs → chunk-HR4TFBU2.cjs} +35 -35
  22. package/dist/{chunk-IMQB7GIK.cjs.map → chunk-HR4TFBU2.cjs.map} +1 -1
  23. package/dist/{chunk-Z755PH3K.cjs → chunk-IADAATZK.cjs} +49 -49
  24. package/dist/{chunk-Z755PH3K.cjs.map → chunk-IADAATZK.cjs.map} +1 -1
  25. package/dist/{chunk-CMEUYFBZ.js → chunk-IXBTD2LW.js} +4 -4
  26. package/dist/{chunk-CMEUYFBZ.js.map → chunk-IXBTD2LW.js.map} +1 -1
  27. package/dist/{chunk-JV2WSMAA.cjs → chunk-JGEBO6L6.cjs} +8 -8
  28. package/dist/{chunk-JV2WSMAA.cjs.map → chunk-JGEBO6L6.cjs.map} +1 -1
  29. package/dist/{chunk-KNAKOSKV.cjs → chunk-KYKCHOOS.cjs} +13 -13
  30. package/dist/{chunk-KNAKOSKV.cjs.map → chunk-KYKCHOOS.cjs.map} +1 -1
  31. package/dist/{chunk-XGNUOU4F.cjs → chunk-MJIKM4YZ.cjs} +130 -127
  32. package/dist/{chunk-XGNUOU4F.cjs.map → chunk-MJIKM4YZ.cjs.map} +1 -1
  33. package/dist/{chunk-MGK6SFTV.cjs → chunk-RVBO63D5.cjs} +4 -4
  34. package/dist/{chunk-MGK6SFTV.cjs.map → chunk-RVBO63D5.cjs.map} +1 -1
  35. package/dist/{chunk-PJFX3MFI.cjs → chunk-TYJCCODP.cjs} +18 -18
  36. package/dist/{chunk-PJFX3MFI.cjs.map → chunk-TYJCCODP.cjs.map} +1 -1
  37. package/dist/{chunk-YXJSPQC6.js → chunk-VJYKE6AY.js} +3 -3
  38. package/dist/{chunk-YXJSPQC6.js.map → chunk-VJYKE6AY.js.map} +1 -1
  39. package/dist/{chunk-MYVJVOPB.cjs → chunk-VYCTRGJ7.cjs} +39 -39
  40. package/dist/{chunk-MYVJVOPB.cjs.map → chunk-VYCTRGJ7.cjs.map} +1 -1
  41. package/dist/{chunk-54BYDWAM.cjs → chunk-WLN2MJ7F.cjs} +102 -55
  42. package/dist/chunk-WLN2MJ7F.cjs.map +1 -0
  43. package/dist/chunk-XBY53RF5.cjs +33 -0
  44. package/dist/chunk-XBY53RF5.cjs.map +1 -0
  45. package/dist/{chunk-36XMRVY4.js → chunk-ZZ6UOK2P.js} +4 -4
  46. package/dist/{chunk-36XMRVY4.js.map → chunk-ZZ6UOK2P.js.map} +1 -1
  47. package/dist/entries/assembly.cjs +29 -29
  48. package/dist/entries/assembly.js +6 -6
  49. package/dist/entries/catalog-packs.d.cts +4 -4
  50. package/dist/entries/catalog-packs.d.ts +4 -4
  51. package/dist/entries/person.d.cts +3 -3
  52. package/dist/entries/person.d.ts +3 -3
  53. package/dist/entries/picker-art.cjs +28 -20
  54. package/dist/entries/picker-art.d.cts +18 -2
  55. package/dist/entries/picker-art.d.ts +18 -2
  56. package/dist/entries/picker-art.js +2 -2
  57. package/dist/entries/picker-catalogs.cjs +57 -57
  58. package/dist/entries/picker-catalogs.d.cts +5 -3
  59. package/dist/entries/picker-catalogs.d.ts +5 -3
  60. package/dist/entries/picker-catalogs.js +5 -5
  61. package/dist/entries/pickers.cjs +407 -375
  62. package/dist/entries/pickers.d.cts +59 -3
  63. package/dist/entries/pickers.d.ts +59 -3
  64. package/dist/entries/pickers.js +3 -3
  65. package/dist/entries/presets.cjs +35 -35
  66. package/dist/entries/presets.js +1 -1
  67. package/dist/entries/prompt-builder.cjs +76 -76
  68. package/dist/entries/prompt-builder.js +5 -5
  69. package/dist/{images-DrbQB9Yh.d.cts → images-ChAOLY5V.d.cts} +5 -4
  70. package/dist/{images-DrbQB9Yh.d.ts → images-ChAOLY5V.d.ts} +5 -4
  71. package/dist/index.cjs +621 -581
  72. package/dist/index.d.cts +5 -5
  73. package/dist/index.d.ts +5 -5
  74. package/dist/index.js +11 -11
  75. package/dist/{picker-catalogs-BiRCs_5G.d.cts → picker-catalogs-DzLCu6qQ.d.cts} +10 -1
  76. package/dist/{picker-catalogs-CX8pHi8q.d.ts → picker-catalogs-SDSWrufc.d.ts} +10 -1
  77. package/package.json +2 -2
  78. package/src/__tests__/fixtures/root-exports.baseline.json +5 -0
  79. package/src/__tests__/picker-art-images.test.ts +39 -0
  80. package/src/__tests__/style-display-name.test.ts +153 -0
  81. package/src/__tests__/style-media.test.ts +141 -0
  82. package/src/__tests__/style-video-wording.test.ts +22 -7
  83. package/src/factory-presets/generate-image.ts +1 -1
  84. package/src/factory-presets/generate-video.ts +1 -1
  85. package/src/picker-analyzer-registry.ts +9 -2
  86. package/src/picker-art/images.ts +6 -5
  87. package/src/picker-art/index.ts +1 -0
  88. package/src/picker-art/look-preview-sets.ts +5 -0
  89. package/src/picker-art/restyle-previews.ts +47 -0
  90. package/src/picker-catalogs.ts +19 -0
  91. package/src/setting.ts +1 -1
  92. package/src/style.ts +140 -50
  93. package/dist/chunk-54BYDWAM.cjs.map +0 -1
  94. package/dist/chunk-HOMR5XFP.js +0 -3
  95. package/dist/chunk-HOMR5XFP.js.map +0 -1
  96. package/dist/chunk-LD47QOKS.js.map +0 -1
  97. package/dist/chunk-T4OJFIJK.cjs +0 -4
  98. package/dist/chunk-T4OJFIJK.cjs.map +0 -1
  99. package/dist/chunk-UNR34GYU.cjs.map +0 -1
  100. package/dist/chunk-XZXUU3YH.js.map +0 -1
@@ -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
+ })
@@ -33,8 +33,13 @@ afterEach(() => resetCatalogPacks())
33
33
 
34
34
  const LOW_POLY_VIDEO =
35
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 three wordings tested on video (Seedance 2.5), word for word. */
36
+ /** The wordings tested on video (Seedance 2.5), word for word. */
37
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.",
38
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.",
39
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.",
40
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.",
@@ -58,9 +63,19 @@ const LOW_POLY_IMAGE =
58
63
 
59
64
  const styleNode = (style: string) => ({ id: "s1", type: "style", data: { style } })
60
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
+
61
75
  describe("the catalog", () => {
62
- it("has 49 styles, and every one carries its own video wording, different from its image wording", () => {
63
- expect(STYLES).toHaveLength(49)
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)
64
79
  for (const s of STYLES) {
65
80
  expect(s.videoPromptHint, s.id).toEqual(expect.any(String))
66
81
  expect(s.videoPromptHint, s.id).not.toBe(s.promptHint)
@@ -71,11 +86,11 @@ describe("the catalog", () => {
71
86
  for (const s of STYLES) {
72
87
  const v = s.videoPromptHint!
73
88
  expect(v, s.id).toMatch(/^[^:]+: \S[^\n]*\.$/)
74
- expect(v.length, s.id).toBeLessThanOrEqual(240)
89
+ expect(v.length, s.id).toBeLessThanOrEqual(capOf(s.id))
75
90
  }
76
91
  })
77
92
 
78
- it("the three wordings tested on video are word for word", () => {
93
+ it("the wordings tested on video are word for word", () => {
79
94
  for (const [id, wording] of Object.entries(TESTED_ON_VIDEO)) expect(getStyleVideoPromptHint(id), id).toBe(wording)
80
95
  })
81
96
 
@@ -106,7 +121,7 @@ describe("the catalog", () => {
106
121
  })
107
122
  })
108
123
 
109
- describe("image surfaces read only promptHint, for all 49 styles", () => {
124
+ describe("image surfaces read only promptHint, for every style (a video-only one wired into an image included)", () => {
110
125
  it("the direction fold on image", () => {
111
126
  for (const s of STYLES) {
112
127
  expect(renderDirectionHints({ style: s.id }, { surface: "image" }), s.id).toEqual([s.promptHint])
@@ -185,7 +200,7 @@ describe("catalog packs", () => {
185
200
 
186
201
  it("the composed catalog carries every style's video wording", () => {
187
202
  const options = styleCatalog().options!
188
- expect(options).toHaveLength(49)
203
+ expect(options).toHaveLength(54)
189
204
  for (const o of options) expect(o.videoPromptHint, o.id).toBe(videoOf(o.id))
190
205
  })
191
206
 
@@ -1117,7 +1117,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
1117
1117
  },
1118
1118
  {
1119
1119
  id: "generate-image/pixar-3d",
1120
- name: "3D Animated (Pixar-style)",
1120
+ name: "3D Animated Film",
1121
1121
  description: "Polished CG character look.",
1122
1122
  group: "Illustration & Art Styles",
1123
1123
  data: {
@@ -591,7 +591,7 @@ export const GENERATE_VIDEO_PRESETS: readonly FactoryPreset[] = [
591
591
  {
592
592
  id: "generate-video/cartoon-3d",
593
593
  name: "3D Cartoon",
594
- description: "Playful Pixar-like motion.",
594
+ description: "Playful animated-film motion.",
595
595
  group: "Animation & Style",
596
596
  data: {
597
597
  provider: "kling-3.0",
@@ -50,11 +50,18 @@ import { VOICE_PACES, VOICE_EMOTIONS, VOICE_ARCHETYPES } from "./voice-delivery.
50
50
  interface AnalyzerEntry {
51
51
  readonly id: string
52
52
  readonly label: string
53
+ /** What the analyzers' list calls this entry where it differs from `label` (`Style.promptLabel`). */
54
+ readonly promptLabel?: string
53
55
  readonly description: string
54
56
  readonly dimension?: string
55
57
  readonly category?: string
56
58
  }
57
59
 
60
+ /** An entry's row in the system-prompt legend: a list only models read. */
61
+ function legendRow(e: AnalyzerEntry): { id: string; label: string; description: string } {
62
+ return { id: e.id, label: e.promptLabel ?? e.label, description: e.description }
63
+ }
64
+
58
65
  export type PickerApplyMode = "override" | "overwrite-detected" | "fill-empty"
59
66
  type ApplyCleanup = (patch: Record<string, unknown>, mode: PickerApplyMode) => void
60
67
 
@@ -362,7 +369,7 @@ export function buildPickerAnalyzerSpec(pickerType: PickerType): PickerAnalyzerS
362
369
  label: d.label,
363
370
  limit: 1,
364
371
  entryIds: d.entries.map((e) => e.id),
365
- legend: d.entries.map((e) => ({ id: e.id, label: e.label, description: e.description })),
372
+ legend: d.entries.map(legendRow),
366
373
  },
367
374
  ],
368
375
  }
@@ -379,7 +386,7 @@ export function buildPickerAnalyzerSpec(pickerType: PickerType): PickerAnalyzerS
379
386
  label: d.labels[key] ?? key,
380
387
  limit: d.limitFn(key),
381
388
  entryIds: entries.map((e) => e.id),
382
- legend: entries.map((e) => ({ id: e.id, label: e.label, description: e.description })),
389
+ legend: entries.map(legendRow),
383
390
  }
384
391
  })
385
392
  return { pickerType, toolName: d.toolName, dimensions, cleanup: d.cleanup }
@@ -41,11 +41,12 @@ function originOf(baseUrl: string): string | undefined {
41
41
 
42
42
  /**
43
43
  * A still, 480px-wide picture of a rendered look preview: the image resized by
44
- * the CDN, or — for a clip (camera motion) — a frame taken 1s in. The same
45
- * transforms the editor's tiles use. A URL off the transforming CDN is kept
46
- * as is for an image, and dropped for a clip (no still to offer).
44
+ * the CDN, or — for a clip (camera motion) — a frame taken 1s in (or at
45
+ * `clipSeconds`). The same transforms the editor's tiles use. A URL off the
46
+ * transforming CDN is kept as is for an image, and dropped for a clip (no still
47
+ * to offer).
47
48
  */
48
- export function lookPreviewStillUrl(url: string, width = STILL_WIDTH): string | undefined {
49
+ export function lookPreviewStillUrl(url: string, width = STILL_WIDTH, clipSeconds = 1): string | undefined {
49
50
  let parsed: URL
50
51
  try {
51
52
  parsed = new URL(url)
@@ -55,7 +56,7 @@ export function lookPreviewStillUrl(url: string, width = STILL_WIDTH): string |
55
56
  const clip = /\.(mp4|webm|mov)$/i.test(parsed.pathname)
56
57
  if (!TRANSFORM_HOSTS.has(parsed.hostname)) return clip ? undefined : url
57
58
  return clip
58
- ? `${parsed.origin}/cdn-cgi/media/mode=frame,time=1s,width=${width}${parsed.pathname}`
59
+ ? `${parsed.origin}/cdn-cgi/media/mode=frame,time=${clipSeconds}s,width=${width}${parsed.pathname}`
59
60
  : `${parsed.origin}/cdn-cgi/image/width=${width},format=auto,quality=80${parsed.pathname}`
60
61
  }
61
62
 
@@ -9,6 +9,7 @@ export { SOUND_ART } from "./sound-art-map.js"
9
9
  export { SOUND_ART_FILES } from "./sound-art-files.generated.js"
10
10
  export { CHARACTER_ART_FILES } from "./character-art-files.generated.js"
11
11
  export { LOOK_PREVIEW_SETS, type LookPreviewSet, type LookPreviewSets } from "./look-preview-sets.js"
12
+ export { RESTYLE_PREVIEWS, restylePreviewStillUrl, type RestylePreview } from "./restyle-previews.js"
12
13
  export { SOUND_RENDER_SETS } from "./sound-render-sets.js"
13
14
  export * from "./paths.js"
14
15
  export * from "./images.js"
@@ -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",
@@ -0,0 +1,47 @@
1
+ // Restyle previews: for each art style, a short clip of real footage wiping
2
+ // into that style, for a picker that restyles a video. Keyed by the `style`
3
+ // catalog's option id. An entry exists only for an approved clip: a style
4
+ // without one has no preview here.
5
+ //
6
+ // Each approved style keeps the URLs of its source shot and its generated clip
7
+ // (no wipe) in a comment beside its entry, so the clip can be composed again if
8
+ // the switch animation changes.
9
+ //
10
+ // Separate from LOOK_PREVIEW_SETS.style, whose stills every other art style
11
+ // picker keeps showing. Like every render, a clip lives under picker-renders/
12
+ // on the CDN, a key no user owns; a test holds this.
13
+ import { lookPreviewStillUrl } from "./images.js"
14
+
15
+ /** One style's preview clip. */
16
+ export interface RestylePreview {
17
+ readonly url: string
18
+ /** The moment of the clip its still is cut from, for a clip whose usual moment frames it badly. Absent: the usual moment (`lookPreviewStillUrl`). */
19
+ readonly stillAtSeconds?: number
20
+ }
21
+
22
+ /** Art style id → its restyle preview clip. */
23
+ export const RESTYLE_PREVIEWS: Readonly<Record<string, RestylePreview>> = {
24
+ // Source shot: https://cdn.nodaro.ai/picker-renders/4054a0e1-af60-47f9-86bd-a78ab93f3800.mp4
25
+ // Generated, no wipe: https://cdn.nodaro.ai/picker-renders/9dc6b337-58b4-4a63-889f-70561a8f1f00.mp4
26
+ anime: { url: "https://cdn.nodaro.ai/picker-renders/7fbad6e4-023f-4282-ac3d-d186f2cbb37b.mp4", stillAtSeconds: 7 },
27
+ // Source shot: https://cdn.nodaro.ai/picker-renders/4ffff922-e807-42cc-a2a0-c66c3a8d492c.mp4
28
+ // Generated, no wipe: https://cdn.nodaro.ai/picker-renders/fca5d96d-ee9c-430e-ba82-2795c0adb8d6.mp4
29
+ claymation: { url: "https://cdn.nodaro.ai/picker-renders/2916a2f7-114c-4c7e-9be0-5bf229d76133.mp4" },
30
+ // Source shot: https://cdn.nodaro.ai/picker-renders/41faaa51-5e77-4097-aa9b-bef6738328b7.mp4
31
+ // Generated, no wipe: https://cdn.nodaro.ai/picker-renders/5a28da72-f3ee-49a8-b732-f73e79741b62.mp4
32
+ "comic-book": { url: "https://cdn.nodaro.ai/picker-renders/7a2cd17c-b02f-4ae4-9723-ef48d0eda52a.mp4" },
33
+ // Source shot: https://cdn.nodaro.ai/picker-renders/4a70c22d-04f0-4b5a-90d4-e9dd7ddb01c4.mp4
34
+ // Generated, no wipe: https://cdn.nodaro.ai/picker-renders/5ab33529-ef2a-42bc-a625-d6b1abfeef1c.mp4
35
+ "felt-puppet": { url: "https://cdn.nodaro.ai/picker-renders/3aa3c81b-ba19-4776-becc-e0c47c700699.mp4" },
36
+ // Source shot: https://cdn.nodaro.ai/picker-renders/d9e12a4a-677e-4e36-8f44-8503f14dd6b8.mp4
37
+ // Generated, no wipe: https://cdn.nodaro.ai/picker-renders/d4dad451-802a-4795-9cc3-71ff3ed70c69.mp4
38
+ "pixar-3d": { url: "https://cdn.nodaro.ai/picker-renders/c5ea2111-3283-4f80-ab05-8ab4450946ce.mp4" },
39
+ // Source shot: https://cdn.nodaro.ai/picker-renders/789f4330-660b-42bd-8d11-6e638e8c7c88.mp4
40
+ // Generated, no wipe: https://cdn.nodaro.ai/picker-renders/10471107-b26a-42e9-a3a5-068a42436d3c.mp4
41
+ watercolor: { url: "https://cdn.nodaro.ai/picker-renders/61a26588-d09a-4959-b677-ee8b32e3281a.mp4" },
42
+ }
43
+
44
+ /** A preview clip's still, a frame the CDN cuts from it at the clip's own moment. */
45
+ export function restylePreviewStillUrl(preview: RestylePreview, width?: number): string | undefined {
46
+ return lookPreviewStillUrl(preview.url, width, preview.stillAtSeconds)
47
+ }
@@ -140,6 +140,15 @@ export interface PickerOption {
140
140
  * does not carry it.
141
141
  */
142
142
  readonly videoPromptHint?: string
143
+ /**
144
+ * Style only: the outputs the style is offered for. Present only on a style
145
+ * offered for one of them (`["video"]`: a look that only works in motion);
146
+ * absent means both. Carried to the wire (`projectPickerCatalog`). An image style list leaves out an option without
147
+ * "image".
148
+ */
149
+ readonly media?: ReadonlyArray<"image" | "video">
150
+ /** Style only: `false` on a style restyle lists leave out (`Style.restyle`); absent means offered. Carried to the wire. */
151
+ readonly restyle?: false
143
152
  /**
144
153
  * Transitions only: present (`true`) on a row whose mechanism is a CUT, so
145
154
  * it takes no duration and no intensity — see `Transition.instant` /
@@ -231,6 +240,10 @@ interface BaseCatalogEntry {
231
240
  readonly term?: string
232
241
  /** Style: see `Style.videoPromptHint`. Propagated verbatim into the flattened option. */
233
242
  readonly videoPromptHint?: string
243
+ /** Style: see `Style.media`. Propagated verbatim into the flattened option. */
244
+ readonly media?: ReadonlyArray<"image" | "video">
245
+ /** Style: see `Style.restyle`. Propagated verbatim into the flattened option. */
246
+ readonly restyle?: false
234
247
  /** W1-a: see Person.adultOnly. Propagated verbatim into the flattened option. */
235
248
  readonly adultOnly?: true
236
249
  /** Transitions: see `Transition.instant`. Propagated into the flattened option as `instant: true`. */
@@ -278,6 +291,8 @@ function toOptions<T extends BaseCatalogEntry>(
278
291
  }
279
292
  if (e.description) opt.description = e.description
280
293
  if (e.videoPromptHint) opt.videoPromptHint = e.videoPromptHint
294
+ if (e.media) opt.media = e.media
295
+ if (e.restyle === false) opt.restyle = false
281
296
  if (categoryField) opt.category = e[categoryField] as unknown as string
282
297
  if (e.adultOnly) opt.adultOnly = true
283
298
  if (e.instant) opt.instant = true
@@ -1077,6 +1092,10 @@ function projectOptionFields(o: PickerOption, detail: PickerCatalogDetail, image
1077
1092
  // present only when the option has a picture (a srcset only for a CDN render).
1078
1093
  const extras = {
1079
1094
  ...(o.motion ? { motion: o.motion } : {}),
1095
+ // A style's media rides at both detail levels: a thin client filters its image style list by it.
1096
+ ...(o.media ? { media: [...o.media] } : {}),
1097
+ // A style restyle lists leave out says so at both detail levels too.
1098
+ ...(o.restyle === false ? { restyle: false as const } : {}),
1080
1099
  ...(image ? { imageUrl: image.url } : {}),
1081
1100
  ...(image?.srcSet ? { imageSrcSet: image.srcSet } : {}),
1082
1101
  }
package/src/setting.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * shop, forest clearing, cyberpunk alley, cathedral, etc.). Orthogonal to the
6
6
  * other cinematography dimensions:
7
7
  *
8
- * - Style = artistic medium (Oil Painting, Pixar 3D, Photorealistic)
8
+ * - Style = artistic medium (Oil Painting, 3D Animated Film, Photorealistic)
9
9
  * - Atmosphere = what's in the air (Fog, Rain, God rays, Dust)
10
10
  * - Lighting = direction / quality of light
11
11
  * - Setting = *where* (this file)