@nodaro/prompts 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +576 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +396 -2
- package/dist/index.d.ts +396 -2
- package/dist/index.js +573 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/doctrine-roster-completeness.test.ts +59 -0
- package/src/__tests__/picker-analyzer-registry.test.ts +11 -1
- package/src/__tests__/provider-prompt-doctrine.test.ts +5 -3
- package/src/index.ts +1 -0
- package/src/picker-analyzer-registry.ts +190 -0
- package/src/picker-wiring.ts +336 -0
- package/src/provider-prompt-doctrine.ts +227 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodaro/prompts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Nodaro's prompt-engineering layer — person/picker catalogs with prompt hints, identity-lock clauses, entity prompt builders, brand presets, and prompt/reference assembly shared by the Nodaro platform and SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { MODEL_CATALOG } from "@nodaro/shared"
|
|
3
|
+
import { getPromptDoctrine } from "../provider-prompt-doctrine.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The badge's truthfulness guarantee (Cine build-brief §7 — never overclaim):
|
|
7
|
+
* every video GENERATION model is either doctrine-covered or DELIBERATELY
|
|
8
|
+
* generic. A new video model that is neither fails this test — forcing the
|
|
9
|
+
* author to write a sourced doctrine or consciously add it to the generic
|
|
10
|
+
* set with a reason.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Models deliberately left WITHOUT a doctrine, with the reason. */
|
|
14
|
+
const DELIBERATELY_GENERIC: ReadonlyMap<string, string> = new Map([
|
|
15
|
+
// Older ByteDance engine (pre-Seedance-2 surface: single image, no refs/audio
|
|
16
|
+
// levers) — mapping the Seedance family doctrine would overclaim.
|
|
17
|
+
["bytedance-lite", "older engine, different surface"],
|
|
18
|
+
["bytedance-pro", "older engine, different surface"],
|
|
19
|
+
["bytedance-pro-fast", "older engine, different surface"],
|
|
20
|
+
// Hailuo pre-H3 tiers: single-image i2v without the multimodal reference
|
|
21
|
+
// surface the H3 doctrine teaches.
|
|
22
|
+
["hailuo-2.3", "pre-H3 tier without the multimodal surface"],
|
|
23
|
+
["hailuo-2.3-pro", "pre-H3 tier without the multimodal surface"],
|
|
24
|
+
["hailuo-standard", "pre-H3 tier without the multimodal surface"],
|
|
25
|
+
// Legacy/simple tiers with no vendor guidance beyond the platform default.
|
|
26
|
+
["minimax", "legacy 5s tier, no vendor guide"],
|
|
27
|
+
["seedance", "legacy Seedance 1.x, superseded"],
|
|
28
|
+
["ltx-2.3-fast", "no vendor prompt guide published"],
|
|
29
|
+
["sora2", "roster utility — no first-party guide via KIE"],
|
|
30
|
+
["sora2-pro", "roster utility — no first-party guide via KIE"],
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
/** Utility modes that are driven by inputs, not prose prompting — doctrine
|
|
34
|
+
* coverage isn't meaningful for them (spec excludes them from the roster). */
|
|
35
|
+
const UTILITY_ONLY_MODES = new Set(["extend", "motion-transfer", "lip-sync", "video-upscale", "v2v", "video-analysis", "video-audit"])
|
|
36
|
+
|
|
37
|
+
describe("doctrine roster completeness", () => {
|
|
38
|
+
const videoGenerationIds = Object.values(MODEL_CATALOG)
|
|
39
|
+
.filter((m) => m.kind === "video")
|
|
40
|
+
.filter((m) => m.modes?.some((mode) => (mode === "t2v" || mode === "i2v") && !UTILITY_ONLY_MODES.has(mode)))
|
|
41
|
+
.map((m) => m.id)
|
|
42
|
+
|
|
43
|
+
it("covers every video generation model — or lists it as deliberately generic", () => {
|
|
44
|
+
const uncovered = videoGenerationIds.filter(
|
|
45
|
+
(id) => getPromptDoctrine(id) === undefined && !DELIBERATELY_GENERIC.has(id),
|
|
46
|
+
)
|
|
47
|
+
expect(
|
|
48
|
+
uncovered,
|
|
49
|
+
`New video model(s) with neither a doctrine nor a deliberate-generic entry: ${uncovered.join(", ")}. ` +
|
|
50
|
+
"Write a sourced doctrine or add to DELIBERATELY_GENERIC with a reason.",
|
|
51
|
+
).toEqual([])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("the deliberate-generic set stays honest (no entry that is actually covered)", () => {
|
|
55
|
+
for (const id of DELIBERATELY_GENERIC.keys()) {
|
|
56
|
+
expect(getPromptDoctrine(id), `${id} is covered — remove it from DELIBERATELY_GENERIC`).toBeUndefined()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -77,7 +77,17 @@ describe("registry membership (Task 2: person only)", () => {
|
|
|
77
77
|
describe("registry invariants (all analyzable pickers)", () => {
|
|
78
78
|
it("registers the batch", () => {
|
|
79
79
|
expect(new Set(PICKER_TYPES)).toEqual(
|
|
80
|
-
new Set([
|
|
80
|
+
new Set([
|
|
81
|
+
// multi-dim (discriminated)
|
|
82
|
+
"person", "styling", "framing", "lighting", "temporal", "exposure-settings",
|
|
83
|
+
"music-genre", "music-mood", "instrumentation", "voice-character", "voice-delivery",
|
|
84
|
+
// single-value (flat)
|
|
85
|
+
"lens", "camera-format", "setting", "atmosphere", "style", "mood", "color-look",
|
|
86
|
+
"photographer", "aesthetic", "era", "photo-genre", "backdrop", "render-quality",
|
|
87
|
+
"composition-effects", "post-process-effects", "action-fx", "loop-subject",
|
|
88
|
+
"transition", "character-fx", "pose", "material", "held-prop", "camera-motion",
|
|
89
|
+
"animal", "vehicle", "weapon", "furniture",
|
|
90
|
+
]),
|
|
81
91
|
)
|
|
82
92
|
})
|
|
83
93
|
it.each(PICKER_TYPES)("%s: every dimension enum equals its catalog ids and is non-empty", (t) => {
|
|
@@ -13,7 +13,7 @@ describe("PROVIDER_PROMPT_DOCTRINES", () => {
|
|
|
13
13
|
expect(d.providers.length).toBeGreaterThan(0)
|
|
14
14
|
for (const p of d.providers) expect(MODEL_CATALOG[p]).toBeDefined()
|
|
15
15
|
expect(d.tips.length).toBeGreaterThanOrEqual(3)
|
|
16
|
-
expect(d.tips.length).toBeLessThanOrEqual(
|
|
16
|
+
expect(d.tips.length).toBeLessThanOrEqual(7)
|
|
17
17
|
for (const t of d.tips) expect(t.length).toBeLessThanOrEqual(220)
|
|
18
18
|
expect(d.doctrine.length).toBeGreaterThan(500)
|
|
19
19
|
expect(d.heading.length).toBeGreaterThan(0)
|
|
@@ -23,9 +23,11 @@ describe("PROVIDER_PROMPT_DOCTRINES", () => {
|
|
|
23
23
|
it("resolves by provider id, returns undefined/[] for providers without doctrine", () => {
|
|
24
24
|
expect(getPromptDoctrine("seedance-2")).toBeDefined()
|
|
25
25
|
expect(getPromptDoctrine("seedance-2-fast")).toBeDefined()
|
|
26
|
-
|
|
26
|
+
// bytedance-lite/pro + hailuo-2.3 are DELIBERATELY uncovered (older engines
|
|
27
|
+
// whose surfaces differ — mapping the family doctrine would overclaim).
|
|
28
|
+
expect(getPromptDoctrine("bytedance-lite")).toBeUndefined()
|
|
27
29
|
expect(getPromptTips("seedance-2").length).toBeGreaterThan(0)
|
|
28
|
-
expect(getPromptTips("
|
|
30
|
+
expect(getPromptTips("bytedance-lite")).toEqual([])
|
|
29
31
|
})
|
|
30
32
|
|
|
31
33
|
it("seedance doctrine encodes the official rules and bans the unstable patterns", () => {
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,36 @@ import { STYLINGS, STYLING_DIMENSION_ORDER, STYLING_DIMENSION_LABELS, STYLING_FI
|
|
|
10
10
|
import { FRAMINGS, FRAMING_CATEGORY_ORDER, FRAMING_CATEGORY_LABELS, FRAMING_FIELD_BY_CATEGORY, getFramingCategoryLimit } from "./framing.js"
|
|
11
11
|
import { LENSES } from "./lens.js"
|
|
12
12
|
import { CAMERA_FORMATS } from "./camera-format.js"
|
|
13
|
+
import { ANIMALS, VEHICLES, WEAPONS, FURNITURE } from "@nodaro/shared"
|
|
14
|
+
import { SETTINGS } from "./setting.js"
|
|
15
|
+
import { ATMOSPHERES } from "./atmosphere.js"
|
|
16
|
+
import { STYLES } from "./style.js"
|
|
17
|
+
import { MOODS } from "./mood.js"
|
|
18
|
+
import { COLOR_LOOKS } from "./color-look.js"
|
|
19
|
+
import { PHOTOGRAPHERS } from "./photographer.js"
|
|
20
|
+
import { AESTHETICS } from "./aesthetic.js"
|
|
21
|
+
import { ERAS } from "./era.js"
|
|
22
|
+
import { PHOTO_GENRES } from "./photo-genre.js"
|
|
23
|
+
import { BACKDROPS } from "./backdrop.js"
|
|
24
|
+
import { RENDER_QUALITIES } from "./render-quality.js"
|
|
25
|
+
import { COMPOSITION_EFFECTS } from "./composition-effects.js"
|
|
26
|
+
import { POST_PROCESS_EFFECTS } from "./post-process-effects.js"
|
|
27
|
+
import { ACTION_FX } from "./action-fx.js"
|
|
28
|
+
import { LOOP_SUBJECTS } from "./loop-subject.js"
|
|
29
|
+
import { TRANSITIONS } from "./transitions.js"
|
|
30
|
+
import { CHARACTER_FX } from "./character-fx.js"
|
|
31
|
+
import { POSES } from "./pose.js"
|
|
32
|
+
import { MATERIALS } from "./materials.js"
|
|
33
|
+
import { HELD_PROPS } from "./held-prop.js"
|
|
34
|
+
import { CAMERA_MOTIONS } from "./camera-motions.js"
|
|
35
|
+
import { LIGHTINGS, LIGHTING_CATEGORY_ORDER, LIGHTING_CATEGORY_LABELS, LIGHTING_FIELD_BY_CATEGORY } from "./lighting.js"
|
|
36
|
+
import { TEMPORALS } from "./temporal.js"
|
|
37
|
+
import { EXPOSURE_SETTINGS } from "./exposure-settings.js"
|
|
38
|
+
import { MUSIC_GENRES, MUSIC_ERAS } from "./music-genre.js"
|
|
39
|
+
import { MUSIC_ENERGIES, MUSIC_EMOTIONS, MUSIC_VIBES } from "./music-mood.js"
|
|
40
|
+
import { INSTRUMENTS, PRODUCTION_STYLES, VOCAL_PRESENCE, SINGING_STYLES } from "./instrumentation.js"
|
|
41
|
+
import { VOICE_AGES, VOICE_GENDERS, VOICE_LANGUAGES, VOICE_ACCENTS, VOICE_TIMBRES } from "./voice-character.js"
|
|
42
|
+
import { VOICE_PACES, VOICE_EMOTIONS, VOICE_ARCHETYPES } from "./voice-delivery.js"
|
|
13
43
|
|
|
14
44
|
// ─── Descriptor model ────────────────────────────────────────────────────────
|
|
15
45
|
|
|
@@ -57,6 +87,16 @@ export type PickerAnalyzerDescriptor =
|
|
|
57
87
|
|
|
58
88
|
const PERSON_EXCLUDED = new Set<string>(["age-custom"])
|
|
59
89
|
|
|
90
|
+
/** Tag catalog entries with an explicit dimension — for discriminated pickers
|
|
91
|
+
* whose fields live in SEPARATE catalogs (music/voice) rather than one
|
|
92
|
+
* discriminated catalog. */
|
|
93
|
+
function tagDim<T extends { id: string; label: string; description: string }>(
|
|
94
|
+
arr: ReadonlyArray<T>,
|
|
95
|
+
dimension: string,
|
|
96
|
+
): ReadonlyArray<AnalyzerEntry> {
|
|
97
|
+
return arr.map((e) => ({ id: e.id, label: e.label, description: e.description, dimension }))
|
|
98
|
+
}
|
|
99
|
+
|
|
60
100
|
const personCleanup: ApplyCleanup = (patch, mode) => {
|
|
61
101
|
if (mode === "override") {
|
|
62
102
|
patch.customAge = undefined
|
|
@@ -113,10 +153,160 @@ export const PICKER_ANALYZER_REGISTRY = {
|
|
|
113
153
|
label: "Camera / Film Stock",
|
|
114
154
|
entries: CAMERA_FORMATS as ReadonlyArray<AnalyzerEntry>,
|
|
115
155
|
},
|
|
156
|
+
|
|
157
|
+
// ─── Text-to-picker expansion (Cine AI Fill): every remaining catalog ─────
|
|
158
|
+
// Flat single-value pickers — field names match the picker wiring's
|
|
159
|
+
// valueField (and the node-data shape the published-app input card writes).
|
|
160
|
+
setting: { kind: "flat", toolName: "emit_setting", field: "setting", label: "Setting", entries: SETTINGS as ReadonlyArray<AnalyzerEntry> },
|
|
161
|
+
atmosphere: { kind: "flat", toolName: "emit_atmosphere", field: "atmosphere", label: "Atmosphere", entries: ATMOSPHERES as ReadonlyArray<AnalyzerEntry> },
|
|
162
|
+
style: { kind: "flat", toolName: "emit_style", field: "style", label: "Style", entries: STYLES as ReadonlyArray<AnalyzerEntry> },
|
|
163
|
+
mood: { kind: "flat", toolName: "emit_mood", field: "mood", label: "Mood", entries: MOODS as ReadonlyArray<AnalyzerEntry> },
|
|
164
|
+
"color-look": { kind: "flat", toolName: "emit_color_look", field: "colorLook", label: "Color / Look", entries: COLOR_LOOKS as ReadonlyArray<AnalyzerEntry> },
|
|
165
|
+
photographer: { kind: "flat", toolName: "emit_photographer", field: "photographer", label: "Photographer / Artist", entries: PHOTOGRAPHERS as ReadonlyArray<AnalyzerEntry> },
|
|
166
|
+
aesthetic: { kind: "flat", toolName: "emit_aesthetic", field: "aesthetic", label: "Aesthetic / Microtrend", entries: AESTHETICS as ReadonlyArray<AnalyzerEntry> },
|
|
167
|
+
era: { kind: "flat", toolName: "emit_era", field: "era", label: "Era / Period", entries: ERAS as ReadonlyArray<AnalyzerEntry> },
|
|
168
|
+
"photo-genre": { kind: "flat", toolName: "emit_photo_genre", field: "photoGenre", label: "Photo Genre", entries: PHOTO_GENRES as ReadonlyArray<AnalyzerEntry> },
|
|
169
|
+
backdrop: { kind: "flat", toolName: "emit_backdrop", field: "backdrop", label: "Backdrop", entries: BACKDROPS as ReadonlyArray<AnalyzerEntry> },
|
|
170
|
+
"render-quality": { kind: "flat", toolName: "emit_render_quality", field: "renderQuality", label: "Render Quality", entries: RENDER_QUALITIES as ReadonlyArray<AnalyzerEntry> },
|
|
171
|
+
"composition-effects": { kind: "flat", toolName: "emit_composition_effects", field: "compositionEffect", label: "Composition Effect", entries: COMPOSITION_EFFECTS as ReadonlyArray<AnalyzerEntry> },
|
|
172
|
+
"post-process-effects": { kind: "flat", toolName: "emit_post_process_effects", field: "postProcess", label: "Post-Process Effect", entries: POST_PROCESS_EFFECTS as ReadonlyArray<AnalyzerEntry> },
|
|
173
|
+
"action-fx": { kind: "flat", toolName: "emit_action_fx", field: "actionFx", label: "Action FX", entries: ACTION_FX as ReadonlyArray<AnalyzerEntry> },
|
|
174
|
+
"loop-subject": { kind: "flat", toolName: "emit_loop_subject", field: "loopSubject", label: "Loop Subject", entries: LOOP_SUBJECTS as ReadonlyArray<AnalyzerEntry> },
|
|
175
|
+
transition: { kind: "flat", toolName: "emit_transition", field: "transition", label: "Transition", entries: TRANSITIONS as ReadonlyArray<AnalyzerEntry> },
|
|
176
|
+
"character-fx": { kind: "flat", toolName: "emit_character_fx", field: "characterFx", label: "Character FX", entries: CHARACTER_FX as ReadonlyArray<AnalyzerEntry> },
|
|
177
|
+
pose: { kind: "flat", toolName: "emit_pose", field: "pose", label: "Pose", entries: POSES as ReadonlyArray<AnalyzerEntry> },
|
|
178
|
+
material: { kind: "flat", toolName: "emit_material", field: "material", label: "Material", entries: MATERIALS as ReadonlyArray<AnalyzerEntry> },
|
|
179
|
+
"held-prop": { kind: "flat", toolName: "emit_held_prop", field: "heldProp", label: "Held Prop", entries: HELD_PROPS as ReadonlyArray<AnalyzerEntry> },
|
|
180
|
+
"camera-motion": { kind: "flat", toolName: "emit_camera_motion", field: "cameraMotion", label: "Camera Motion", entries: CAMERA_MOTIONS as ReadonlyArray<AnalyzerEntry> },
|
|
181
|
+
animal: { kind: "flat", toolName: "emit_animal", field: "animal", label: "Animal", entries: ANIMALS as ReadonlyArray<AnalyzerEntry> },
|
|
182
|
+
vehicle: { kind: "flat", toolName: "emit_vehicle", field: "vehicle", label: "Vehicle", entries: VEHICLES as ReadonlyArray<AnalyzerEntry> },
|
|
183
|
+
weapon: { kind: "flat", toolName: "emit_weapon", field: "weapon", label: "Weapon", entries: WEAPONS as ReadonlyArray<AnalyzerEntry> },
|
|
184
|
+
furniture: { kind: "flat", toolName: "emit_furniture", field: "furniture", label: "Furniture", entries: FURNITURE as ReadonlyArray<AnalyzerEntry> },
|
|
185
|
+
|
|
186
|
+
// Discriminated multi-dim pickers. lighting/temporal/exposure discriminate
|
|
187
|
+
// on the single catalog's `category`; the sound/voice pickers span several
|
|
188
|
+
// per-field catalogs, so their entries are synthesized with an explicit
|
|
189
|
+
// `dimension` tag (tagDim below) — same wire shape either way.
|
|
190
|
+
lighting: {
|
|
191
|
+
kind: "discriminated",
|
|
192
|
+
toolName: "emit_lighting",
|
|
193
|
+
discriminator: "category",
|
|
194
|
+
order: LIGHTING_CATEGORY_ORDER as ReadonlyArray<string>,
|
|
195
|
+
fieldByKey: LIGHTING_FIELD_BY_CATEGORY as Readonly<Record<string, string>>,
|
|
196
|
+
labels: LIGHTING_CATEGORY_LABELS as Readonly<Record<string, string>>,
|
|
197
|
+
entries: LIGHTINGS as ReadonlyArray<AnalyzerEntry>,
|
|
198
|
+
limitFn: () => 1,
|
|
199
|
+
},
|
|
200
|
+
temporal: {
|
|
201
|
+
kind: "discriminated",
|
|
202
|
+
toolName: "emit_temporal",
|
|
203
|
+
discriminator: "category",
|
|
204
|
+
order: ["speed", "freeze", "direction", "shutter"],
|
|
205
|
+
fieldByKey: { speed: "temporalSpeed", freeze: "temporalFreeze", direction: "temporalDirection", shutter: "temporalShutter" },
|
|
206
|
+
labels: { speed: "Playback Speed", freeze: "Freeze", direction: "Direction", shutter: "Shutter" },
|
|
207
|
+
entries: TEMPORALS as ReadonlyArray<AnalyzerEntry>,
|
|
208
|
+
limitFn: () => 1,
|
|
209
|
+
},
|
|
210
|
+
"exposure-settings": {
|
|
211
|
+
kind: "discriminated",
|
|
212
|
+
toolName: "emit_exposure_settings",
|
|
213
|
+
discriminator: "category",
|
|
214
|
+
order: ["aperture", "shutter-speed", "iso"],
|
|
215
|
+
fieldByKey: { aperture: "aperture", "shutter-speed": "shutterSpeed", iso: "isoValue" },
|
|
216
|
+
labels: { aperture: "Aperture", "shutter-speed": "Shutter Speed", iso: "ISO" },
|
|
217
|
+
entries: EXPOSURE_SETTINGS as ReadonlyArray<AnalyzerEntry>,
|
|
218
|
+
limitFn: () => 1,
|
|
219
|
+
},
|
|
220
|
+
"music-genre": {
|
|
221
|
+
kind: "discriminated",
|
|
222
|
+
toolName: "emit_music_genre",
|
|
223
|
+
discriminator: "dimension",
|
|
224
|
+
order: ["genre", "subgenre", "era"],
|
|
225
|
+
fieldByKey: { genre: "genre", subgenre: "subgenre", era: "era" },
|
|
226
|
+
labels: { genre: "Genre", subgenre: "Subgenre", era: "Era" },
|
|
227
|
+
entries: [
|
|
228
|
+
...tagDim(MUSIC_GENRES, "genre"),
|
|
229
|
+
// Subgenres carry promptHint but no description — the hint doubles as
|
|
230
|
+
// the legend text (it describes the sound well enough for matching).
|
|
231
|
+
...MUSIC_GENRES.flatMap((g) =>
|
|
232
|
+
g.subgenres.map((s) => ({ id: s.id, label: s.label, description: s.promptHint, dimension: "subgenre" })),
|
|
233
|
+
),
|
|
234
|
+
...tagDim(MUSIC_ERAS, "era"),
|
|
235
|
+
],
|
|
236
|
+
limitFn: (k) => (k === "genre" ? 2 : 1),
|
|
237
|
+
},
|
|
238
|
+
"music-mood": {
|
|
239
|
+
kind: "discriminated",
|
|
240
|
+
toolName: "emit_music_mood",
|
|
241
|
+
discriminator: "dimension",
|
|
242
|
+
order: ["energy", "emotion", "vibe"],
|
|
243
|
+
fieldByKey: { energy: "energy", emotion: "emotion", vibe: "vibe" },
|
|
244
|
+
labels: { energy: "Energy", emotion: "Emotion", vibe: "Vibe" },
|
|
245
|
+
entries: [...tagDim(MUSIC_ENERGIES, "energy"), ...tagDim(MUSIC_EMOTIONS, "emotion"), ...tagDim(MUSIC_VIBES, "vibe")],
|
|
246
|
+
limitFn: (k) => (k === "energy" ? 1 : 2),
|
|
247
|
+
},
|
|
248
|
+
instrumentation: {
|
|
249
|
+
kind: "discriminated",
|
|
250
|
+
toolName: "emit_instrumentation",
|
|
251
|
+
discriminator: "dimension",
|
|
252
|
+
order: ["instruments", "production", "vocalPresence", "singingStyle"],
|
|
253
|
+
fieldByKey: { instruments: "instruments", production: "production", vocalPresence: "vocalPresence", singingStyle: "singingStyle" },
|
|
254
|
+
labels: { instruments: "Instruments", production: "Production Style", vocalPresence: "Vocal Presence", singingStyle: "Singing Style" },
|
|
255
|
+
entries: [
|
|
256
|
+
...tagDim(INSTRUMENTS, "instruments"),
|
|
257
|
+
...tagDim(PRODUCTION_STYLES, "production"),
|
|
258
|
+
...tagDim(VOCAL_PRESENCE, "vocalPresence"),
|
|
259
|
+
...tagDim(SINGING_STYLES, "singingStyle"),
|
|
260
|
+
],
|
|
261
|
+
limitFn: (k) => (k === "instruments" ? 3 : k === "production" ? 1 : 2),
|
|
262
|
+
},
|
|
263
|
+
"voice-character": {
|
|
264
|
+
kind: "discriminated",
|
|
265
|
+
toolName: "emit_voice_character",
|
|
266
|
+
discriminator: "dimension",
|
|
267
|
+
order: ["age", "gender", "language", "accent", "timbre"],
|
|
268
|
+
fieldByKey: { age: "age", gender: "gender", language: "language", accent: "accent", timbre: "timbre" },
|
|
269
|
+
labels: { age: "Age", gender: "Gender", language: "Language", accent: "Accent", timbre: "Timbre" },
|
|
270
|
+
entries: [
|
|
271
|
+
...tagDim(VOICE_AGES, "age"),
|
|
272
|
+
...tagDim(VOICE_GENDERS, "gender"),
|
|
273
|
+
...tagDim(VOICE_LANGUAGES, "language"),
|
|
274
|
+
...tagDim(VOICE_ACCENTS, "accent"),
|
|
275
|
+
...tagDim(VOICE_TIMBRES, "timbre"),
|
|
276
|
+
],
|
|
277
|
+
limitFn: (k) => (k === "language" ? 2 : 1),
|
|
278
|
+
},
|
|
279
|
+
"voice-delivery": {
|
|
280
|
+
kind: "discriminated",
|
|
281
|
+
toolName: "emit_voice_delivery",
|
|
282
|
+
discriminator: "dimension",
|
|
283
|
+
order: ["pace", "emotion", "archetype"],
|
|
284
|
+
fieldByKey: { pace: "pace", emotion: "emotion", archetype: "archetype" },
|
|
285
|
+
labels: { pace: "Pace", emotion: "Emotion", archetype: "Archetype" },
|
|
286
|
+
entries: [...tagDim(VOICE_PACES, "pace"), ...tagDim(VOICE_EMOTIONS, "emotion"), ...tagDim(VOICE_ARCHETYPES, "archetype")],
|
|
287
|
+
limitFn: () => 1,
|
|
288
|
+
},
|
|
116
289
|
} satisfies Record<string, PickerAnalyzerDescriptor>
|
|
117
290
|
|
|
118
291
|
export type PickerType = keyof typeof PICKER_ANALYZER_REGISTRY
|
|
119
292
|
export const PICKER_TYPES = Object.keys(PICKER_ANALYZER_REGISTRY) as PickerType[]
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Family grouping for BATCHED analysis. A single call across all 38 catalogs
|
|
296
|
+
* carries a ~211k-char legend (~53k tokens — measured 2026-08-09, the
|
|
297
|
+
* measure-first probe from the text-to-picker spec), which is slow, costly,
|
|
298
|
+
* and dilutes per-section accuracy. The text-to-picker route fans out one
|
|
299
|
+
* structured call per family (6-15k tokens each) and merges. Mirrors the
|
|
300
|
+
* build-brief's §5 UI grouping so Cine can reuse the same partition.
|
|
301
|
+
*/
|
|
302
|
+
export const PICKER_ANALYZER_FAMILIES: Readonly<Record<string, ReadonlyArray<PickerType>>> = {
|
|
303
|
+
scene: ["setting", "atmosphere", "backdrop", "era", "temporal"],
|
|
304
|
+
look: ["style", "color-look", "mood", "aesthetic", "photographer", "photo-genre", "render-quality", "composition-effects", "post-process-effects"],
|
|
305
|
+
camera: ["framing", "camera-motion", "lens", "camera-format", "lighting", "exposure-settings"],
|
|
306
|
+
character: ["person", "styling", "pose", "character-fx"],
|
|
307
|
+
elements: ["animal", "vehicle", "weapon", "furniture", "held-prop", "material", "action-fx", "loop-subject", "transition"],
|
|
308
|
+
audio: ["music-genre", "music-mood", "instrumentation", "voice-character", "voice-delivery"],
|
|
309
|
+
}
|
|
120
310
|
export const ANALYZABLE_PICKER_TYPES: ReadonlySet<string> = new Set(PICKER_TYPES)
|
|
121
311
|
export function isAnalyzablePicker(t: string): t is PickerType {
|
|
122
312
|
return ANALYZABLE_PICKER_TYPES.has(t)
|