@nodaro/shared 3.6.0 → 3.8.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.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * What a Scene3D render COSTS as a function of FRAME SIZE.
3
+ *
4
+ * A render's work is per pixel per frame, and until the 2560 px cap landed
5
+ * every admissible frame was small enough that one flat price was honest. It
6
+ * no longer is: on the software GL path a container actually uses, one frame
7
+ * costs ~65 ms at 1920x1080, ~97 ms at 2560x1440 and ~154 ms at 2560x2560, and
8
+ * peak memory roughly doubles across that range. A single price for all three
9
+ * is either an overcharge on the small frame everyone renders or a giveaway on
10
+ * the large one almost nobody does.
11
+ *
12
+ * So the price is TIERED by frame size, and the tiers are the frames that were
13
+ * measured — not invented brackets:
14
+ *
15
+ * | Tier | Frame | Price |
16
+ * |----------|------------------------------------|--------------|
17
+ * | `base` | longest side <= 1920 px | 1x |
18
+ * | `large` | longer than 1920 px, <= 5.12 MP | 1.5x |
19
+ * | `xlarge` | longer than 1920 px, over 5.12 MP | 2.5x |
20
+ *
21
+ * Two rules make that table decidable for any frame, in this order:
22
+ *
23
+ * 1. **The 1920 gate.** A frame whose LONGEST side is at most 1920 px is
24
+ * `base`, whatever its area. This is a promise, not an optimisation:
25
+ * every frame that was renderable before the cap moved keeps exactly the
26
+ * price it had, so raising the ceiling cannot make an existing workflow
27
+ * more expensive.
28
+ * 2. **Nearest measured reference, by pixel area.** Above the gate a frame is
29
+ * priced at whichever measured frame it is closer to in PIXEL COUNT, and
30
+ * `SCENE3D_RENDER_XLARGE_MIN_AREA_PX` is exactly the midpoint between
31
+ * them. 2560x1440 (3.69 MP) and 2560x1097 are `large`; 2048x2560 (5.24 MP)
32
+ * and 2560x2560 (6.55 MP) are `xlarge`.
33
+ *
34
+ * The multipliers are the measured cost ratios rounded to halves (1.49 -> 1.5,
35
+ * 2.37 -> 2.5), which is why they are stated as multipliers and not as three
36
+ * unrelated numbers: an operator who reprices the base render keeps the whole
37
+ * ladder consistent, and `scene3DRenderTierCredits` is the one place the
38
+ * arithmetic happens.
39
+ *
40
+ * This module carries the SHAPE of the price (which tier, which identifier,
41
+ * which multiplier). It carries no rates: what a tier actually costs is a
42
+ * `model_pricing` row, so a deployment can price its own hardware.
43
+ */
44
+ import { PRO3D_RENDER_CREDIT_ID } from "./pro-3d-render.js"
45
+ import { SCENE3D_PLAN_TYPE } from "./scene3d.js"
46
+
47
+ /** Cheapest first. A vocabulary, so a surface can render the whole ladder. */
48
+ export const SCENE3D_RENDER_TIERS = ["base", "large", "xlarge"] as const
49
+ export type Scene3DRenderTier = (typeof SCENE3D_RENDER_TIERS)[number]
50
+
51
+ /**
52
+ * The longest side (px) that still renders at the base price.
53
+ *
54
+ * Deliberately NOT `SCENE3D_LIMITS.maxDimensionPx` minus something: it is the
55
+ * old ceiling, frozen, because its job is to hold every pre-cap frame at the
56
+ * price it already had. Raising the cap again moves `maxDimensionPx` and must
57
+ * leave this alone.
58
+ */
59
+ export const SCENE3D_RENDER_BASE_MAX_PX = 1920
60
+
61
+ /**
62
+ * Pixel area at which a frame stops being priced as 2560x1440 and starts being
63
+ * priced as 2560x2560 — the exact midpoint of those two measured frames
64
+ * (3,686,400 and 6,553,600). Only consulted for frames past the 1920 gate.
65
+ */
66
+ export const SCENE3D_RENDER_XLARGE_MIN_AREA_PX = 5_120_000
67
+
68
+ /** The measured cost ratios, rounded to halves. `base` is 1 by definition. */
69
+ export const SCENE3D_RENDER_TIER_MULTIPLIERS: Readonly<Record<Scene3DRenderTier, number>> = {
70
+ base: 1,
71
+ large: 1.5,
72
+ xlarge: 2.5,
73
+ }
74
+
75
+ /**
76
+ * Which tier a frame renders at.
77
+ *
78
+ * Total and defensive on purpose: it is read from a route guard, an
79
+ * orchestrator payload builder and a canvas badge, all of which hold a plan
80
+ * that has not necessarily been parsed yet. Anything that is not a pair of
81
+ * positive finite numbers is `base` — the safe answer for a display, and
82
+ * harmless for a charge because the route's Zod refuses such a plan anyway.
83
+ */
84
+ export function scene3DRenderTier(width: unknown, height: unknown): Scene3DRenderTier {
85
+ const w = typeof width === "number" && Number.isFinite(width) && width > 0 ? width : 0
86
+ const h = typeof height === "number" && Number.isFinite(height) && height > 0 ? height : 0
87
+ if (w === 0 || h === 0) return "base"
88
+ if (Math.max(w, h) <= SCENE3D_RENDER_BASE_MAX_PX) return "base"
89
+ return w * h > SCENE3D_RENDER_XLARGE_MIN_AREA_PX ? "xlarge" : "large"
90
+ }
91
+
92
+ /**
93
+ * A tier's price, from the base render's price.
94
+ *
95
+ * `Math.ceil` matches how the platform rounds every other derived credit
96
+ * figure, so a tier can never round DOWN into charging less than the base
97
+ * render it is a multiple of.
98
+ */
99
+ export function scene3DRenderTierCredits(baseCredits: number, tier: Scene3DRenderTier): number {
100
+ return Math.ceil(baseCredits * SCENE3D_RENDER_TIER_MULTIPLIERS[tier])
101
+ }
102
+
103
+ /** The flat identifier a render settles under when its frame is base-sized. */
104
+ export const RENDER_VIDEO_CREDIT_ID = "render-video"
105
+
106
+ /**
107
+ * The identifier one render-video request settles under.
108
+ *
109
+ * `base` keeps the BARE `render-video` id rather than gaining a `:base`
110
+ * suffix. That is what makes this change free of a price move: an existing
111
+ * deployment's configured `render-video` row keeps pricing every frame it
112
+ * priced before, and the two new rows only ever describe frames that were not
113
+ * renderable at all until the cap moved.
114
+ *
115
+ * Only `3d-scene` plans are tiered. A scene-graph or template render admits
116
+ * frames up to 3840 px today at the flat price, and re-tiering those would be
117
+ * a price INCREASE on work people already run — a separate decision, not a
118
+ * side effect of this one.
119
+ */
120
+ export function renderVideoCreditId(input: unknown): string {
121
+ if (!input || typeof input !== "object") return RENDER_VIDEO_CREDIT_ID
122
+ const body = input as { planType?: unknown; plan?: unknown }
123
+ if (body.planType !== SCENE3D_PLAN_TYPE) return RENDER_VIDEO_CREDIT_ID
124
+ if (!body.plan || typeof body.plan !== "object") return RENDER_VIDEO_CREDIT_ID
125
+ const plan = body.plan as { width?: unknown; height?: unknown }
126
+ const tier = scene3DRenderTier(plan.width, plan.height)
127
+ return tier === "base" ? RENDER_VIDEO_CREDIT_ID : `${RENDER_VIDEO_CREDIT_ID}:3d-${tier}`
128
+ }
129
+
130
+ /**
131
+ * The per-frame unit a 3D Render Pro run's render stage reads.
132
+ *
133
+ * Pro prices its render per OUTPUT FRAME, so the frame-size tier belongs on
134
+ * that unit rather than on a whole-run identifier — the same ladder, applied
135
+ * where Pro actually multiplies. `base` keeps the existing
136
+ * `pro-3d-render:render-frame:<quality>` spelling so the rows an operator has
137
+ * already configured keep serving every frame they serve today.
138
+ *
139
+ * The rates themselves are deployment configuration and live nowhere in this
140
+ * repository; this function only says which row to read.
141
+ */
142
+ export function pro3DRenderFrameUnit(quality: string, tier: Scene3DRenderTier = "base"): string {
143
+ const unit = `${PRO3D_RENDER_CREDIT_ID}:render-frame:${quality}`
144
+ return tier === "base" ? unit : `${unit}:${tier}`
145
+ }
package/src/scene3d-v2.ts CHANGED
@@ -91,7 +91,9 @@ export const SCENE3D_V2_LIMITS = {
91
91
  defaultFps: 24,
92
92
  /** Even integers only — an odd axis breaks H.264 chroma subsampling. */
93
93
  minDimensionPx: 100,
94
- maxDimensionPx: 1920,
94
+ /** Deliberately kept equal to v1's `SCENE3D_LIMITS.maxDimensionPx`: one
95
+ * renderer draws both versions, so one measured ceiling bounds both. */
96
+ maxDimensionPx: SCENE3D_LIMITS.maxDimensionPx,
95
97
  minEntities: 1,
96
98
  /** SEMANTIC entities, not exported mesh nodes. */
97
99
  maxEntities: 100,
package/src/scene3d.ts CHANGED
@@ -33,6 +33,15 @@
33
33
  * Sampling itself lives with the renderer (`packages/remotion`) — this file
34
34
  * only guarantees the data it samples is well-formed.
35
35
  *
36
+ * Camera and object RIGS are deliberately absent, and their absence is a
37
+ * decision rather than an unfinished TODO: spline rails, follow-path and
38
+ * track-to constraints, and procedural noise modifiers are authored UPSTREAM
39
+ * (in Blender) and reach this contract already BAKED — v1 as keyframes on the
40
+ * tracks above, v2 as one camera sample per frame. The format carries no
41
+ * constraint or noise vocabulary ON PURPOSE, because evaluating a rig in two
42
+ * different renderers cannot be guaranteed to agree frame for frame, and that
43
+ * agreement is the promise everything else here rests on.
44
+ *
36
45
  * ## Revisions
37
46
  *
38
47
  * A plan is IMMUTABLE. Every accepted edit produces a NEW `revisionId` and
@@ -59,7 +68,16 @@ export const SCENE3D_DEFAULT_DURATION_SECONDS = 4
59
68
  */
60
69
  export const SCENE3D_LIMITS = {
61
70
  minDimensionPx: 100,
62
- maxDimensionPx: 1920,
71
+ /** Applies to BOTH axes, so the worst admissible frame is SQUARE, not merely
72
+ * wider — 2560x2560 is 3.2x the pixels of 1920x1080, and that is the frame
73
+ * this bound was measured at. Raised from 1920 once that cost was measured
74
+ * rather than assumed: on the software GL path a container actually uses,
75
+ * an animated 2560x2560 frame costs ~154 ms/frame against ~65 ms at
76
+ * 1920x1080, so even `maxDurationInFrames` (3600) lands near 9 min against
77
+ * the render worker's 25-minute budget. Peak memory is the real price —
78
+ * ~3.1 GB across the browser process tree versus ~1.6 GB — and is the number
79
+ * to re-measure before widening this again. */
80
+ maxDimensionPx: 2560,
63
81
  minFps: 15,
64
82
  maxFps: 60,
65
83
  minDurationInFrames: 1,