@nodaro/prompts 1.12.0 → 1.14.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 (36) hide show
  1. package/dist/index.cjs +389 -194
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +244 -29
  4. package/dist/index.d.ts +244 -29
  5. package/dist/index.js +377 -196
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/__tests__/assemble-image-input-cap.test.ts +37 -13
  9. package/src/__tests__/assemble-image-input.test.ts +100 -19
  10. package/src/__tests__/assemble-video-input-cap.test.ts +101 -15
  11. package/src/__tests__/assemble-video-input.test.ts +167 -33
  12. package/src/__tests__/direction-hint-token-safety.test.ts +21 -0
  13. package/src/__tests__/multi-picker-spec.test.ts +21 -1
  14. package/src/__tests__/person-regional-aesthetic.test.ts +2 -1
  15. package/src/__tests__/prompt-style-section.test.ts +345 -0
  16. package/src/__tests__/provider-prompt-doctrine.test.ts +39 -0
  17. package/src/__tests__/style-section-boundary.test.ts +179 -0
  18. package/src/__tests__/subject-fold.test.ts +32 -13
  19. package/src/assemble-image-input.ts +51 -26
  20. package/src/assemble-video-input.ts +54 -34
  21. package/src/direction-registry.ts +108 -37
  22. package/src/gemini-omni-inputs.ts +11 -3
  23. package/src/held-prop.ts +1 -0
  24. package/src/hint-shedding.ts +23 -4
  25. package/src/index.ts +1 -0
  26. package/src/person.ts +4 -0
  27. package/src/picker-analyzer-registry.ts +37 -0
  28. package/src/prompt-builder.ts +84 -27
  29. package/src/prompt-hint-join.ts +9 -0
  30. package/src/prompt-style-section.ts +256 -0
  31. package/src/prompt-wizard-categories.ts +6 -0
  32. package/src/provider-prompt-doctrine.ts +51 -2
  33. package/src/setting.ts +1 -0
  34. package/src/style.ts +1 -0
  35. package/src/styling.ts +5 -1
  36. package/src/video-reference-resolver.ts +5 -2
package/dist/index.cjs CHANGED
@@ -1766,7 +1766,8 @@ var STYLES = [
1766
1766
  { id: "pastel", label: "Pastel", description: "Degas-era soft chalk pastel", promptHint: "rendered as a soft chalk pastel drawing, dry powdery pigment laid in feathered strokes on tinted paper with luminous dusty colors, gently blended edges and the dreamy Degas-era atmosphere of dancers in stage light", term: "soft pastel drawing" },
1767
1767
  { id: "acrylic-paint", label: "Acrylic Paint", description: "Fast-drying opaque acrylic on canvas", promptHint: "rendered as an acrylic painting on canvas, fast-drying opaque pigment with crisp sharp edges, confident quick brush strokes, high-key saturated color and a flatter more graphic finish than traditional oil paint", term: "acrylic painting" },
1768
1768
  { id: "mixed-media", label: "Mixed Media", description: "Collage + paint + ink hybrid", promptHint: "rendered as a mixed-media artwork combining torn paper collage, acrylic paint, ink and graphite on a layered substrate, heterogeneous textures, visible tape and stitching, and an exuberant hand-assembled studio-art quality" },
1769
- { id: "manga", label: "Manga", description: "Inked B&W Japanese comic panel", promptHint: "rendered as inked manga panel art, crisp black ink on white with confident line weight variation, screen-tone dot patterns for shading, dramatic speed lines and the distinctly Japanese black-and-white comic aesthetic \u2014 separate from full-color anime" }
1769
+ { id: "manga", label: "Manga", description: "Inked B&W Japanese comic panel", promptHint: "rendered as inked manga panel art, crisp black ink on white with confident line weight variation, screen-tone dot patterns for shading, dramatic speed lines and the distinctly Japanese black-and-white comic aesthetic \u2014 separate from full-color anime" },
1770
+ { id: "early-color-photo", label: "Early Color Photo", description: "Prokudin-Gorsky / autochrome early-1900s color", promptHint: "rendered as an early-1900s color photograph in the Prokudin-Gorsky / autochrome tradition \u2014 soft three-colour-separation registration, muted dye-toned palette, fine grain and a gentle antique warmth", term: "early autochrome color photograph" }
1770
1771
  ];
1771
1772
  var styleById = new Map(STYLES.map((s) => [s.id, s]));
