@nodaro/shared 3.11.0 → 3.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/index.cjs +2047 -84
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1556 -27
  4. package/dist/index.d.ts +1556 -27
  5. package/dist/index.js +1861 -85
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/__tests__/caption-styles.test.ts +207 -0
  9. package/src/__tests__/edl-multicam.test.ts +304 -0
  10. package/src/__tests__/edl.test.ts +822 -0
  11. package/src/__tests__/fan-out-rows.test.ts +208 -0
  12. package/src/__tests__/instagram-scrape.test.ts +66 -0
  13. package/src/__tests__/llm-models.test.ts +48 -11
  14. package/src/__tests__/meta-ads-scrape.test.ts +284 -0
  15. package/src/__tests__/node-runtime-keys.test.ts +15 -0
  16. package/src/__tests__/presentation-utils.test.ts +67 -0
  17. package/src/__tests__/producer-types.test.ts +19 -0
  18. package/src/__tests__/schedule-rules.test.ts +265 -0
  19. package/src/__tests__/speaker-layouts.test.ts +203 -0
  20. package/src/__tests__/transcribe-capabilities.test.ts +104 -0
  21. package/src/__tests__/transcribe-preflight.test.ts +60 -0
  22. package/src/__tests__/trigger-feeds.test.ts +39 -0
  23. package/src/__tests__/video-duration-auto.test.ts +65 -0
  24. package/src/__tests__/video-duration.test.ts +56 -0
  25. package/src/__tests__/video-link.test.ts +137 -0
  26. package/src/__tests__/workflow-export-strip.test.ts +59 -1
  27. package/src/caption-styles.ts +240 -0
  28. package/src/credit-identifiers.ts +31 -0
  29. package/src/edit-plan-contract.ts +96 -0
  30. package/src/edl-multicam.ts +185 -0
  31. package/src/edl.ts +747 -0
  32. package/src/entity-image-handle.ts +24 -1
  33. package/src/fan-out-rows.ts +213 -0
  34. package/src/i18n/action-fx.he.ts +8 -8
  35. package/src/i18n/aesthetic.he.ts +8 -8
  36. package/src/i18n/animals.he.ts +34 -34
  37. package/src/i18n/atmosphere.he.ts +6 -6
  38. package/src/i18n/backdrop.he.ts +7 -7
  39. package/src/i18n/camera-format.he.ts +4 -4
  40. package/src/i18n/camera-motions.he.ts +20 -20
  41. package/src/i18n/character-fx.he.ts +16 -16
  42. package/src/i18n/character-motion.he.ts +184 -184
  43. package/src/i18n/color-look.he.ts +4 -4
  44. package/src/i18n/composition-effects.he.ts +3 -3
  45. package/src/i18n/era.he.ts +9 -9
  46. package/src/i18n/exposure-settings.he.ts +2 -2
  47. package/src/i18n/framing.he.ts +13 -13
  48. package/src/i18n/furniture.he.ts +52 -52
  49. package/src/i18n/held-prop.he.ts +10 -10
  50. package/src/i18n/instrumentation.he.ts +26 -26
  51. package/src/i18n/lens.he.ts +6 -6
  52. package/src/i18n/lighting.he.ts +17 -17
  53. package/src/i18n/loop-subject.he.ts +4 -4
  54. package/src/i18n/materials.he.ts +8 -8
  55. package/src/i18n/mood.he.ts +12 -12
  56. package/src/i18n/music-genre.he.ts +9 -9
  57. package/src/i18n/music-mood.he.ts +7 -7
  58. package/src/i18n/person.he.ts +94 -94
  59. package/src/i18n/photo-genre.he.ts +24 -24
  60. package/src/i18n/photographer.he.ts +21 -21
  61. package/src/i18n/pose.he.ts +14 -14
  62. package/src/i18n/post-process-effects.he.ts +4 -4
  63. package/src/i18n/render-quality.he.ts +7 -7
  64. package/src/i18n/setting.he.ts +14 -14
  65. package/src/i18n/style.he.ts +9 -9
  66. package/src/i18n/styling.he.ts +126 -126
  67. package/src/i18n/temporal.he.ts +8 -8
  68. package/src/i18n/transitions.he.ts +9 -9
  69. package/src/i18n/vehicles.he.ts +25 -25
  70. package/src/i18n/voice-character.he.ts +14 -14
  71. package/src/i18n/voice-delivery.he.ts +11 -11
  72. package/src/i18n/weapons.he.ts +23 -23
  73. package/src/index.ts +206 -3
  74. package/src/instagram-scrape.ts +204 -0
  75. package/src/llm-models.ts +80 -3
  76. package/src/meta-ads-scrape.ts +463 -0
  77. package/src/model-catalog.ts +48 -5
  78. package/src/model-constants.ts +148 -5
  79. package/src/node-mappable-fields.ts +2 -0
  80. package/src/node-runtime-keys.ts +28 -0
  81. package/src/presentation-utils.ts +49 -0
  82. package/src/producer-types.ts +20 -0
  83. package/src/schedule-rules.ts +484 -0
  84. package/src/speaker-layouts.ts +220 -0
  85. package/src/transcribe-preflight.ts +101 -0
  86. package/src/trigger-feeds.ts +59 -0
  87. package/src/trigger-node-types.ts +20 -0
  88. package/src/video-duration-auto.ts +18 -0
  89. package/src/video-duration.ts +32 -0
  90. package/src/video-link.ts +167 -0
  91. package/src/workflow-export.ts +37 -1