1772
1773
  function getStyle(id) {
@@ -1832,6 +1833,7 @@ var SETTINGS = [
1832
1833
  { id: "parking-lot", label: "Parking Lot", category: "urban", description: "Suburban parking lot at dusk", promptHint: "set in an empty suburban parking lot at dusk with sodium-vapor lamps casting orange pools, scattered shopping carts and painted lane lines" },
1833
1834
  { id: "penthouse", label: "Penthouse", category: "urban", description: "Luxury penthouse with skyline view", promptHint: "set in a luxury penthouse interior with panoramic skyline views, marble floors, modernist furniture and low warm ambient light" },
1834
1835
  { id: "gas-station", label: "Gas Station", category: "urban", description: "Lonely highway gas station at night", promptHint: "set at a lonely highway gas station at night with a fluorescent canopy, bug-swarmed sodium lamps and cracked asphalt" },
1836
+ { id: "open-air-market", label: "Open-Air Market", category: "urban", description: "Bustling market of vendor stalls under canopies", term: "open-air market", promptHint: "set in a bustling open-air market \u2014 rows of vendor stalls under thatched and canvas canopies, produce piled high, warm dusty light and crowds moving between the stalls" },
1835
1837
  // -------------------- Nature --------------------
1836
1838
  { id: "forest", label: "Forest Clearing", category: "nature", description: "Sunlit mossy clearing", promptHint: "set in a sunlit forest clearing with moss-covered stones, dappled light through tall trees and a soft carpet of fallen leaves" },
1837
1839
  { id: "beach", label: "Beach", category: "nature", description: "Wide sandy beach with surf", promptHint: "set on a wide sandy beach with gentle breaking surf, footprints in wet sand and a pastel horizon" },
@@ -2958,6 +2960,9 @@ var PEOPLE = [
2958
2960
  { id: "mumbai-bollywood", label: "Mumbai Bollywood", group: "Asia", dimension: "regional-aesthetic", description: "Mumbai Indian film-industry aesthetic", promptHint: "a Mumbai Bollywood aesthetic \u2014 Indian film-industry vibe, vibrant statement-glamour", term: "mumbai bollywood aesthetic" },
2959
2961
  { id: "south-india-traditional", label: "South India Traditional", group: "Asia", dimension: "regional-aesthetic", description: "Tamil / Kerala temple-town classical aesthetic", promptHint: "a South Indian traditional aesthetic \u2014 Tamil / Kerala temple-town vibe, classical refinement", term: "south indian traditional aesthetic" },
2960
2962
  { id: "bangkok-street", label: "Bangkok Street", group: "Asia", dimension: "regional-aesthetic", description: "Bangkok Thai night-market neon-urban aesthetic", promptHint: "a Bangkok street aesthetic \u2014 Thai night-market energy, neon-and-warmth urban vibe", term: "bangkok street aesthetic" },
2963
+ // ----- Central Asia -----
2964
+ { id: "samarkand-silk-road", label: "Samarkand Silk Road", group: "Central Asia", dimension: "regional-aesthetic", description: "Uzbek Silk Road bazaar aesthetic (Samarkand / Bukhara)", promptHint: "a Central Asian Silk Road aesthetic \u2014 Samarkand / Bukhara bazaar vibe, ikat-and-suzani textiles, sun-baked adobe and blue-tiled madrasa mood", term: "samarkand silk road aesthetic" },
2965
+ { id: "tashkent-modern", label: "Tashkent Modern", group: "Central Asia", dimension: "regional-aesthetic", description: "Contemporary Uzbek metropolitan Central Asian aesthetic", promptHint: "a modern Tashkent aesthetic \u2014 contemporary Uzbek metropolitan vibe, Soviet-modern-meets-Silk-Road blend, warm steppe-city confidence", term: "tashkent modern aesthetic" },
2961
2966
  // ----- Latin America -----
2962
2967
  { id: "carioca-rio", label: "Carioca (Rio)", group: "Latin America", dimension: "regional-aesthetic", description: "Rio de Janeiro beach-and-favela-music Brazilian aesthetic", promptHint: "a Carioca aesthetic \u2014 Rio de Janeiro beach-and-favela-music vibe, sun-warmed Brazilian energy", term: "carioca aesthetic" },
2963
2968
  { id: "paulista", label: "Paulista (S\xE3o Paulo)", group: "Latin America", dimension: "regional-aesthetic", description: "S\xE3o Paulo metropolitan Brazilian creative-class aesthetic", promptHint: "a Paulista aesthetic \u2014 S\xE3o Paulo metropolitan vibe, urban-Brazilian creative-class polish", term: "paulista aesthetic" },
@@ -3798,6 +3803,9 @@ var STYLINGS = [
3798
3803
  { id: "outfit-fairy", label: "Fairy", dimension: "outfit", description: "Fantasy fairy: gauzy wings, flower crown, ethereal dress", promptHint: "wearing a fantasy fairy costume \u2014 gauzy translucent wings, a flower crown, and an ethereal flowing dress", term: "fairy costume with wings" },
3799
3804
  { id: "outfit-mermaid", label: "Mermaid", dimension: "outfit", description: "Fantasy mermaid: scaled tail/skirt, shell top, flowing hair", promptHint: "wearing a fantasy mermaid costume \u2014 a scaled tail or fitted scaled skirt, a shell top, and long flowing hair", term: "mermaid costume" },
3800
3805
  { id: "outfit-pharaoh", label: "Pharaoh Regalia", dimension: "outfit", description: "Ancient Egyptian royalty: usekh collar, pectoral, pleated kilt", promptHint: "wearing ancient Egyptian pharaoh regalia \u2014 a broad beaded usekh collar, a jeweled falcon pectoral, and a pleated linen shendyt kilt with golden arm cuffs" },
3806
+ { id: "outfit-workwear-overalls", label: "Workwear Overalls", dimension: "outfit", description: "Denim bib overalls over a plaid flannel shirt", promptHint: "dressed in a farmer's workwear outfit \u2014 denim bib overalls over a checked plaid flannel shirt, sturdy and worn-in", term: "denim overalls and plaid shirt" },
3807
+ { id: "outfit-chapan", label: "Chapan Robe", dimension: "outfit", description: "Central Asian long quilted ikat robe", promptHint: "dressed in a traditional Central Asian chapan \u2014 a long quilted robe with an ikat-striped weave, tied at the waist with a sash" },
3808
+ { id: "outfit-caftan", label: "Caftan", dimension: "outfit", description: "Long flowing Middle-Eastern / North-African robe", promptHint: "dressed in a long flowing caftan robe, a full-length garment worn across the Middle East and North Africa" },
3801
3809
  // -------------------- Top (upper-body garment) --------------------
3802
3810
  { id: "top-tshirt", label: "T-Shirt", dimension: "top", description: "Plain crewneck t-shirt", promptHint: "wearing a fitted plain crewneck t-shirt with short sleeves" },
3803
3811
  { id: "top-tank", label: "Tank Top", dimension: "top", description: "Scoop-neck tank top", promptHint: "wearing a fitted scoop-neck tank top with thin shoulder straps" },
@@ -3972,6 +3980,8 @@ var STYLING_FIELD_BY_DIMENSION = {
3972
3980
  "wardrobe-state": "wardrobeState"
3973
3981
  };
3974
3982
  var MAX_SELECTED_BY_STYLING_DIMENSION = {
3983
+ headwear: 2,
3984
+ // a hat layered over a wrap/turban
3975
3985
  jewelry: 3,
3976
3986
  "wardrobe-state": 3,
3977
3987
  "hair-state": 2
@@ -6326,7 +6336,8 @@ var HELD_PROPS = [
6326
6336
  { id: "flashlight", label: "Flashlight", category: "occupational", description: "Modern flashlight cutting a beam", promptHint: "holding a modern flashlight raised forward in one hand, a sharp white beam cutting through the darkness ahead and side-lighting the face", term: "holding a flashlight raised forward" },
6327
6337
  { id: "compass", label: "Compass", category: "occupational", description: "Vintage handheld nautical compass", promptHint: "holding a vintage brass nautical compass open in one cupped palm at chest height, the needle clearly visible as the eyes drift down to read the bearing", term: "holding an open brass nautical compass" },
6328
6338
  { id: "bow-and-arrow", label: "Bow and Arrow", category: "occupational", description: "Drawn archery bow with arrow nocked", promptHint: "holding an archery bow drawn at full tension with one hand on the grip and the other pulling the string back to the cheek, an arrow nocked and aimed forward", term: "drawing an archery bow with a nocked arrow" },
6329
- { id: "shield", label: "Shield", category: "occupational", description: "Handheld medieval shield", promptHint: "holding a medieval shield raised across the body with one arm strapped through the back, the front face angled forward in a defensive stance", term: "holding a raised medieval shield" }
6339
+ { id: "shield", label: "Shield", category: "occupational", description: "Handheld medieval shield", promptHint: "holding a medieval shield raised across the body with one arm strapped through the back, the front face angled forward in a defensive stance", term: "holding a raised medieval shield" },
6340
+ { id: "work-gloves", label: "Work Gloves", category: "occupational", description: "Worn leather work gloves held in hand", promptHint: "holding a worn pair of tan leather work gloves in both hands at waist height, the thick weathered leather clearly visible", term: "holding a pair of leather work gloves" }
6330
6341
  ];
6331
6342
  var heldPropById = new Map(HELD_PROPS.map((p) => [p.id, p]));
6332
6343
  function getHeldProp(id) {
@@ -10217,6 +10228,218 @@ function applyTemplate(template, vars) {
10217
10228
  template
10218
10229
  );
10219
10230
  }
10231
+
10232
+ // src/prompt-hint-join.ts
10233
+ var PROMPT_HINT_SEPARATOR = ". ";
10234
+ function joinPromptHints(userPrompt, hints) {
10235
+ if (hints.length === 0) return userPrompt;
10236
+ return [userPrompt.trim(), ...hints].filter((p) => p.length > 0).join(PROMPT_HINT_SEPARATOR);
10237
+ }
10238
+
10239
+ // src/direction-registry.ts
10240
+ var perId = (full, compact) => (ids, mode) => {
10241
+ const out = [];
10242
+ for (const id of ids) {
10243
+ const frag = mode === "compact" ? compact(id) : full(id);
10244
+ if (frag.length > 0) out.push(frag);
10245
+ }
10246
+ return out;
10247
+ };
10248
+ var viaListBuilder = (build) => (ids, mode) => build(ids.length === 1 ? ids[0] : [...ids], mode).filter((s) => s.length > 0);
10249
+ var viaStringBuilder = (build) => (ids, mode) => {
10250
+ const s = build(ids.length === 1 ? ids[0] : [...ids], mode);
10251
+ return s.length > 0 ? [s] : [];
10252
+ };
10253
+ var viaMood = (ids, mode) => buildMoodHints({ mood: ids.length === 1 ? ids[0] : [...ids] }, mode).filter(
10254
+ (s) => s.length > 0
10255
+ );
10256
+ var framing = perId(getFramingPromptHint, getFramingTerm);
10257
+ var lighting = perId(getLightingPromptHint, getLightingTerm);
10258
+ var exposure = perId(getExposurePromptHint, getExposureTerm);
10259
+ var temporal = perId(getTemporalPromptHint, getTemporalTerm);
10260
+ var DIRECTION_FIELDS = [
10261
+ { key: "cameraMotion", surface: "video", family: "motion", maxPicks: 1, render: perId(getCameraMotionPromptHint, getCameraMotionTerm) },
10262
+ // Composition (the Framing catalog, one row per category).
10263
+ { key: "shotSize", surface: "both", family: "look", maxPicks: 1, render: framing },
10264
+ { key: "angle", surface: "both", family: "look", maxPicks: 1, render: framing },
10265
+ { key: "coverage", surface: "both", family: "look", maxPicks: 1, render: framing },
10266
+ { key: "composition", surface: "both", family: "look", maxPicks: 2, render: framing },
10267
+ { key: "vantage", surface: "both", family: "look", maxPicks: 1, render: framing },
10268
+ { key: "pose", surface: "both", family: "look", maxPicks: 1, render: perId(getPosePromptHint, getPoseTerm) },
10269
+ { key: "compositionEffect", surface: "both", family: "look", maxPicks: 1, render: perId(getCompositionEffectPromptHint, getCompositionEffectTerm) },
10270
+ // Camera.
10271
+ { key: "cameraFormat", surface: "both", family: "look", styleGroup: "film", maxPicks: 1, render: perId(getCameraFormatPromptHint, getCameraFormatTerm) },
10272
+ { key: "lens", surface: "both", family: "look", maxPicks: 1, render: perId(getLensPromptHint, getLensTerm) },
10273
+ // Exposure (stills only — a video's exposure rides its own temporal levers).
10274
+ { key: "aperture", surface: "image", family: "look", maxPicks: 1, render: exposure },
10275
+ { key: "shutterSpeed", surface: "image", family: "look", maxPicks: 1, render: exposure },
10276
+ { key: "isoValue", surface: "image", family: "look", maxPicks: 1, render: exposure },
10277
+ // Light (the Lighting catalog, one row per category).
10278
+ { key: "timeOfDay", surface: "both", family: "look", maxPicks: 1, render: lighting },
10279
+ { key: "lightingStyle", surface: "both", family: "look", maxPicks: 2, render: lighting },
10280
+ { key: "lightingDirection", surface: "both", family: "look", maxPicks: 1, render: lighting },
10281
+ { key: "lightingRatio", surface: "both", family: "look", maxPicks: 1, render: lighting },
10282
+ { key: "colorTemperature", surface: "both", family: "look", maxPicks: 1, render: lighting },
10283
+ { key: "colorLook", surface: "both", family: "look", styleGroup: "film", maxPicks: 1, render: perId(getColorLookPromptHint, getColorLookTerm) },
10284
+ { key: "atmosphere", surface: "both", family: "look", maxPicks: 2, render: viaListBuilder(buildAtmosphereHints) },
10285
+ { key: "postProcess", surface: "image", family: "look", maxPicks: 2, render: viaListBuilder(buildPostProcessHints) },
10286
+ // Style.
10287
+ { key: "style", surface: "both", family: "look", styleGroup: "film", maxPicks: 1, render: perId(getStylePromptHint, getStyleTerm) },
10288
+ { key: "mood", surface: "both", family: "look", maxPicks: 2, render: viaMood },
10289
+ { key: "aesthetic", surface: "both", family: "look", maxPicks: 2, render: viaStringBuilder(buildAestheticHints) },
10290
+ { key: "photoGenre", surface: "image", family: "look", maxPicks: 1, render: perId(getPhotoGenrePromptHint, getPhotoGenreTerm) },
10291
+ { key: "photographer", surface: "image", family: "look", maxPicks: 2, render: viaStringBuilder(buildPhotographerHints) },
10292
+ { key: "renderQuality", surface: "image", family: "look", maxPicks: 1, render: perId(getRenderQualityPromptHint, getRenderQualityTerm) },
10293
+ // Scene.
10294
+ { key: "setting", surface: "both", family: "look", maxPicks: 1, render: perId(getSettingPromptHint, getSettingTerm) },
10295
+ { key: "era", surface: "both", family: "look", styleGroup: "film", maxPicks: 1, render: perId(getEraPromptHint, getEraTerm) },
10296
+ { key: "backdrop", surface: "both", family: "look", maxPicks: 1, render: perId(getBackdropPromptHint, getBackdropTerm) },
10297
+ // Motion & time.
10298
+ { key: "actionFx", surface: "video", family: "motion", maxPicks: 2, render: viaListBuilder(buildActionFxHints) },
10299
+ { key: "temporalSpeed", surface: "video", family: "motion", maxPicks: 1, render: temporal },
10300
+ { key: "temporalFreeze", surface: "video", family: "motion", maxPicks: 1, render: temporal },
10301
+ { key: "temporalDirection", surface: "video", family: "motion", maxPicks: 1, render: temporal },
10302
+ { key: "temporalShutter", surface: "video", family: "motion", maxPicks: 1, render: temporal },
10303
+ { key: "transition", surface: "video", family: "motion", maxPicks: 2, render: perId(getTransitionPromptHint, getTransitionTerm) },
10304
+ { key: "loopSubject", surface: "video", family: "motion", maxPicks: 1, render: perId(getLoopSubjectPromptHint, getLoopSubjectTerm) },
10305
+ // ── LEGACY BLOCK — see the table doc above. Placed LAST, in today's exact
10306
+ // `composePromptText` order, so every pre-registry caller is byte-identical.
10307
+ { key: "framingId", surface: "both", family: "look", maxPicks: 1, render: framing },
10308
+ { key: "framingAngleId", surface: "both", family: "look", maxPicks: 1, render: framing },
10309
+ { key: "lightingId", surface: "both", family: "look", maxPicks: 1, render: lighting },
10310
+ { key: "lensId", surface: "both", family: "look", maxPicks: 1, render: perId(getLensPromptHint, getLensTerm) },
10311
+ { key: "cameraFormatId", surface: "both", family: "look", styleGroup: "film", maxPicks: 1, render: perId(getCameraFormatPromptHint, getCameraFormatTerm) }
10312
+ ];
10313
+ var DIRECTION_SPECS = DIRECTION_FIELDS;
10314
+ var DIRECTION_KEYS = DIRECTION_FIELDS.map((f) => f.key);
10315
+ var FILM_STYLE_KEYS = DIRECTION_SPECS.filter(
10316
+ (f) => f.styleGroup === "film"
10317
+ ).map((f) => f.key);
10318
+ var IMAGE_HINT_MODE_DEFAULT = "full";
10319
+ var VIDEO_HINT_MODE_DEFAULT = { look: "full", motion: "compact" };
10320
+ function modeForFamily(mode, family) {
10321
+ return typeof mode === "string" ? mode : family === "motion" ? mode.motion : mode.look;
10322
+ }
10323
+ var DIRECTION_ARRAY_CEILING = 8;
10324
+ var DIRECTION_ID_MAX_CHARS = 100;
10325
+ function normalizeDirectionIds(value, maxPicks) {
10326
+ const ids = [];
10327
+ if (typeof value === "string") {
10328
+ if (value) ids.push(value);
10329
+ } else if (Array.isArray(value)) {
10330
+ for (const v of value) {
10331
+ if (ids.length >= maxPicks) break;
10332
+ if (typeof v === "string" && v && !ids.includes(v)) ids.push(v);
10333
+ }
10334
+ }
10335
+ return ids.slice(0, maxPicks);
10336
+ }
10337
+ function directionFieldsForSurface(surface) {
10338
+ return DIRECTION_SPECS.filter((f) => f.surface === "both" || f.surface === surface);
10339
+ }
10340
+ function renderDirectionHintClauses(direction, opts) {
10341
+ if (!direction) return [];
10342
+ const mode = opts.mode ?? "full";
10343
+ const out = [];
10344
+ const seen = /* @__PURE__ */ new Set();
10345
+ for (const spec of DIRECTION_SPECS) {
10346
+ if (spec.surface !== "both" && spec.surface !== opts.surface) continue;
10347
+ const ids = normalizeDirectionIds(
10348
+ direction[spec.key],
10349
+ spec.maxPicks
10350
+ );
10351
+ if (ids.length === 0) continue;
10352
+ for (const hint of spec.render(ids, modeForFamily(mode, spec.family))) {
10353
+ if (hint.length > 0 && !seen.has(hint)) {
10354
+ seen.add(hint);
10355
+ out.push({
10356
+ key: spec.key,
10357
+ family: spec.family,
10358
+ ...spec.styleGroup !== void 0 ? { styleGroup: spec.styleGroup } : {},
10359
+ text: hint
10360
+ });
10361
+ }
10362
+ }
10363
+ }
10364
+ return out;
10365
+ }
10366
+ function renderDirectionHints(direction, opts) {
10367
+ return renderDirectionHintClauses(direction, opts).map((c) => c.text);
10368
+ }
10369
+
10370
+ // src/prompt-style-section.ts
10371
+ var STYLE_SECTION_HEADER = "[style]:";
10372
+ var STYLE_SECTION_GAP = "\n\n";
10373
+ var STYLE_SECTION_OPENING = `${STYLE_SECTION_GAP}${STYLE_SECTION_HEADER}
10374
+ `;
10375
+ function splitStyleSection(prompt) {
10376
+ const at = prompt.lastIndexOf(STYLE_SECTION_OPENING);
10377
+ if (at >= 0) {
10378
+ return { body: prompt.slice(0, at), section: prompt.slice(at + STYLE_SECTION_GAP.length) };
10379
+ }
10380
+ return prompt.startsWith(`${STYLE_SECTION_HEADER}
10381
+ `) ? { body: "", section: prompt } : { body: prompt, section: "" };
10382
+ }
10383
+ function insertBeforeStyleSection(prompt, lines) {
10384
+ if (lines.length === 0) return prompt;
10385
+ const block = lines.join("\n");
10386
+ const { body, section } = splitStyleSection(prompt);
10387
+ if (section.length === 0) return `${prompt}
10388
+ ${block}`;
10389
+ return `${body.length > 0 ? `${body}
10390
+ ${block}` : block}${STYLE_SECTION_GAP}${section}`;
10391
+ }
10392
+ function endsInsideStyleSection(prompt) {
10393
+ const at = prompt.lastIndexOf(STYLE_SECTION_GAP);
10394
+ const lastBlock = at >= 0 ? prompt.slice(at + STYLE_SECTION_GAP.length) : prompt;
10395
+ return lastBlock.startsWith(`${STYLE_SECTION_HEADER}
10396
+ `);
10397
+ }
10398
+ function styleSlotFor(row) {
10399
+ if (row.family === "motion") return "body";
10400
+ return row.styleGroup === "film" ? "film" : "scene";
10401
+ }
10402
+ function asBodyClauses(texts) {
10403
+ return texts.map((text) => ({ text, slot: "body" }));
10404
+ }
10405
+ function partitionStyleClauses(direction, opts) {
10406
+ return renderDirectionHintClauses(direction, opts).map((clause) => ({
10407
+ text: clause.text,
10408
+ slot: styleSlotFor(clause)
10409
+ }));
10410
+ }
10411
+ function styleSectionFromClauses(clauses) {
10412
+ const line = (slot) => clauses.filter((c) => c.slot === slot).map((c) => c.text).join(PROMPT_HINT_SEPARATOR);
10413
+ const lines = [line("film"), line("scene")].filter((l) => l.length > 0);
10414
+ return lines.length === 0 ? "" : `${STYLE_SECTION_HEADER}
10415
+ ${lines.join("\n")}`;
10416
+ }
10417
+ function renderStyleSection(direction, opts) {
10418
+ return styleSectionFromClauses(partitionStyleClauses(direction, opts));
10419
+ }
10420
+ function composeSectionedPrompt(userPrompt, clauses, structuredFragment) {
10421
+ const bodyHints = [
10422
+ ...clauses.filter((c) => c.slot === "body").map((c) => c.text),
10423
+ structuredFragment
10424
+ ].filter((p) => p.length > 0);
10425
+ const section = styleSectionFromClauses(clauses);
10426
+ if (section.length === 0) {
10427
+ return bodyHints.length === 0 ? userPrompt : joinPromptHints(userPrompt ?? "", bodyHints);
10428
+ }
10429
+ const body = bodyHints.length > 0 ? joinPromptHints(userPrompt ?? "", bodyHints) : (userPrompt ?? "").trim();
10430
+ return body.length > 0 ? `${body}${STYLE_SECTION_GAP}${section}` : section;
10431
+ }
10432
+ function sectionedClauseCosts(userPrompt, clauses, structuredFragment) {
10433
+ const lengthAt = (kept) => composeSectionedPrompt(userPrompt, clauses.slice(0, kept), structuredFragment)?.length ?? 0;
10434
+ const costs = [];
10435
+ let below = lengthAt(0);
10436
+ for (let i = 0; i < clauses.length; i++) {
10437
+ const at = lengthAt(i + 1);
10438
+ costs.push(at - below);
10439
+ below = at;
10440
+ }
10441
+ return costs;
10442
+ }
10220
10443
  function composeIdentityDescPart(subject, canonicalDesc, elementInjection) {
10221
10444
  const parts = [];
10222
10445
  const c = canonicalDesc?.trim();
@@ -10906,6 +11129,12 @@ function reconcileBodySegments(body, bodySegments) {
10906
11129
  }
10907
11130
  return [{ text: body, origin: "user" }];
10908
11131
  }
11132
+ function controlSuffixes(body, styleLine, avoidLine) {
11133
+ const separator = (text) => endsInsideStyleSection(text) ? STYLE_SECTION_GAP : "\n";
11134
+ const styleSuffix = styleLine ? `${separator(body)}${styleLine}` : "";
11135
+ const avoidSuffix = avoidLine ? `${separator(body + styleSuffix)}${avoidLine}` : "";
11136
+ return { styleSuffix, avoidSuffix };
11137
+ }
10909
11138
  function buildImagePrompt(config2) {
10910
11139
  return buildImagePromptInternal(config2);
10911
11140
  }
@@ -11182,9 +11411,7 @@ ${promptForNext}`;
11182
11411
  const lockBlock = allLockLines.length > 0 ? `${allLockLines.join("\n")}
11183
11412
 
11184
11413
  ` : "";
11185
- const trailingBlock = trailingLines.length > 0 ? `
11186
- ${trailingLines.join("\n")}` : "";
11187
- promptForNext = `${lockBlock}${promptForNext}${trailingBlock}`;
11414
+ promptForNext = `${lockBlock}${insertBeforeStyleSection(promptForNext, trailingLines)}`;
11188
11415
  }
11189
11416
  config2 = {
11190
11417
  ...config2,
@@ -11232,9 +11459,7 @@ ${trailingLines.join("\n")}` : "";
11232
11459
  ...locCanon.elementDirectives,
11233
11460
  ...objCanon.elementDirectives
11234
11461
  ];
11235
- const canonTrailingBlock = canonTrailingLines.length > 0 ? `
11236
- ${canonTrailingLines.join("\n")}` : "";
11237
- const composedScene = `${canonLockBlock}${scene}${canonTrailingBlock}`;
11462
+ const composedScene = `${canonLockBlock}${insertBeforeStyleSection(scene, canonTrailingLines)}`;
11238
11463
  prompt2 = config2.referenceLockSnippet ? `${config2.referenceLockSnippet}
11239
11464
  ${composedScene}` : composedScene;
11240
11465
  if (marks) marks.directivesPrefix = "";
@@ -11260,29 +11485,29 @@ ${directives}`;
11260
11485
  }
11261
11486
  }
11262
11487
  const styleText2 = style?.trim();
11263
- const styleSuffix2 = styleText2 ? `
11264
- Style: ${getStylePromptHint(styleText2) || styleText2}` : "";
11488
+ const styleLine2 = styleText2 ? `Style: ${getStylePromptHint(styleText2) || styleText2}` : "";
11265
11489
  const negPrompt2 = negativePrompt?.trim();
11266
11490
  let nativeNegativePrompt2;
11267
- let avoidSuffix2 = "";
11491
+ let avoidLine2 = "";
11268
11492
  if (negPrompt2) {
11269
11493
  if (shared.NATIVE_NEGATIVE_PROMPT_MODELS.has(provider)) {
11270
11494
  nativeNegativePrompt2 = negPrompt2.slice(0, shared.getMaxNegativePromptChars(provider));
11271
11495
  } else {
11272
- avoidSuffix2 = `
11273
- Avoid: ${negPrompt2}`;
11496
+ avoidLine2 = `Avoid: ${negPrompt2}`;
11274
11497
  }
11275
11498
  }
11276
- if (marks) {
11277
- marks.styleSuffix = styleSuffix2;
11278
- marks.avoidSuffix = avoidSuffix2;
11279
- }
11280
11499
  const maxLen2 = shared.getMaxImagePromptChars(provider);
11281
- const reserved2 = styleSuffix2.length + avoidSuffix2.length;
11500
+ const preCut2 = controlSuffixes(prompt2, styleLine2, avoidLine2);
11501
+ const reserved2 = preCut2.styleSuffix.length + preCut2.avoidSuffix.length;
11282
11502
  if (prompt2.length + reserved2 > maxLen2) {
11283
11503
  if (marks) marks.overflowChars += prompt2.length + reserved2 - maxLen2;
11284
11504
  prompt2 = prompt2.slice(0, Math.max(0, maxLen2 - reserved2 - 3)) + "...";
11285
11505
  }
11506
+ const { styleSuffix: styleSuffix2, avoidSuffix: avoidSuffix2 } = controlSuffixes(prompt2, styleLine2, avoidLine2);
11507
+ if (marks) {
11508
+ marks.styleSuffix = styleSuffix2;
11509
+ marks.avoidSuffix = avoidSuffix2;
11510
+ }
11286
11511
  if (marks) marks.bodyBeforeSuffixes = prompt2.slice(marks.directivesPrefix.length);
11287
11512
  prompt2 = prompt2 + styleSuffix2 + avoidSuffix2;
11288
11513
  if (prompt2.length > maxLen2) {
@@ -11334,35 +11559,37 @@ Avoid: ${negPrompt2}`;
11334
11559
  let prompt = config2.prompt;
11335
11560
  if (charDescs.length > 0) {
11336
11561
  const wrapperTemplate = resolveTemplate("generate-image-wrapper", userTemplates, flowTemplates);
11337
- prompt = applyTemplate(wrapperTemplate, {
11338
- userPrompt: prompt,
11562
+ const { body, section } = splitStyleSection(prompt);
11563
+ const wrapped = applyTemplate(wrapperTemplate, {
11564
+ userPrompt: body,
11339
11565
  assetDescriptions: charDescs.join(" ")
11340
11566
  });
11567
+ prompt = section.length > 0 ? `${wrapped}${STYLE_SECTION_GAP}${section}` : wrapped;
11341
11568
  }
11342
11569
  const styleText = style?.trim();
11343
- const styleSuffix = styleText ? `
11344
- Style: ${getStylePromptHint(styleText) || styleText}` : "";
11570
+ const styleLine = styleText ? `Style: ${getStylePromptHint(styleText) || styleText}` : "";
11345
11571
  const negPrompt = negativePrompt?.trim();
11346
11572
  let nativeNegativePrompt;
11347
- let avoidSuffix = "";
11573
+ let avoidLine = "";
11348
11574
  if (negPrompt) {
11349
11575
  if (shared.NATIVE_NEGATIVE_PROMPT_MODELS.has(provider)) {
11350
11576
  nativeNegativePrompt = negPrompt.slice(0, shared.getMaxNegativePromptChars(provider));
11351
11577
  } else {
11352
- avoidSuffix = `
11353
- Avoid: ${negPrompt}`;
11578
+ avoidLine = `Avoid: ${negPrompt}`;
11354
11579
  }
11355
11580
  }
11356
- if (marks) {
11357
- marks.styleSuffix = styleSuffix;
11358
- marks.avoidSuffix = avoidSuffix;
11359
- }
11360
11581
  const maxLen = shared.getMaxImagePromptChars(provider);
11361
- const reserved = styleSuffix.length + avoidSuffix.length;
11582
+ const preCut = controlSuffixes(prompt, styleLine, avoidLine);
11583
+ const reserved = preCut.styleSuffix.length + preCut.avoidSuffix.length;
11362
11584
  if (prompt.length + reserved > maxLen) {
11363
11585
  if (marks) marks.overflowChars += prompt.length + reserved - maxLen;
11364
11586
  prompt = prompt.slice(0, Math.max(0, maxLen - reserved - 3)) + "...";
11365
11587
  }
11588
+ const { styleSuffix, avoidSuffix } = controlSuffixes(prompt, styleLine, avoidLine);
11589
+ if (marks) {
11590
+ marks.styleSuffix = styleSuffix;
11591
+ marks.avoidSuffix = avoidSuffix;
11592
+ }
11366
11593
  if (marks) marks.bodyBeforeSuffixes = prompt;
11367
11594
  prompt = prompt + styleSuffix + avoidSuffix;
11368
11595
  if (prompt.length > maxLen) {
@@ -11589,7 +11816,12 @@ function capitalizeLineInitial(line) {
11589
11816
  return line.replace(/[a-zA-Z]/, (c) => c.toUpperCase());
11590
11817
  }
11591
11818
  function buildHybridScene(prompt, refs, finalIndexByUrl) {
11592
- return prompt.split("\n").map((line) => capitalizeLineInitial(expandImageRefTokensHybrid(line, refs, finalIndexByUrl))).join("\n");
11819
+ const lines = prompt.split("\n");
11820
+ const sectionAt = lines.indexOf(STYLE_SECTION_HEADER);
11821
+ return lines.map((line, i) => {
11822
+ const expanded = expandImageRefTokensHybrid(line, refs, finalIndexByUrl);
11823
+ return sectionAt >= 0 && i >= sectionAt ? expanded : capitalizeLineInitial(expanded);
11824
+ }).join("\n");
11593
11825
  }
11594
11826
  function buildIdentityDirectives(prompt, refs, meta3 = [], suppressedCanonicalLocationIds) {
11595
11827
  return collectIdentities(prompt, refs, meta3).map((id) => buildIdentityDirective(id, suppressedCanonicalLocationIds)).filter((s) => s.length > 0).join("\n");
@@ -11807,125 +12039,6 @@ function renderLens(l) {
11807
12039
  if (l.aperture) bits.push(`f/${l.aperture}`);
11808
12040
  return bits.length > 0 ? `Lens: ${bits.join(", ")}.` : "";
11809
12041
  }
11810
-
11811
- // src/direction-registry.ts
11812
- var perId = (full, compact) => (ids, mode) => {
11813
- const out = [];
11814
- for (const id of ids) {
11815
- const frag = mode === "compact" ? compact(id) : full(id);
11816
- if (frag.length > 0) out.push(frag);
11817
- }
11818
- return out;
11819
- };
11820
- var viaListBuilder = (build) => (ids, mode) => build(ids.length === 1 ? ids[0] : [...ids], mode).filter((s) => s.length > 0);
11821
- var viaStringBuilder = (build) => (ids, mode) => {
11822
- const s = build(ids.length === 1 ? ids[0] : [...ids], mode);
11823
- return s.length > 0 ? [s] : [];
11824
- };
11825
- var viaMood = (ids, mode) => buildMoodHints({ mood: ids.length === 1 ? ids[0] : [...ids] }, mode).filter(
11826
- (s) => s.length > 0
11827
- );
11828
- var framing = perId(getFramingPromptHint, getFramingTerm);
11829
- var lighting = perId(getLightingPromptHint, getLightingTerm);
11830
- var exposure = perId(getExposurePromptHint, getExposureTerm);
11831
- var temporal = perId(getTemporalPromptHint, getTemporalTerm);
11832
- var DIRECTION_FIELDS = [
11833
- { key: "cameraMotion", surface: "video", family: "motion", maxPicks: 1, render: perId(getCameraMotionPromptHint, getCameraMotionTerm) },
11834
- // Composition (the Framing catalog, one row per category).
11835
- { key: "shotSize", surface: "both", family: "look", maxPicks: 1, render: framing },
11836
- { key: "angle", surface: "both", family: "look", maxPicks: 1, render: framing },
11837
- { key: "coverage", surface: "both", family: "look", maxPicks: 1, render: framing },
11838
- { key: "composition", surface: "both", family: "look", maxPicks: 2, render: framing },
11839
- { key: "vantage", surface: "both", family: "look", maxPicks: 1, render: framing },
11840
- { key: "pose", surface: "both", family: "look", maxPicks: 1, render: perId(getPosePromptHint, getPoseTerm) },
11841
- { key: "compositionEffect", surface: "both", family: "look", maxPicks: 1, render: perId(getCompositionEffectPromptHint, getCompositionEffectTerm) },
11842
- // Camera.
11843
- { key: "cameraFormat", surface: "both", family: "look", maxPicks: 1, render: perId(getCameraFormatPromptHint, getCameraFormatTerm) },
11844
- { key: "lens", surface: "both", family: "look", maxPicks: 1, render: perId(getLensPromptHint, getLensTerm) },
11845
- // Exposure (stills only — a video's exposure rides its own temporal levers).
11846
- { key: "aperture", surface: "image", family: "look", maxPicks: 1, render: exposure },
11847
- { key: "shutterSpeed", surface: "image", family: "look", maxPicks: 1, render: exposure },
11848
- { key: "isoValue", surface: "image", family: "look", maxPicks: 1, render: exposure },
11849
- // Light (the Lighting catalog, one row per category).
11850
- { key: "timeOfDay", surface: "both", family: "look", maxPicks: 1, render: lighting },
11851
- { key: "lightingStyle", surface: "both", family: "look", maxPicks: 2, render: lighting },
11852
- { key: "lightingDirection", surface: "both", family: "look", maxPicks: 1, render: lighting },
11853
- { key: "lightingRatio", surface: "both", family: "look", maxPicks: 1, render: lighting },
11854
- { key: "colorTemperature", surface: "both", family: "look", maxPicks: 1, render: lighting },
11855
- { key: "colorLook", surface: "both", family: "look", maxPicks: 1, render: perId(getColorLookPromptHint, getColorLookTerm) },
11856
- { key: "atmosphere", surface: "both", family: "look", maxPicks: 2, render: viaListBuilder(buildAtmosphereHints) },
11857
- { key: "postProcess", surface: "image", family: "look", maxPicks: 2, render: viaListBuilder(buildPostProcessHints) },
11858
- // Style.
11859
- { key: "style", surface: "both", family: "look", maxPicks: 1, render: perId(getStylePromptHint, getStyleTerm) },
11860
- { key: "mood", surface: "both", family: "look", maxPicks: 2, render: viaMood },
11861
- { key: "aesthetic", surface: "both", family: "look", maxPicks: 2, render: viaStringBuilder(buildAestheticHints) },
11862
- { key: "photoGenre", surface: "image", family: "look", maxPicks: 1, render: perId(getPhotoGenrePromptHint, getPhotoGenreTerm) },
11863
- { key: "photographer", surface: "image", family: "look", maxPicks: 2, render: viaStringBuilder(buildPhotographerHints) },
11864
- { key: "renderQuality", surface: "image", family: "look", maxPicks: 1, render: perId(getRenderQualityPromptHint, getRenderQualityTerm) },
11865
- // Scene.
11866
- { key: "setting", surface: "both", family: "look", maxPicks: 1, render: perId(getSettingPromptHint, getSettingTerm) },
11867
- { key: "era", surface: "both", family: "look", maxPicks: 1, render: perId(getEraPromptHint, getEraTerm) },
11868
- { key: "backdrop", surface: "both", family: "look", maxPicks: 1, render: perId(getBackdropPromptHint, getBackdropTerm) },
11869
- // Motion & time.
11870
- { key: "actionFx", surface: "video", family: "motion", maxPicks: 2, render: viaListBuilder(buildActionFxHints) },
11871
- { key: "temporalSpeed", surface: "video", family: "motion", maxPicks: 1, render: temporal },
11872
- { key: "temporalFreeze", surface: "video", family: "motion", maxPicks: 1, render: temporal },
11873
- { key: "temporalDirection", surface: "video", family: "motion", maxPicks: 1, render: temporal },
11874
- { key: "temporalShutter", surface: "video", family: "motion", maxPicks: 1, render: temporal },
11875
- { key: "transition", surface: "video", family: "motion", maxPicks: 2, render: perId(getTransitionPromptHint, getTransitionTerm) },
11876
- { key: "loopSubject", surface: "video", family: "motion", maxPicks: 1, render: perId(getLoopSubjectPromptHint, getLoopSubjectTerm) },
11877
- // ── LEGACY BLOCK — see the table doc above. Placed LAST, in today's exact
11878
- // `composePromptText` order, so every pre-registry caller is byte-identical.
11879
- { key: "framingId", surface: "both", family: "look", maxPicks: 1, render: framing },
11880
- { key: "framingAngleId", surface: "both", family: "look", maxPicks: 1, render: framing },
11881
- { key: "lightingId", surface: "both", family: "look", maxPicks: 1, render: lighting },
11882
- { key: "lensId", surface: "both", family: "look", maxPicks: 1, render: perId(getLensPromptHint, getLensTerm) },
11883
- { key: "cameraFormatId", surface: "both", family: "look", maxPicks: 1, render: perId(getCameraFormatPromptHint, getCameraFormatTerm) }
11884
- ];
11885
- var DIRECTION_KEYS = DIRECTION_FIELDS.map((f) => f.key);
11886
- var IMAGE_HINT_MODE_DEFAULT = "full";
11887
- var VIDEO_HINT_MODE_DEFAULT = { look: "full", motion: "compact" };
11888
- function modeForFamily(mode, family) {
11889
- return typeof mode === "string" ? mode : family === "motion" ? mode.motion : mode.look;
11890
- }
11891
- var DIRECTION_ARRAY_CEILING = 8;
11892
- var DIRECTION_ID_MAX_CHARS = 100;
11893
- function normalizeDirectionIds(value, maxPicks) {
11894
- const ids = [];
11895
- if (typeof value === "string") {
11896
- if (value) ids.push(value);
11897
- } else if (Array.isArray(value)) {
11898
- for (const v of value) {
11899
- if (ids.length >= maxPicks) break;
11900
- if (typeof v === "string" && v && !ids.includes(v)) ids.push(v);
11901
- }
11902
- }
11903
- return ids.slice(0, maxPicks);
11904
- }
11905
- function directionFieldsForSurface(surface) {
11906
- return DIRECTION_FIELDS.filter((f) => f.surface === "both" || f.surface === surface);
11907
- }
11908
- function renderDirectionHints(direction, opts) {
11909
- if (!direction) return [];
11910
- const mode = opts.mode ?? "full";
11911
- const out = [];
11912
- const seen = /* @__PURE__ */ new Set();
11913
- for (const spec of DIRECTION_FIELDS) {
11914
- if (spec.surface !== "both" && spec.surface !== opts.surface) continue;
11915
- const ids = normalizeDirectionIds(
11916
- direction[spec.key],
11917
- spec.maxPicks
11918
- );
11919
- if (ids.length === 0) continue;
11920
- for (const hint of spec.render(ids, modeForFamily(mode, spec.family))) {
11921
- if (hint.length > 0 && !seen.has(hint)) {
11922
- seen.add(hint);
11923
- out.push(hint);
11924
- }
11925
- }
11926
- }
11927
- return out;
11928
- }
11929
12042
  var viaFragmentBuilder = (build) => (subject, mode) => {
11930
12043
  const clause = build(subject, mode).filter((s) => s.length > 0).join(", ");
11931
12044
  return clause.length > 0 ? [clause] : [];
@@ -12050,20 +12163,13 @@ function renderSubjectHints(subject, opts) {
12050
12163
  return out;
12051
12164
  }
12052
12165
 
12053
- // src/prompt-hint-join.ts
12054
- var PROMPT_HINT_SEPARATOR = ". ";
12055
- function joinPromptHints(userPrompt, hints) {
12056
- if (hints.length === 0) return userPrompt;
12057
- return [userPrompt.trim(), ...hints].filter((p) => p.length > 0).join(PROMPT_HINT_SEPARATOR);
12058
- }
12059
-
12060
12166
  // src/hint-shedding.ts
12061
- function keepableDirectionHints(hintClauses, kept, deficit) {
12167
+ function keepableDirectionHints(hintClauses, kept, deficit, clauseCosts) {
12062
12168
  let remaining = deficit;
12063
12169
  let next = kept;
12064
12170
  while (next > 0 && remaining > 0) {
12065
12171
  next -= 1;
12066
- remaining -= hintClauses[next].length + PROMPT_HINT_SEPARATOR.length;
12172
+ remaining -= clauseCosts?.[next] ?? hintClauses[next].length + PROMPT_HINT_SEPARATOR.length;
12067
12173
  }
12068
12174
  return next;
12069
12175
  }
@@ -12417,9 +12523,7 @@ function resolveVideoReferenceCore(args) {
12417
12523
  const lockBlock = lockLines.length > 0 ? `${lockLines.join("\n")}
12418
12524
 
12419
12525
  ` : "";
12420
- const trailingBlock = trailingLines.length > 0 ? `
12421
- ${trailingLines.join("\n")}` : "";
12422
- finalPrompt = `${lockBlock}${finalPrompt}${trailingBlock}`;
12526
+ finalPrompt = `${lockBlock}${insertBeforeStyleSection(finalPrompt, trailingLines)}`;
12423
12527
  } else {
12424
12528
  const allFallbackLines = [...fallbackDirectiveLines, ...extraDirectiveLines];
12425
12529
  if (allFallbackLines.length > 0) {
@@ -12656,24 +12760,26 @@ function assembleSunoInput(input) {
12656
12760
  function renderImageHintPieces(subject, direction, structured) {
12657
12761
  return {
12658
12762
  hintClauses: [
12659
- ...renderSubjectHints(subject, {
12660
- surface: "image",
12661
- mode: SUBJECT_IMAGE_HINT_MODE_DEFAULT
12662
- }),
12663
- ...renderDirectionHints(direction, {
12763
+ ...asBodyClauses(
12764
+ renderSubjectHints(subject, {
12765
+ surface: "image",
12766
+ mode: SUBJECT_IMAGE_HINT_MODE_DEFAULT
12767
+ })
12768
+ ),
12769
+ ...partitionStyleClauses(direction, {
12664
12770
  surface: "image",
12665
12771
  mode: IMAGE_HINT_MODE_DEFAULT
12666
12772
  })
12667
- ].filter((p) => p.length > 0),
12773
+ ].filter((c) => c.text.length > 0),
12668
12774
  structuredFragment: structured ? renderStructuredFields(structured) : ""
12669
12775
  };
12670
12776
  }
12671
12777
  function composePromptText(userPrompt, pieces, keptHintClauses) {
12672
- const hints = [
12673
- ...pieces.hintClauses.slice(0, keptHintClauses),
12778
+ return composeSectionedPrompt(
12779
+ userPrompt,
12780
+ pieces.hintClauses.slice(0, keptHintClauses),
12674
12781
  pieces.structuredFragment
12675
- ].filter((p) => p.length > 0);
12676
- return joinPromptHints(userPrompt, hints);
12782
+ );
12677
12783
  }
12678
12784
  function assembleImageInput(input) {
12679
12785
  const pieces = renderImageHintPieces(input.subject, input.direction, input.structured);
@@ -12703,9 +12809,17 @@ function assembleImageInput(input) {
12703
12809
  });
12704
12810
  let kept = pieces.hintClauses.length;
12705
12811
  let fitted = assembleWith(kept);
12706
- while (fitted.overflowChars > 0 && kept > 0) {
12707
- kept = keepableDirectionHints(pieces.hintClauses, kept, fitted.overflowChars);
12708
- fitted = assembleWith(kept);
12812
+ if (fitted.overflowChars > 0) {
12813
+ const costs = sectionedClauseCosts(
12814
+ input.userPrompt,
12815
+ pieces.hintClauses,
12816
+ pieces.structuredFragment
12817
+ );
12818
+ const texts = pieces.hintClauses.map((c) => c.text);
12819
+ while (fitted.overflowChars > 0 && kept > 0) {
12820
+ kept = keepableDirectionHints(texts, kept, fitted.overflowChars, costs);
12821
+ fitted = assembleWith(kept);
12822
+ }
12709
12823
  }
12710
12824
  const { overflowChars, ...result } = fitted;
12711
12825
  if (input.throwOnEmpty && !result.prompt.trim()) {
@@ -12719,31 +12833,30 @@ function assembleImageInput(input) {
12719
12833
  // src/assemble-video-input.ts
12720
12834
  function composeVideoPromptText(userPrompt, direction, structured, opts) {
12721
12835
  const hintClauses = [
12722
- ...renderSubjectHints(opts?.subject, {
12723
- surface: "video",
12724
- mode: opts?.subjectHintMode ?? SUBJECT_VIDEO_HINT_MODE_DEFAULT
12725
- }),
12726
- ...renderDirectionHints(direction, {
12836
+ ...asBodyClauses(
12837
+ renderSubjectHints(opts?.subject, {
12838
+ surface: "video",
12839
+ mode: opts?.subjectHintMode ?? SUBJECT_VIDEO_HINT_MODE_DEFAULT
12840
+ })
12841
+ ),
12842
+ ...partitionStyleClauses(direction, {
12727
12843
  surface: "video",
12728
12844
  mode: opts?.hintMode ?? VIDEO_HINT_MODE_DEFAULT
12729
12845
  })
12730
- ].filter((p) => p.length > 0);
12846
+ ].filter((c) => c.text.length > 0);
12731
12847
  const structuredFragment = structured ? renderStructuredFields(structured) : "";
12732
- const composeWith = (kept2) => {
12733
- const hints = [...hintClauses.slice(0, kept2), structuredFragment].filter(
12734
- (p) => p.length > 0
12735
- );
12736
- if (hints.length === 0) return userPrompt;
12737
- return joinPromptHints(userPrompt ?? "", hints);
12738
- };
12848
+ const composeWith = (kept2) => composeSectionedPrompt(userPrompt, hintClauses.slice(0, kept2), structuredFragment);
12739
12849
  const cap = opts?.cap;
12740
12850
  if (cap === void 0) return composeWith(hintClauses.length);
12741
12851
  const frame = opts?.frame ?? ((body2) => body2);
12742
12852
  let kept = hintClauses.length;
12743
12853
  let body = composeWith(kept);
12744
12854
  let framedLength = frame(body)?.length ?? 0;
12855
+ if (framedLength <= cap) return body;
12856
+ const costs = sectionedClauseCosts(userPrompt, hintClauses, structuredFragment);
12857
+ const texts = hintClauses.map((c) => c.text);
12745
12858
  while (framedLength > cap && kept > 0) {
12746
- kept = keepableDirectionHints(hintClauses, kept, framedLength - cap);
12859
+ kept = keepableDirectionHints(texts, kept, framedLength - cap, costs);
12747
12860
  body = composeWith(kept);
12748
12861
  framedLength = frame(body)?.length ?? 0;
12749
12862
  }
@@ -12920,10 +13033,13 @@ function resolveSeedance2Inputs(args) {
12920
13033
  droppedRefImages
12921
13034
  };
12922
13035
  }
12923
- var GEMINI_OMNI_INPUT_SLOTS = shared.VIDEO_REF_LIMITS_BY_PROVIDER["gemini-omni-video"]?.images ?? 7;
13036
+ var DEFAULT_GEMINI_OMNI_PROVIDER = "gemini-omni-video";
13037
+ function geminiOmniInputSlots(provider) {
13038
+ return shared.VIDEO_REF_LIMITS_BY_PROVIDER[provider ?? DEFAULT_GEMINI_OMNI_PROVIDER]?.images ?? 7;
13039
+ }
12924
13040
  function resolveGeminiOmniI2vInputs(args) {
12925
13041
  const refs = (args.refImageUrls ?? []).filter((u) => typeof u === "string" && u.length > 0);
12926
- const slots = GEMINI_OMNI_INPUT_SLOTS - (args.videoConnected ? 2 : 0);
13042
+ const slots = geminiOmniInputSlots(args.provider) - (args.videoConnected ? 2 : 0);
12927
13043
  const refSlots = Math.max(0, slots - 1);
12928
13044
  const kept = refs.slice(0, refSlots);
12929
13045
  const droppedRefImages = refs.length - kept.length;
@@ -27762,6 +27878,23 @@ var GAPS_SCHEMA = external_exports.object({
27762
27878
  ).max(8).default([])
27763
27879
  }).default({ missingItems: [], missingCategories: [] });
27764
27880
  var MULTI_CACHE = /* @__PURE__ */ new Map();
27881
+ function pickerDisplayName(type) {
27882
+ return type.split("-").map((w) => w.length > 0 ? w[0].toUpperCase() + w.slice(1) : w).join(" ");
27883
+ }
27884
+ function buildOtherPickersLegend(sorted) {
27885
+ const otherTypes = PICKER_TYPES.filter((t) => !sorted.includes(t));
27886
+ if (otherTypes.length === 0) return "";
27887
+ const lines = otherTypes.map((type) => {
27888
+ const descriptor = PICKER_ANALYZER_REGISTRY[type];
27889
+ if (descriptor.kind === "flat") {
27890
+ return `- ${type}: ${descriptor.label}`;
27891
+ }
27892
+ const dims = descriptor.order.map((k) => descriptor.labels[k]).filter(Boolean).join(", ");
27893
+ return `- ${type}: ${pickerDisplayName(type)}${dims ? ` \u2014 ${dims}` : ""}`;
27894
+ });
27895
+ return `Non-wired pickers \u2014 use one of these keys in a gap's \`picker\` when an attribute belongs to it:
27896
+ ${lines.join("\n")}`;
27897
+ }
27765
27898
  function buildMultiPickerAnalyzerSpec(types) {
27766
27899
  const sorted = [...new Set(types)].sort();
27767
27900
  const key = sorted.join(",");
@@ -27779,7 +27912,8 @@ ${buildPickerLegend(spec)}`);
27779
27912
  const result = {
27780
27913
  schema: external_exports.object(shape).strict(),
27781
27914
  toolName: "emit_pickers",
27782
- legend: legendParts.join("\n\n")
27915
+ legend: legendParts.join("\n\n"),
27916
+ otherPickersLegend: buildOtherPickersLegend(sorted)
27783
27917
  };
27784
27918
  MULTI_CACHE.set(key, result);
27785
27919
  return result;
@@ -28185,8 +28319,8 @@ Sources: Google Cloud "Ultimate prompting guide for Veo 3.1"
28185
28319
  KIE VEO API docs (docs.kie.ai/veo3-api/generate-veo-3-video). Captured 2026-08-09.`
28186
28320
  };
28187
28321
  var GEMINI_OMNI_DOCTRINE = {
28188
- providers: ["gemini-omni-video"],
28189
- heading: "Gemini Omni Video (gemini-omni-video)",
28322
+ providers: ["gemini-omni-video", "gemini-omni-flash"],
28323
+ heading: "Gemini Omni (gemini-omni-video, gemini-omni-flash)",
28190
28324
  tips: [
28191
28325
  "Multimodal Google video with native audio: text-to-video, image-to-video, and video-edit through the same prompt surface. 4/6/8/10s; 720p/1080p or 4K tier.",
28192
28326
  "Structure like the platform default: subject \u2192 action \u2192 scene \u2192 lighting \u2192 camera \u2192 style. Quote dialogue lines to have them spoken; describe SFX/ambience plainly in the prompt.",
@@ -28208,6 +28342,7 @@ subject \u2192 action \u2192 scene/environment \u2192 lighting \u2192 camera mov
28208
28342
 
28209
28343
  **Duration & tiers**
28210
28344
  - 4 / 6 / 8 / 10 seconds. 720p/1080p tier or the pricier 4K tier \u2014 pick 4K only when the deliverable needs it (nearly 2\xD7 the credits).
28345
+ - gemini-omni-flash is the faster/cheaper tier with the identical request surface \u2014 same 4/6/8/10s, same 720p/1080p and 4K tiers, same video-edit path. Everything above applies verbatim.
28211
28346
 
28212
28347
  Source: KIE gemini-omni-video market contract (parameters + live behavior probed for the
28213
28348
  aspect-ratio hard-reject, see providers/kie/video.ts). Captured 2026-08-09.`
@@ -28277,6 +28412,45 @@ push-in (intimacy/tension), pull-out (scale/isolation), tracking shot, orbit, fi
28277
28412
  Source: Alibaba Cloud Model Studio \u2014 "Text-to-video / image-to-video prompt guide"
28278
28413
  (alibabacloud.com/help/en/model-studio/text-to-video-prompt). Captured 2026-08-09.`
28279
28414
  };
28415
+ var WAN_3_DOCTRINE = {
28416
+ providers: ["wan-3", "wan-3-prime"],
28417
+ heading: "Wan 3.0 (wan-3, wan-3-prime)",
28418
+ tips: [
28419
+ "Two INPUT MODES, exclusive on the wire: first/last frame, OR reference mode (images + videos + audio). With any reference wired the platform folds the frame into the references and names it in the prompt.",
28420
+ `References bind by ordinal token in array order: Image1, Image2, Video1, Audio1 \u2014 no space, unlike Wan 2.x's "Image 1". Name every wired asset or it may be ignored.`,
28421
+ "Reference caps: 10 images / 5 videos / 5 audio clips; each video and each audio clip 1-15s, with \u226415s combined per array. With reference videos, input seconds + output duration \u2264 30.",
28422
+ "2-30 seconds (default 5); 480p/720p/1080p; aspect adaptive (default, matches the input media) or 16:9 / 4:3 / 1:1 / 3:4 / 9:16. Prompt cap 20,000 chars \u2014 excess is truncated silently.",
28423
+ '`audio` is a boolean, ON by default: the clip comes back with an ambient/SFX track. Cue the sound you want in the prompt, or state the exclusion ("no music") \u2014 it is not a dialogue guarantee.',
28424
+ "wan-3-prime is the HIGH-SPEED tier: identical surface and limits, faster turnaround at a higher per-second rate. It is not a quality upgrade \u2014 choose it for latency, not for looks."
28425
+ ],
28426
+ doctrine: `Prompt structure (no public Wan 3.0 prompt guide exists \u2014 the KIE API contract is the
28427
+ doctrine source, like MiniMax H3 and HappyHorse; platform-standard structure applies):
28428
+ subject \u2192 action \u2192 scene/environment \u2192 lighting \u2192 camera movement \u2192 style \u2192 constraints.
28429
+
28430
+ **Modes (mutually exclusive at the provider)**
28431
+ - Frame mode: first_frame_url, optionally with last_frame_url, and NO references \u2014 the frames anchor the shot exactly, so describe MOTION and camera, not the still.
28432
+ - Reference mode: image / video / audio reference arrays. The provider CANNOT take these together with the first/last frame parameters, so when both are wired the platform folds \u2014 the frame is appended to the reference images (after the caller's own, ordinals unchanged) and bound in the prompt as the opening/closing frame. Write for reference mode whenever a reference is attached.
28433
+ - Text-only runs are supported and are the model's default mode.
28434
+
28435
+ **Reference binding**
28436
+ - Assets bind by ORDINAL TOKEN in array order: Image1, Image2, \u2026, Video1, \u2026, Audio1, \u2026. Note the format has NO space \u2014 Wan 2.x's "Image 1" is a different generation and does not apply here.
28437
+ - Write the binding into the prompt explicitly ("Image1 walks into the room described in Image2"); an unnamed reference may simply be ignored.
28438
+ - Caps: up to 10 images, 5 videos, 5 audio clips. Each video and each audio clip must be 1-15s with \u226415s combined per array. Audio should not be the only media input \u2014 pair it with an image or a video.
28439
+
28440
+ **Duration, resolution, aspect**
28441
+ - 2-30 seconds (provider default 5). With reference videos there is an extra ceiling: input video duration + output duration \u2264 30 seconds.
28442
+ - 480p / 720p / 1080p. Aspect "adaptive" (the default \u2014 the model selects the ratio from the input media and intent) or 16:9 / 4:3 / 1:1 / 3:4 / 9:16. There is no 21:9.
28443
+ - Prompts accept Chinese and English, up to 20,000 characters; anything beyond is truncated silently, so front-load the load-bearing content.
28444
+
28445
+ **Audio**
28446
+ - The "audio" boolean defaults ON and produces an ambient/SFX track with the clip. Describe the soundscape you want plainly ("rain on glass, distant traffic"), or state the exclusion, or turn the toggle off. The contract documents no lip-synced dialogue guarantee \u2014 plan spoken lines as a separate TTS + lip-sync pass.
28447
+
28448
+ **Tiers**
28449
+ - wan-3 and wan-3-prime take identical inputs. Prime trades a higher per-second rate for faster turnaround; it is not documented as a quality tier.
28450
+
28451
+ Source: KIE Wan 3.0 market contract (docs.kie.ai/market/wan/3-0-video,
28452
+ docs.kie.ai/market/wan/3-0-video-prime). Captured 2026-09-01.`
28453
+ };
28280
28454
  var HAPPYHORSE_DOCTRINE = {
28281
28455
  providers: ["happyhorse", "happyhorse-i2v", "happyhorse-ref2v", "happyhorse-edit"],
28282
28456
  heading: "HappyHorse 1.1 (happyhorse, happyhorse-i2v, happyhorse-ref2v)",
@@ -28332,6 +28506,7 @@ var PROVIDER_PROMPT_DOCTRINES = [
28332
28506
  GEMINI_OMNI_DOCTRINE,
28333
28507
  GROK_IMAGINE_DOCTRINE,
28334
28508
  WAN_DOCTRINE,
28509
+ WAN_3_DOCTRINE,
28335
28510
  HAPPYHORSE_DOCTRINE,
28336
28511
  RUNWAY_KIE_DOCTRINE
28337
28512
  ];
@@ -28556,6 +28731,9 @@ var PROVIDER_CAPABILITIES = {
28556
28731
  "ltx-2.3-pro": "Lightricks LTX 2.3 Pro \u2014 text/image/audio\u2192video, 6\u201310s, up to 4K",
28557
28732
  "ltx-2.3-fast": "Lightricks LTX 2.3 Fast \u2014 text/image\u2192video, 6\u201320s, up to 4K",
28558
28733
  "gemini-omni-video": "Google Gemini Omni \u2014 multimodal video with native audio, 4\u201310s, up to 4K.",
28734
+ "gemini-omni-flash": "Google Gemini Omni Flash \u2014 faster, cheaper Omni tier; multimodal video with native audio, 4\u201310s, up to 4K.",
28735
+ "wan-3": "Wan 3.0 \u2014 multimodal refs (10 images / 5 videos / 5 audio) or first+last frame, native audio, 2\u201330s, 480p/720p/1080p",
28736
+ "wan-3-prime": "Wan 3.0 Prime \u2014 high-speed Wan 3.0 tier; same surface, faster turnaround at a higher rate",
28559
28737
  "grok-imagine-video-1.5": "Grok Imagine 1.5 \u2014 image-to-video only; requires an input image"
28560
28738
  },
28561
28739
  "image-to-video": {
@@ -28589,6 +28767,9 @@ var PROVIDER_CAPABILITIES = {
28589
28767
  "ltx-2.3-pro": "Lightricks LTX 2.3 Pro \u2014 start/end frame i2v + audio\u2192video, 6\u201310s, up to 4K",
28590
28768
  "ltx-2.3-fast": "Lightricks LTX 2.3 Fast \u2014 start/end frame i2v, 6\u201320s, up to 4K",
28591
28769
  "gemini-omni-video": "Google Gemini Omni \u2014 multimodal video with native audio, 4\u201310s, up to 4K.",
28770
+ "gemini-omni-flash": "Google Gemini Omni Flash \u2014 faster, cheaper Omni tier; multimodal video with native audio, 4\u201310s, up to 4K.",
28771
+ "wan-3": "Wan 3.0 \u2014 multimodal refs (10 images / 5 videos / 5 audio) or first+last frame, native audio, 2\u201330s, 480p/720p/1080p",
28772
+ "wan-3-prime": "Wan 3.0 Prime \u2014 high-speed Wan 3.0 tier; same surface, faster turnaround at a higher rate",
28592
28773
  "grok-imagine-video-1.5": "Grok Imagine 1.5 \u2014 stylized animation, 1\u201315s, 480p/720p (image required)"
28593
28774
  },
28594
28775
  "video-to-video": {
@@ -34121,6 +34302,7 @@ exports.EXPOSURE_SETTINGS = EXPOSURE_SETTINGS;
34121
34302
  exports.FACTORY_PRESETS = FACTORY_PRESETS;
34122
34303
  exports.FACTORY_SNIPPETS = FACTORY_SNIPPETS;
34123
34304
  exports.FILM_STILL_PREFIX = FILM_STILL_PREFIX;
34305
+ exports.FILM_STYLE_KEYS = FILM_STYLE_KEYS;
34124
34306
  exports.FRAMINGS = FRAMINGS;
34125
34307
  exports.FRAMING_CATEGORY_LABELS = FRAMING_CATEGORY_LABELS;
34126
34308
  exports.FRAMING_CATEGORY_ORDER = FRAMING_CATEGORY_ORDER;
@@ -34231,6 +34413,8 @@ exports.SNIPPET_MEDIA_VALUES = SNIPPET_MEDIA_VALUES;
34231
34413
  exports.STYLES = STYLES;
34232
34414
  exports.STYLE_IDS = STYLE_IDS;
34233
34415
  exports.STYLE_PRESETS = STYLE_PRESETS;
34416
+ exports.STYLE_SECTION_GAP = STYLE_SECTION_GAP;
34417
+ exports.STYLE_SECTION_HEADER = STYLE_SECTION_HEADER;
34234
34418
  exports.STYLINGS = STYLINGS;
34235
34419
  exports.STYLING_DIMENSION_LABELS = STYLING_DIMENSION_LABELS;
34236
34420
  exports.STYLING_DIMENSION_ORDER = STYLING_DIMENSION_ORDER;
@@ -34283,6 +34467,7 @@ exports.applyPickerJson = applyPickerJson;
34283
34467
  exports.applyPromptAffixes = applyPromptAffixes;
34284
34468
  exports.applyReferenceOrderToVideo = applyReferenceOrderToVideo;
34285
34469
  exports.applyTemplate = applyTemplate;
34470
+ exports.asBodyClauses = asBodyClauses;
34286
34471
  exports.assembleImageInput = assembleImageInput;
34287
34472
  exports.assembleSunoInput = assembleSunoInput;
34288
34473
  exports.buildActionFxHints = buildActionFxHints;
@@ -34356,6 +34541,7 @@ exports.composeCameraMotionTermFromConnections = composeCameraMotionTermFromConn
34356
34541
  exports.composeCharacterFxHintFromConnections = composeCharacterFxHintFromConnections;
34357
34542
  exports.composeNegative = composeNegative;
34358
34543
  exports.composePickerCatalogs = composePickerCatalogs;
34544
+ exports.composeSectionedPrompt = composeSectionedPrompt;
34359
34545
  exports.composeSoundHintFromConnections = composeSoundHintFromConnections;
34360
34546
  exports.composeTransitionHintFromConnections = composeTransitionHintFromConnections;
34361
34547
  exports.composeVideoPromptText = composeVideoPromptText;
@@ -34364,6 +34550,7 @@ exports.computeNodePrompt = computeNodePrompt;
34364
34550
  exports.computePackSidecarCoverage = computePackSidecarCoverage;
34365
34551
  exports.deriveTerm = deriveTerm;
34366
34552
  exports.directionFieldsForSurface = directionFieldsForSurface;
34553
+ exports.endsInsideStyleSection = endsInsideStyleSection;
34367
34554
  exports.expandImagePositionRefs = expandImagePositionRefs;
34368
34555
  exports.expandImageRefTokens = expandImageRefTokens;
34369
34556
  exports.filmStillPrefix = filmStillPrefix;
@@ -34550,6 +34737,7 @@ exports.getWardrobePromptHint = getWardrobePromptHint;
34550
34737
  exports.groupFactoryPresets = groupFactoryPresets;
34551
34738
  exports.hasUpstreamCharacter = hasUpstreamCharacter;
34552
34739
  exports.identityRefsSentence = identityRefsSentence;
34740
+ exports.insertBeforeStyleSection = insertBeforeStyleSection;
34553
34741
  exports.isAnalyzablePicker = isAnalyzablePicker;
34554
34742
  exports.isInstrumentalVocal = isInstrumentalVocal;
34555
34743
  exports.isSuspiciousDerivedTerm = isSuspiciousDerivedTerm;
@@ -34565,6 +34753,7 @@ exports.nodeHasInlinePrompt = nodeHasInlinePrompt;
34565
34753
  exports.nodeHasPromptField = nodeHasPromptField;
34566
34754
  exports.nodeSupportsPromptAffixes = nodeSupportsPromptAffixes;
34567
34755
  exports.normalizeSubjectFields = normalizeSubjectFields;
34756
+ exports.partitionStyleClauses = partitionStyleClauses;
34568
34757
  exports.personPacksVersion = personPacksVersion;
34569
34758
  exports.pickerFanoutTargets = pickerFanoutTargets;
34570
34759
  exports.projectAllCatalogs = projectAllCatalogs;
@@ -34579,8 +34768,10 @@ exports.readSubjectFields = readSubjectFields;
34579
34768
  exports.referenceRulesBlock = referenceRulesBlock;
34580
34769
  exports.registerCatalogPack = registerCatalogPack;
34581
34770
  exports.registerPersonPack = registerPersonPack;
34771
+ exports.renderDirectionHintClauses = renderDirectionHintClauses;
34582
34772
  exports.renderDirectionHints = renderDirectionHints;
34583
34773
  exports.renderStructuredFields = renderStructuredFields;
34774
+ exports.renderStyleSection = renderStyleSection;
34584
34775
  exports.renderSubjectHints = renderSubjectHints;
34585
34776
  exports.resetCatalogPacks = resetCatalogPacks;
34586
34777
  exports.resetPersonPacks = resetPersonPacks;
@@ -34596,6 +34787,10 @@ exports.resolveTemplate = resolveTemplate;
34596
34787
  exports.resolveTerm = resolveTerm;
34597
34788
  exports.resolveVeoI2vInputs = resolveVeoI2vInputs;
34598
34789
  exports.resolveVideoReferenceCore = resolveVideoReferenceCore;
34790
+ exports.sectionedClauseCosts = sectionedClauseCosts;
34791
+ exports.splitStyleSection = splitStyleSection;
34792
+ exports.styleSectionFromClauses = styleSectionFromClauses;
34793
+ exports.styleSlotFor = styleSlotFor;
34599
34794
  exports.subjectFieldsForSurface = subjectFieldsForSurface;
34600
34795
  exports.summarizePickerCatalogs = summarizePickerCatalogs;
34601
34796
  exports.toIdentityLockMode = toIdentityLockMode;