package/src/index.ts CHANGED
@@ -71,6 +71,10 @@ export {
71
71
  TEXT_TO_VIDEO_PROVIDERS,
72
72
  VIDEO_GEN_PROVIDERS,
73
73
  VIDEO_TO_VIDEO_PROVIDERS,
74
+ SEEDANCE_VIDEO_EDIT_PROVIDERS,
75
+ isSeedanceVideoEditProvider,
76
+ VIDEO_TO_VIDEO_NODE_PROVIDERS,
77
+ SEEDANCE_VIDEO_EDIT_SHAPE,
74
78
  FACE_SWAP_PROVIDERS,
75
79
  VIDEO_UPSCALE_PROVIDERS,
76
80
  EXTEND_VIDEO_PROVIDERS,
@@ -85,6 +89,12 @@ export {
85
89
  TEXT_TO_AUDIO_PROVIDERS,
86
90
  MUSIC_PROVIDERS,
87
91
  TRANSCRIBE_PROVIDERS,
92
+ TRANSCRIBE_LANES,
93
+ TRANSCRIBE_PROVIDER_CAPABILITIES,
94
+ transcribeProvidersWithWordTimestamps,
95
+ transcribeLaneSupportsWordTimestamps,
96
+ DEFAULT_TRANSCRIBE_PROVIDER,
97
+ DEFAULT_TRANSCRIBE_NODE_PROVIDER,
88
98
  SCRIPT_PROVIDERS,
89
99
  AI_WRITER_PROVIDERS,
90
100
  QA_CHECK_PROVIDERS,
@@ -168,6 +178,8 @@ export {
168
178
  applyDefaultVideoSelection,
169
179
  PRICING_DEFAULT_DURATION_SEC,
170
180
  pricedOutputDurationSec,
181
+ supportsAutoVideoDuration,
182
+ maxVideoDurationSec,
171
183
  PRICING_DEFAULT_RESOLUTION,
172
184
  } from "./model-constants.js"
173
185
 
@@ -196,6 +208,8 @@ export type {
196
208
  VideoGenProvider,
197
209
  VideoModeAlias,
198
210
  VideoToVideoProvider,
211
+ SeedanceVideoEditProvider,
212
+ VideoToVideoNodeProvider,
199
213
  VideoUpscaleProvider,
200
214
  ExtendVideoProvider,
201
215
  FaceSwapProvider,
@@ -205,6 +219,7 @@ export type {
205
219
  TextToAudioProvider,
206
220
  MusicProvider,
207
221
  TranscribeProvider,
222
+ TranscribeLane,
208
223
  ScriptProvider,
209
224
  AiWriterProvider,
210
225
  QaCheckProvider,
@@ -251,6 +266,7 @@ export {
251
266
  resolveImageGenCreditIdentifier,
252
267
  resolveNormalizedImageGen,
253
268
  buildVideoCreditModelIdentifier,
269
+ seedanceVideoEditCreditId,
254
270
  pricedVideoSelection,
255
271
  buildMotionCreditModelIdentifier,
256
272
  sunoCreditType,
@@ -261,7 +277,7 @@ export {
261
277
  export type { NormalizedImageGen, PricedVideoSelection } from "./credit-identifiers.js"
262
278
 
263
279
  export * from "./credit-estimators/index.js"
264
- export { extractVideoDurationFromNode } from "./video-duration.js"
280
+ export { extractVideoDurationFromNode, editPlanSourceDurationSec } from "./video-duration.js"
265
281
 
266
282
  export {
267
283
  resolveTopazUpscale,
@@ -284,6 +300,7 @@ export {
284
300
  getNodeResult,
285
301
  getNodeLabel,
286
302
  getInputFieldSchema,
303
+ mergeNodeInputOverrides,
287
304
  flattenItems,
288
305
  migrateToItems,
289
306
  validateNoNestedGroups,
@@ -357,6 +374,8 @@ export {
357
374
  ADVANCED_MODE_UNAVAILABLE_REASON,
358
375
  motionGraphicsFeature,
359
376
  effectiveReasoningEffort,
377
+ REASONING_OUTPUT_FLOOR,
378
+ reasoningOutputFloor,
360
379
  type LlmTier,
361
380
  type LlmFeature,
362
381
  type KieApiFormat,
@@ -426,6 +445,18 @@ export type {
426
445
 
427
446
  export { REPEATABLE_NODE_TYPES, REPEAT_PLACEHOLDER, PROVIDER_PLACEHOLDER_PREFIX, encodeProviderItem, decodeProviderItem, getEffectiveRepeatCount, expandItemsWithRepeat } from "./repeat-types.js"
428
447
 
448
+ export {
449
+ NON_PROMPT_TEXT_LANES,
450
+ fanOutTextFeedsPrompt,
451
+ isFanOutUrlItem,
452
+ compactWithRows,
453
+ liveRowColumn,
454
+ resolveListFanOut,
455
+ planFanOut,
456
+ alignedFieldList,
457
+ } from "./fan-out-rows.js"
458
+ export type { FanOutCandidate, ListFanOut, FanOutPlan } from "./fan-out-rows.js"
459
+
429
460
  export { settledWithLimit } from "./settled-with-limit.js"
430
461
 
431
462
 
@@ -611,6 +642,39 @@ export {
611
642
  } from "./node-default-mappings.js"
612
643
 
613
644
  export { NODE_MAPPABLE_FIELDS, SUNO_FIELD_HANDLE_FIELDS, fieldKeyFromHandle } from "./node-mappable-fields.js"
645
+ export {
646
+ SCHEDULE_TRIGGER_NODE_TYPE,
647
+ WEBHOOK_TRIGGER_NODE_TYPE,
648
+ TELEGRAM_TRIGGER_NODE_TYPE,
649
+ PROJECTED_TRIGGER_NODE_TYPES,
650
+ isProjectedTriggerNodeType,
651
+ } from "./trigger-node-types.js"
652
+
653
+ export { buildFeedMaps, nodeFeedsAnything, type FeedNode, type FeedEdge, type FeedMaps } from "./trigger-feeds.js"
654
+
655
+ export {
656
+ SCHEDULE_RULE_KINDS,
657
+ SCHEDULE_EVERY_LIMITS,
658
+ isCronExpression,
659
+ isValidTimezone,
660
+ normalizeScheduleRule,
661
+ normalizeScheduleRules,
662
+ legacyScheduleToRules,
663
+ localTimeIn,
664
+ localMinuteKey,
665
+ timezoneOffsetMinutes,
666
+ matchesCronField,
667
+ matchesCron,
668
+ ruleMatches,
669
+ scheduleMatchesAt,
670
+ scheduleOccurrences,
671
+ nextScheduleRuns,
672
+ previewHorizonMs,
673
+ type ScheduleRuleKind,
674
+ type ScheduleRule,
675
+ type ScheduleSpec,
676
+ type LocalTime,
677
+ } from "./schedule-rules.js"
614
678
 
615
679
 
616
680
  export {
@@ -622,6 +686,101 @@ export {
622
686
  type ScraperActorId,
623
687
  } from "./scraper-actors.js"
624
688
 
689
+ export {
690
+ META_ADS_SCRAPE_NODE_TYPE,
691
+ META_ADS_SCRAPE_MODES,
692
+ META_ADS_SCRAPE_PERIODS,
693
+ META_ADS_SCRAPE_STATUSES,
694
+ META_ADS_PLATFORMS,
695
+ isMetaAdsPlatform,
696
+ type MetaAdsPlatform,
697
+ META_ADS_FORMATS,
698
+ isMetaAdsFormat,
699
+ classifyCreativeFormat,
700
+ clampMetaAdsFeaturedIndex,
701
+ featuredMetaAdOutputs,
702
+ type FeaturedMetaAdOutputs,
703
+ type MetaAdsFormat,
704
+ type MetaAdsCreativeFormat,
705
+ META_ADS_SCRAPE_COUNT_OPTIONS,
706
+ META_ADS_SCRAPE_DEFAULT_COUNT,
707
+ META_ADS_SCRAPE_MAX_COUNT,
708
+ META_ADS_SCRAPE_MAX_SOURCES,
709
+ META_ADS_SCRAPE_MAX_QUERY_LENGTH,
710
+ META_ADS_SCRAPE_DEFAULT_COUNTRY,
711
+ META_ADS_SCRAPE_TIERS,
712
+ META_ADS_SCRAPE_CREDIT_COSTS,
713
+ META_ADS_SCRAPE_FALLBACK_CREDIT_ID,
714
+ splitMetaAdsPageUrls,
715
+ isMetaAdsScrapeMode,
716
+ isMetaAdsScrapeCount,
717
+ metaAdsScrapeTier,
718
+ buildMetaAdsScrapeCreditId,
719
+ resolveMetaAdsScrapeCreditId,
720
+ META_ADS_NODE_MODES,
721
+ META_ADS_ADVERTISER_MAX_RESULTS,
722
+ META_ADS_ANALYSIS_TIERS,
723
+ META_ADS_ANALYSIS_CREDITS_PER_AD,
724
+ META_ADS_ANALYSIS_CREDIT_ID,
725
+ META_ADS_ANALYSIS_FOCUS_MAX,
726
+ metaAdsAnalysisCreditId,
727
+ metaAdsAnalysisTier,
728
+ metaAdsAnalysisTierFrom,
729
+ adCreativeAnalysisFrom,
730
+ metaAdsScrapeCreditIdFromNode,
731
+ type MetaAdsAnalysisTier,
732
+ type AdCreativeAnalysis,
733
+ type MetaAdsNodeQuoteFields,
734
+ metaAdsNodeMode,
735
+ isFacebookPageUrl,
736
+ isMetaCdnImageUrl,
737
+ splitMetaAdsAdvertiserNames,
738
+ metaAdsAdvertisersFrom,
739
+ metaAdsScrapeSources,
740
+ metaAdsScrapeWireSources,
741
+ type MetaAdsNodeMode,
742
+ type MetaAdsAdvertiser,
743
+ type MetaAdsNodeSourceFields,
744
+ type MetaAdsWireSources,
745
+ type MetaAdsScrapeMode,
746
+ type MetaAdsScrapePeriod,
747
+ type MetaAdsScrapeStatus,
748
+ type MetaAdsScrapeTier,
749
+ } from "./meta-ads-scrape.js"
750
+
751
+ export {
752
+ INSTAGRAM_SCRAPE_NODE_TYPE,
753
+ INSTAGRAM_SCRAPE_MODES,
754
+ INSTAGRAM_SCRAPE_PERIODS,
755
+ INSTAGRAM_SCRAPE_DEFAULT_COUNT,
756
+ INSTAGRAM_SCRAPE_MAX_COUNT,
757
+ INSTAGRAM_SCRAPE_MAX_SOURCES,
758
+ INSTAGRAM_SCRAPE_MAX_TARGET_LENGTH,
759
+ INSTAGRAM_SCRAPE_TIERS,
760
+ INSTAGRAM_SCRAPE_CREDIT_COSTS,
761
+ INSTAGRAM_SCRAPE_FALLBACK_CREDIT_ID,
762
+ INSTAGRAM_ANALYSIS_CREDIT_ID,
763
+ isInstagramScrapeMode,
764
+ instagramScrapeMode,
765
+ isInstagramScrapeCount,
766
+ instagramScrapeTier,
767
+ instagramAnalysisTierFrom,
768
+ instagramAnalysisCreditId,
769
+ buildInstagramScrapeCreditId,
770
+ resolveInstagramScrapeCreditId,
771
+ instagramScrapeSources,
772
+ instagramScrapeCreditIdFromNode,
773
+ splitInstagramTargets,
774
+ clampInstagramFeaturedIndex,
775
+ featuredInstagramOutputs,
776
+ type InstagramScrapeMode,
777
+ type InstagramScrapePeriod,
778
+ type InstagramScrapeTier,
779
+ type InstagramFormat,
780
+ type FeaturedInstagramOutputs,
781
+ type InstagramNodeQuoteFields,
782
+ } from "./instagram-scrape.js"
783
+
625
784
  export { VARIABLES_HANDLE_ID, buildConditionVariables } from "./condition-variables.js"
626
785
 
627
786
  export { extractAllGeneratedResults, extractGeneratedJsonAsList, spreadJsonArrayIfSingleton } from "./generated-results.js"
@@ -782,8 +941,27 @@ export {
782
941
  KINETIC_CAPTION_STYLES,
783
942
  ALL_CAPTION_STYLES,
784
943
  isKineticCaptionStyle,
944
+ CAPTION_LOOK_IDS,
945
+ CAPTION_LOOKS,
946
+ DEFAULT_CAPTION_LOOK,
947
+ DEFAULT_SUBTITLE_LOOK,
948
+ KINETIC_ONLY_CAPTION_LEVER_KEYS,
949
+ captionRoutesToRemotion,
950
+ CAPTION_MAX_WORDS_PER_LINE_MIN,
951
+ CAPTION_MAX_WORDS_PER_LINE_MAX,
952
+ CAPTION_LEVER_BOUNDS,
953
+ normalizeCaptionNumericLevers,
954
+ autoStrokeWidth,
955
+ resolveCaptionLook,
956
+ resolveCaptionLevers,
785
957
  } from "./caption-styles.js"
786
- export type { StaticCaptionStyle, KineticCaptionStyle, CaptionStyle } from "./caption-styles.js"
958
+ export type { StaticCaptionStyle, KineticCaptionStyle, CaptionStyle, CaptionLookId, CaptionLookLevers, KineticOnlyCaptionLeverKey } from "./caption-styles.js"
959
+
960
+ export {
961
+ transcribeWordTimestampsRefusal,
962
+ findWordlessTranscriptFeeds,
963
+ } from "./transcribe-preflight.js"
964
+ export type { PreflightGraphNode, PreflightGraphEdge, WordlessTranscriptFeed } from "./transcribe-preflight.js"
787
965
 
788
966
  // Sound parameter-node dimensions (music + voice pickers + backend hints)
789
967
 
@@ -822,7 +1000,7 @@ export type {
822
1000
  WorkflowImportSkippedAsset,
823
1001
  WorkflowAssetKind,
824
1002
  } from "./workflow-export.js"
825
- export { stripExportContent } from "./workflow-export.js"
1003
+ export { stripExportContent, stripUnownedRefs } from "./workflow-export.js"
826
1004
 
827
1005
  export { validateSubWorkflowRoutes } from "./sub-workflow-validation.js"
828
1006
 
@@ -972,6 +1150,22 @@ export {
972
1150
 
973
1151
  export { SUNO_TRACK_SOURCE_TYPES } from "./suno-track-sources.js"
974
1152
 
1153
+ // --- Video URL node + social-video import (host allowlist, node output rule) ---
1154
+ export {
1155
+ SOCIAL_VIDEO_HOSTS,
1156
+ YOUTUBE_HOSTS,
1157
+ INSTAGRAM_HOSTS,
1158
+ VIDEO_LINK_TOLERANT_CONSUMER_TYPES,
1159
+ hostnameMatchesAllowlist,
1160
+ hasUrlParserHazard,
1161
+ isSocialVideoUrl,
1162
+ detectVideoLinkPlatform,
1163
+ videoLinkDownloadedFile,
1164
+ resolveVideoLinkOutput,
1165
+ videoLinkNeedsDownload,
1166
+ } from "./video-link.js"
1167
+ export type { VideoLinkPlatform, VideoLinkNodeFields } from "./video-link.js"
1168
+
975
1169
  export {
976
1170
  VOICE_CHANGER_MODELS,
977
1171
  VOICE_CHANGER_MODEL_IDS,
@@ -1114,3 +1308,12 @@ export * from "./scene3d-input-assets.js"
1114
1308
  export * from "./video-output-canvas.js"
1115
1309
  export * from "./video-frame-fit.js"
1116
1310
  export type { CharacterMotionMetadata } from "./character-motion-metadata.js"
1311
+
1312
+ // --- EDL: the edit decision list contract (podcast editing primitives).
1313
+ // Types + validators + pure remap/duration functions; structural only. ---
1314
+ export * from "./edl.js"
1315
+ export * from "./speaker-layouts.js"
1316
+ export * from "./edl-multicam.js"
1317
+ export * from "./edit-plan-contract.js"
1318
+
1319
+ export { VIDEO_DURATION_AUTO, isAutoVideoDuration } from "./video-duration-auto.js"
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Instagram scraper node — shared vocabulary + credit identifiers.
3
+ *
4
+ * Pulls PUBLIC Instagram posts (feed images, carousels, reels) by profile or
5
+ * by hashtag and emits a normalized JSON array. Same shape of contract as the
6
+ * Meta Ads node: everything the backend guard/reservation, the frontend credit
7
+ * badge and the docs formula must agree on lives here.
8
+ *
9
+ * Pricing: 1 credit per REQUESTED post, rounded UP to a fixed tier of
10
+ * `count × sources` (a profile / hashtag is one source, up to 5). Optional
11
+ * per-post AI analysis folds into the same identifier, priced by the model's
12
+ * tier — reusing the Meta analysis per-item values so the two nodes stay in
13
+ * lockstep.
14
+ */
15
+ import {
16
+ META_ADS_ANALYSIS_CREDITS_PER_AD,
17
+ META_ADS_ANALYSIS_TIERS,
18
+ metaAdsAnalysisTier,
19
+ type MetaAdsAnalysisTier,
20
+ } from "./meta-ads-scrape.js"
21
+ import { classifyCreativeFormat, type MetaAdsFormat } from "./meta-ads-scrape.js"
22
+
23
+ export const INSTAGRAM_SCRAPE_NODE_TYPE = "instagram-scrape" as const
24
+
25
+ export const INSTAGRAM_SCRAPE_MODES = ["profile", "hashtag"] as const
26
+ export type InstagramScrapeMode = (typeof INSTAGRAM_SCRAPE_MODES)[number]
27
+
28
+ export function isInstagramScrapeMode(value: unknown): value is InstagramScrapeMode {
29
+ return typeof value === "string" && (INSTAGRAM_SCRAPE_MODES as readonly string[]).includes(value)
30
+ }
31
+ export function instagramScrapeMode(value: unknown): InstagramScrapeMode {
32
+ return isInstagramScrapeMode(value) ? value : "profile"
33
+ }
34
+
35
+ /** Same window vocabulary as Meta Ads; the Instagram actor honours it server-side (`onlyPostsNewerThan`). */
36
+ export const INSTAGRAM_SCRAPE_PERIODS = ["24h", "7d", "30d", "all"] as const
37
+ export type InstagramScrapePeriod = (typeof INSTAGRAM_SCRAPE_PERIODS)[number]
38
+
39
+ export const INSTAGRAM_SCRAPE_DEFAULT_COUNT = 20
40
+ export const INSTAGRAM_SCRAPE_MAX_COUNT = 100
41
+ export const INSTAGRAM_SCRAPE_MAX_SOURCES = 5
42
+ /** Instagram usernames / hashtags are short; cap a single target well under a URL. */
43
+ export const INSTAGRAM_SCRAPE_MAX_TARGET_LENGTH = 200
44
+
45
+ /** Requested-total buckets — identical shape to Meta (the pricing model is the same). */
46
+ export const INSTAGRAM_SCRAPE_TIERS = [10, 20, 50, 100, 200, 500] as const
47
+ export type InstagramScrapeTier = (typeof INSTAGRAM_SCRAPE_TIERS)[number]
48
+
49
+ export function instagramScrapeTier(requestedTotal: number): InstagramScrapeTier {
50
+ for (const tier of INSTAGRAM_SCRAPE_TIERS) if (requestedTotal <= tier) return tier
51
+ return INSTAGRAM_SCRAPE_TIERS[INSTAGRAM_SCRAPE_TIERS.length - 1]
52
+ }
53
+
54
+ /** The analysis tier a request / node asks for, or null when analysis is off. */
55
+ export function instagramAnalysisTierFrom(data: { readonly analyze?: unknown; readonly analysisModel?: unknown }): MetaAdsAnalysisTier | null {
56
+ return data.analyze === true ? metaAdsAnalysisTier(data.analysisModel) : null
57
+ }
58
+
59
+ function analysisSuffix(tier: MetaAdsAnalysisTier): string {
60
+ return tier === "standard" ? ":analysis" : `:analysis:${tier}`
61
+ }
62
+
63
+ /** Per-post settlement SKU for a tier (the bare id is the standard tier). */
64
+ export const INSTAGRAM_ANALYSIS_CREDIT_ID = "instagram-analysis" as const
65
+ export function instagramAnalysisCreditId(tier: MetaAdsAnalysisTier): string {
66
+ return tier === "standard" ? INSTAGRAM_ANALYSIS_CREDIT_ID : `${INSTAGRAM_ANALYSIS_CREDIT_ID}:${tier}`
67
+ }
68
+
69
+ export interface InstagramScrapeCreditInput {
70
+ count: number
71
+ sources: number
72
+ analysis?: MetaAdsAnalysisTier | null
73
+ }
74
+
75
+ export function buildInstagramScrapeCreditId(input: InstagramScrapeCreditInput): string {
76
+ const sources = Math.min(Math.max(Math.trunc(input.sources) || 1, 1), INSTAGRAM_SCRAPE_MAX_SOURCES)
77
+ const count = Math.min(Math.max(Math.trunc(input.count) || 1, 1), INSTAGRAM_SCRAPE_MAX_COUNT)
78
+ const base = `${INSTAGRAM_SCRAPE_NODE_TYPE}:${instagramScrapeTier(count * sources)}`
79
+ return input.analysis ? `${base}${analysisSuffix(input.analysis)}` : base
80
+ }
81
+
82
+ /**
83
+ * Cost per SKU — mirror of the backend `STATIC_CREDIT_COSTS` rows / migration,
84
+ * for the frontend badge / estimator. 1 credit per requested post at every
85
+ * tier, plus the analysis multiples.
86
+ */
87
+ export const INSTAGRAM_SCRAPE_CREDIT_COSTS: Record<string, number> = (() => {
88
+ const table: Record<string, number> = { [INSTAGRAM_SCRAPE_NODE_TYPE]: 20 }
89
+ for (const tier of META_ADS_ANALYSIS_TIERS) table[instagramAnalysisCreditId(tier)] = META_ADS_ANALYSIS_CREDITS_PER_AD[tier]
90
+ for (const t of INSTAGRAM_SCRAPE_TIERS) {
91
+ table[`${INSTAGRAM_SCRAPE_NODE_TYPE}:${t}`] = t
92
+ for (const tier of META_ADS_ANALYSIS_TIERS) {
93
+ table[`${INSTAGRAM_SCRAPE_NODE_TYPE}:${t}${analysisSuffix(tier)}`] = t * (1 + META_ADS_ANALYSIS_CREDITS_PER_AD[tier])
94
+ }
95
+ }
96
+ return table
97
+ })()
98
+
99
+ export const INSTAGRAM_SCRAPE_FALLBACK_CREDIT_ID = "instagram-scrape:20"
100
+
101
+ export function isInstagramScrapeCount(value: unknown): value is number {
102
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= INSTAGRAM_SCRAPE_MAX_COUNT
103
+ }
104
+
105
+ /**
106
+ * Targets (profile usernames/URLs or hashtags) are typed one per line; the
107
+ * route wants an array. Unlike page urls a target can be a bare username /
108
+ * `#tag`, so split on lines / commas only (never whitespace), strip a leading
109
+ * `@` or `#`, dedupe, cap at MAX_SOURCES.
110
+ */
111
+ export function splitInstagramTargets(value: unknown): string[] {
112
+ const raw = Array.isArray(value)
113
+ ? value.filter((v): v is string => typeof v === "string")
114
+ : typeof value === "string"
115
+ ? value.split(/[\n,]+/)
116
+ : []
117
+ const seen = new Set<string>()
118
+ const out: string[] = []
119
+ for (const item of raw) {
120
+ const t = item.trim().replace(/^[@#]+/, "").trim()
121
+ if (t.length < 1 || t.length > INSTAGRAM_SCRAPE_MAX_TARGET_LENGTH) continue
122
+ const key = t.toLowerCase()
123
+ if (seen.has(key)) continue
124
+ seen.add(key)
125
+ out.push(t)
126
+ if (out.length >= INSTAGRAM_SCRAPE_MAX_SOURCES) break
127
+ }
128
+ return out
129
+ }
130
+
131
+ /** The featured post index, clamped. */
132
+ export function clampInstagramFeaturedIndex(stored: unknown, count: number): number {
133
+ if (count <= 0) return 0
134
+ const n = typeof stored === "number" && Number.isFinite(stored) ? Math.trunc(stored) : 0
135
+ return Math.min(Math.max(n, 0), count - 1)
136
+ }
137
+
138
+ export interface FeaturedInstagramOutputs {
139
+ readonly text?: string
140
+ readonly imageUrl?: string
141
+ readonly videoUrl?: string
142
+ }
143
+
144
+ function urlStrings(value: unknown): string[] {
145
+ return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string" && v.trim().length > 0) : []
146
+ }
147
+
148
+ /** The featured post's caption (`text`), first image / cover (`image`), and first video (`video`). */
149
+ export function featuredInstagramOutputs(json: unknown, featuredIndex: unknown): FeaturedInstagramOutputs {
150
+ if (!Array.isArray(json) || json.length === 0) return {}
151
+ const post = json[clampInstagramFeaturedIndex(featuredIndex, json.length)]
152
+ if (!post || typeof post !== "object") return {}
153
+ const p = post as Record<string, unknown>
154
+ const text = typeof p.caption === "string" ? p.caption.trim() : ""
155
+ const imageUrl = urlStrings(p.images)[0] ?? urlStrings(p.videoPreviews)[0]
156
+ const videoUrl = urlStrings(p.videos)[0]
157
+ return {
158
+ ...(text ? { text } : {}),
159
+ ...(imageUrl ? { imageUrl } : {}),
160
+ ...(videoUrl ? { videoUrl } : {}),
161
+ }
162
+ }
163
+
164
+ /** The node-data fields a quote reads. */
165
+ export interface InstagramNodeQuoteFields {
166
+ readonly [key: string]: unknown
167
+ readonly mode?: unknown
168
+ readonly targets?: unknown
169
+ readonly count?: unknown
170
+ readonly analyze?: unknown
171
+ readonly analysisModel?: unknown
172
+ }
173
+
174
+ /** Billable source count: number of targets (min 1). */
175
+ export function instagramScrapeSources(data: InstagramNodeQuoteFields): number {
176
+ return Math.max(1, Math.min(splitInstagramTargets(data.targets).length, INSTAGRAM_SCRAPE_MAX_SOURCES))
177
+ }
178
+
179
+ /** The ONE credit identifier for a node's current settings. */
180
+ export function instagramScrapeCreditIdFromNode(data: InstagramNodeQuoteFields): string {
181
+ const count = typeof data.count === "number" ? data.count : INSTAGRAM_SCRAPE_DEFAULT_COUNT
182
+ return buildInstagramScrapeCreditId({ count, sources: instagramScrapeSources(data), analysis: instagramAnalysisTierFrom(data) })
183
+ }
184
+
185
+ /**
186
+ * Resolve the credit identifier from an UNVALIDATED request body (the guard
187
+ * runs before Zod). Lands on the SAME tier the reservation computes.
188
+ */
189
+ export function resolveInstagramScrapeCreditId(body: unknown): string {
190
+ const raw = body as { count?: unknown; targets?: unknown; analyze?: unknown; analysisModel?: unknown } | null | undefined
191
+ if (!raw || typeof raw !== "object") return INSTAGRAM_SCRAPE_FALLBACK_CREDIT_ID
192
+ const count = raw.count === undefined ? INSTAGRAM_SCRAPE_DEFAULT_COUNT : raw.count
193
+ if (!isInstagramScrapeCount(count)) return INSTAGRAM_SCRAPE_FALLBACK_CREDIT_ID
194
+ // Same splitter the handler uses for `sources` (dedupes, caps at MAX), so the
195
+ // pre-Zod guard and the post-Zod reservation always land on the same tier —
196
+ // a duplicate target bills once, not per copy.
197
+ const sources = splitInstagramTargets(raw.targets).length
198
+ if (sources < 1) return INSTAGRAM_SCRAPE_FALLBACK_CREDIT_ID
199
+ return buildInstagramScrapeCreditId({ count, sources, analysis: instagramAnalysisTierFrom(raw) })
200
+ }
201
+
202
+ // Re-export the shared creative-format vocabulary so the node imports one place.
203
+ export { classifyCreativeFormat }
204
+ export type InstagramFormat = MetaAdsFormat
package/src/llm-models.ts CHANGED
@@ -118,12 +118,37 @@ export interface LlmModelDef {
118
118
  *
119
119
  * Consumers MUST give such a model output headroom regardless of the
120
120
  * requested effort, or reasoning silently eats a small legacy cap and the
121
- * answer truncates with `stop_reason: max_tokens` — a paid-for empty reply,
122
- * not an error. `deriveParams` (llm-client.ts) and the film-pipeline's
123
- * `callLLM` both floor on this flag; keep it in sync with the vendor's
121
+ * answer truncates with `stop_reason: max_tokens` — a paid-for empty reply
122
+ * (since #1588 the client fails such a call rather than return the
123
+ * fragment, but the floor is what keeps it from happening). `deriveParams`
124
+ * (llm-client.ts) floors to {@link reasoningOutputFloor}; the film
125
+ * pipeline's `callLLM` — Anthropic SDK only, so only its Claude members
126
+ * matter — floors at the default. Keep the flag in sync with the vendor's
124
127
  * documented default rather than inferring it from the model name.
125
128
  */
126
129
  thinkingDefaultOn?: true
130
+ /**
131
+ * The output-token cap a REASONING call on this model is floored to — the
132
+ * room its thinking shares with the answer (`thinkingDefaultOn`, or an
133
+ * xhigh/max effort). Absent = {@link REASONING_OUTPUT_FLOOR}; read it through
134
+ * {@link reasoningOutputFloor}, never directly.
135
+ *
136
+ * Declare it ONLY where a lane serving this model is not known to accept the
137
+ * default: the floor rides every lane the model can be served on (KIE AND its
138
+ * direct fallback), so it has to sit at the intersection of what they take —
139
+ * the rule `maxOutputTokens` already follows for the Gemini flash entries.
140
+ * Never below `maxOutputTokens` (a floor under the default cap is not a
141
+ * floor — guarded by a registry test).
142
+ */
143
+ reasoningOutputFloor?: number
144
+ }
145
+
146
+ /** The reasoning floor for a model that declares no lane limit of its own. */
147
+ export const REASONING_OUTPUT_FLOOR = 32768
148
+
149
+ /** The output cap a reasoning call on `model` is floored to (see `LlmModelDef.reasoningOutputFloor`). */
150
+ export function reasoningOutputFloor(model: LlmModelDef): number {
151
+ return model.reasoningOutputFloor ?? REASONING_OUTPUT_FLOOR
127
152
  }
128
153
 
129
154
  export const LLM_MODELS: readonly LlmModelDef[] = [
@@ -145,6 +170,11 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
145
170
  // No `reasoningEfforts` at all on the KIE lane, but the vendor API accepts
146
171
  // the full minimal→high ladder (`none` maps to Google's `minimal`).
147
172
  directReasoningEfforts: ["none", "low", "medium", "high"],
173
+ // Reasons with no thinking param sent — Google's Gemini 3 default (dynamic
174
+ // thinking; `minimal` is its floor, never off), measured on 3.6 in #1588.
175
+ // Floored at the KIE-safe 8192, the same intersection as `maxOutputTokens`.
176
+ thinkingDefaultOn: true,
177
+ reasoningOutputFloor: 8192,
148
178
  },
149
179
  {
150
180
  id: "gemini-3.6-flash",
@@ -177,6 +207,15 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
177
207
  // Gemini entry — the lane with the lower unit cost wins by default and
178
208
  // direct is the reliability fallback only.
179
209
  directGeminiModel: "gemini-3.6-flash",
210
+ // Reasons with NO thinking param sent — measured, issue #1588: a Generate
211
+ // Text node capped at 1,100 tokens fell back to the direct lane (KIE 500),
212
+ // spent ~1,060 of them reasoning, and returned 120 characters cut mid-URL.
213
+ // On the same input the KIE runs used ~500 output tokens in all, so only
214
+ // the fallback runs broke — every other run of a 5-minute schedule.
215
+ // Floored at 8192, NOT the default 32768: the floor rides the KIE endpoint
216
+ // too, and 8192 is all it is known to take (see `maxOutputTokens`).
217
+ thinkingDefaultOn: true,
218
+ reasoningOutputFloor: 8192,
180
219
  },
181
220
  {
182
221
  id: "gemini-3.7-flash",
@@ -205,6 +244,10 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
205
244
  // Assumed parity with 3.6 pending a live probe on the direct lane.
206
245
  directReasoningEfforts: ["none", "low", "medium", "high"],
207
246
  directGeminiModel: "gemini-3.7-flash",
247
+ // Gemini 3 default: reasons with no thinking param sent (measured on 3.6,
248
+ // #1588). KIE-safe floor, same intersection as `maxOutputTokens`.
249
+ thinkingDefaultOn: true,
250
+ reasoningOutputFloor: 8192,
208
251
  },
209
252
  {
210
253
  id: "gemini-3.8-flash",
@@ -245,6 +288,11 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
245
288
  // KIE-first (no `preferDirect`) — 3.7's posture exactly: the cheap lane
246
289
  // serves the A/B, direct is Advanced mode + the reliability fallback.
247
290
  directGeminiModel: "gemini-3.8-flash",
291
+ // Gemini 3 default: reasons with no thinking param sent (measured on 3.6,
292
+ // #1588). Floored at its own 16384, inside the 20000 its KIE endpoint was
293
+ // measured to honour.
294
+ thinkingDefaultOn: true,
295
+ reasoningOutputFloor: 16384,
248
296
  },
249
297
  {
250
298
  id: "claude-haiku-4.5",
@@ -318,6 +366,11 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
318
366
  // `additionalProperties` (KIE's `response_format` silently DROPS
319
367
  // record/map-shaped fields — see the z.record rule in backend/CLAUDE.md).
320
368
  preferDirect: true,
369
+ // Reasons with no thinking param sent on both lanes — the proxied endpoint
370
+ // DEFAULTS to "high" (above), and the direct lane reasons harder still.
371
+ // Floored at its own 16384: its KIE fallback is not known to take more.
372
+ thinkingDefaultOn: true,
373
+ reasoningOutputFloor: 16384,
321
374
  },
322
375
  {
323
376
  id: "claude-opus-4.7",
@@ -413,6 +466,22 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
413
466
  maxOutputTokens: 16384,
414
467
  reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
415
468
  supportsTemperature: false,
469
+ // SERVED COLLAPSED, on the astra precedent (2026-09-17). The 2026-07-14
470
+ // verification that KIE's non-stream responses endpoint serves the GPT-5.6
471
+ // family reliably no longer holds: a live call — a recast continuity
472
+ // review, ~3k tokens, `reasoning.effort: high`, `text.format: json_schema`
473
+ // — came back `500 {"error":{"type":"server_error"}}` / "Server exception,
474
+ // please try again later". Same endpoint family, same dialect and the same
475
+ // signature astra was measured on (12 calls: non-stream 2/6, streaming
476
+ // 5/6; a schema-less non-stream call 500'd too, so the lane is the trigger
477
+ // and not the schema). ONE sighting here rather than a fresh 12-call probe
478
+ // — the precedent is strong and the flag is cheap to reverse.
479
+ //
480
+ // THE COST: SSE does not reliably carry `credits_consumed`, so this model's
481
+ // provider cost becomes the rate-table estimate instead of the billed
482
+ // figure. That is the price of a lane that answers, and it is the same
483
+ // trade astra already makes.
484
+ kieCollapseStream: true,
416
485
  },
417
486
  {
418
487
  id: "gpt-6-astra",
@@ -649,6 +718,10 @@ export type LlmFeature =
649
718
  // zero-cost `3d-scene-ops` identifier instead.
650
719
  | "3d-scene"
651
720
  | "image-to-text"
721
+ // Per-ad creative analysis on the social scraper nodes (Meta Ads first):
722
+ // one structured vision call per ad, priced per REQUESTED ad by tier and
723
+ // folded into the scrape's own identifier (packages/shared/meta-ads-scrape).
724
+ | "meta-ads-analysis"
652
725
  | "describe-to-picker"
653
726
  | "qa-check"
654
727
  | "generate-script"
@@ -680,6 +753,10 @@ export const LLM_FEATURE_DEFAULTS: Record<LlmFeature, string> = {
680
753
  "3d-title": "claude-sonnet-4.6",
681
754
  "3d-scene": "claude-sonnet-4.6",
682
755
  "image-to-text": "claude-sonnet-4.6",
756
+ // Economy on purpose: the analysis reads ONE frame + the copy per ad and
757
+ // runs once per returned ad — volume, not depth. Must stay an image-capable
758
+ // structured-output model (STRUCTURED_VISION_MODELS); a registry test pins it.
759
+ "meta-ads-analysis": "gemini-3.6-flash",
683
760
  "describe-to-picker": "claude-opus-5",
684
761
  "qa-check": "gemini-3.6-flash",
685
762
  "generate-script": "gemini-3.6-flash",