@nodetool-ai/blender-nodes 0.8.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 (83) hide show
  1. package/README.md +36 -0
  2. package/blender_ops/depth.py +133 -0
  3. package/blender_ops/errors.py +46 -0
  4. package/blender_ops/exr.py +205 -0
  5. package/blender_ops/framing.py +75 -0
  6. package/blender_ops/ops/__init__.py +24 -0
  7. package/blender_ops/ops/common.py +310 -0
  8. package/blender_ops/ops/export_model.py +95 -0
  9. package/blender_ops/ops/prepare_for_engine.py +352 -0
  10. package/blender_ops/ops/render_animation.py +382 -0
  11. package/blender_ops/ops/render_image.py +41 -0
  12. package/blender_ops/ops/render_passes.py +363 -0
  13. package/blender_ops/run_job.py +110 -0
  14. package/blender_ops/tests/test_camera_lens.py +107 -0
  15. package/blender_ops/tests/test_depth.py +158 -0
  16. package/blender_ops/tests/test_exr.py +218 -0
  17. package/blender_ops/tests/test_framing.py +50 -0
  18. package/dist/.nodetool-build-stamp +1 -0
  19. package/dist/bake.d.ts +107 -0
  20. package/dist/bake.d.ts.map +1 -0
  21. package/dist/bake.js +209 -0
  22. package/dist/bake.js.map +1 -0
  23. package/dist/blender-binary.d.ts +38 -0
  24. package/dist/blender-binary.d.ts.map +1 -0
  25. package/dist/blender-binary.js +169 -0
  26. package/dist/blender-binary.js.map +1 -0
  27. package/dist/index.d.ts +31 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +40 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/job.d.ts +163 -0
  32. package/dist/job.d.ts.map +1 -0
  33. package/dist/job.js +67 -0
  34. package/dist/job.js.map +1 -0
  35. package/dist/nodes/bake-timeline-clip.d.ts +48 -0
  36. package/dist/nodes/bake-timeline-clip.d.ts.map +1 -0
  37. package/dist/nodes/bake-timeline-clip.js +152 -0
  38. package/dist/nodes/bake-timeline-clip.js.map +1 -0
  39. package/dist/nodes/blender-error.d.ts +21 -0
  40. package/dist/nodes/blender-error.d.ts.map +1 -0
  41. package/dist/nodes/blender-error.js +38 -0
  42. package/dist/nodes/blender-error.js.map +1 -0
  43. package/dist/nodes/defaults.d.ts +18 -0
  44. package/dist/nodes/defaults.d.ts.map +1 -0
  45. package/dist/nodes/defaults.js +18 -0
  46. package/dist/nodes/defaults.js.map +1 -0
  47. package/dist/nodes/export-model.d.ts +49 -0
  48. package/dist/nodes/export-model.d.ts.map +1 -0
  49. package/dist/nodes/export-model.js +127 -0
  50. package/dist/nodes/export-model.js.map +1 -0
  51. package/dist/nodes/prepare-for-engine.d.ts +52 -0
  52. package/dist/nodes/prepare-for-engine.d.ts.map +1 -0
  53. package/dist/nodes/prepare-for-engine.js +127 -0
  54. package/dist/nodes/prepare-for-engine.js.map +1 -0
  55. package/dist/nodes/progress.d.ts +11 -0
  56. package/dist/nodes/progress.d.ts.map +1 -0
  57. package/dist/nodes/progress.js +21 -0
  58. package/dist/nodes/progress.js.map +1 -0
  59. package/dist/nodes/render-animation.d.ts +52 -0
  60. package/dist/nodes/render-animation.d.ts.map +1 -0
  61. package/dist/nodes/render-animation.js +136 -0
  62. package/dist/nodes/render-animation.js.map +1 -0
  63. package/dist/nodes/render-base.d.ts +35 -0
  64. package/dist/nodes/render-base.d.ts.map +1 -0
  65. package/dist/nodes/render-base.js +95 -0
  66. package/dist/nodes/render-base.js.map +1 -0
  67. package/dist/nodes/render-image.d.ts +41 -0
  68. package/dist/nodes/render-image.d.ts.map +1 -0
  69. package/dist/nodes/render-image.js +98 -0
  70. package/dist/nodes/render-image.js.map +1 -0
  71. package/dist/nodes/render-passes.d.ts +54 -0
  72. package/dist/nodes/render-passes.d.ts.map +1 -0
  73. package/dist/nodes/render-passes.js +167 -0
  74. package/dist/nodes/render-passes.js.map +1 -0
  75. package/dist/run-job.d.ts +34 -0
  76. package/dist/run-job.d.ts.map +1 -0
  77. package/dist/run-job.js +137 -0
  78. package/dist/run-job.js.map +1 -0
  79. package/dist/runner.d.ts +124 -0
  80. package/dist/runner.d.ts.map +1 -0
  81. package/dist/runner.js +521 -0
  82. package/dist/runner.js.map +1 -0
  83. package/package.json +73 -0
@@ -0,0 +1,158 @@
1
+ """Unit tests for `depth.py`: normalization contracts and the PNG writer.
2
+
3
+ Plain asserts, no pytest, no `bpy`: runs under system Python (`python3
4
+ tests/test_depth.py`) and under Blender's interpreter alike. Exits nonzero
5
+ on the first mismatch.
6
+ """
7
+
8
+ import os
9
+ import struct
10
+ import sys
11
+ import zlib
12
+
13
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
14
+
15
+ from depth import (
16
+ BACKGROUND16,
17
+ depth_range,
18
+ normalize_to_u16,
19
+ write_gray8_png,
20
+ write_gray16_png,
21
+ write_rgb8_png,
22
+ )
23
+
24
+
25
+ def check(label, actual, expected):
26
+ if actual != expected:
27
+ print("MISMATCH %s: got %r want %r" % (label, actual, expected))
28
+ sys.exit(1)
29
+
30
+
31
+ def check_close(label, actual, expected, tol=1e-9):
32
+ if abs(actual - expected) > tol:
33
+ print("MISMATCH %s: got %r want %r" % (label, actual, expected))
34
+ sys.exit(1)
35
+
36
+
37
+ # min/max over the masked finite pixels; inf and unmasked pixels never win.
38
+ near, far = depth_range(
39
+ [1.0, float("inf"), 5.0, 2.0],
40
+ [True, True, False, True],
41
+ )
42
+ check_close("near", near, 1.0)
43
+ check_close("far", far, 2.0)
44
+
45
+ # no finite foreground depth is an error, not a silent (0, 0).
46
+ try:
47
+ depth_range([float("inf"), 3.0], [True, False])
48
+ print("MISMATCH empty-range: expected ValueError")
49
+ sys.exit(1)
50
+ except ValueError:
51
+ pass
52
+
53
+ # endpoints land exactly; background and +inf land on 65535.
54
+ values = normalize_to_u16(
55
+ [1.0, 2.0, 3.0, float("inf"), 99.0],
56
+ [True, True, True, True, False],
57
+ 1.0,
58
+ 3.0,
59
+ )
60
+ check("normalized", values, [0, 32768, 65535, BACKGROUND16, BACKGROUND16])
61
+
62
+ # degenerate range maps finite foreground to 0, never divides by zero.
63
+ check(
64
+ "degenerate",
65
+ normalize_to_u16([2.0, float("inf")], [True, True], 2.0, 2.0),
66
+ [0, BACKGROUND16],
67
+ )
68
+
69
+ # past the endpoints clamps to the endpoint values.
70
+ check(
71
+ "clamped",
72
+ normalize_to_u16([0.0, 9.0], [True, True], 1.0, 3.0),
73
+ [0, 65535],
74
+ )
75
+
76
+ # the writer round-trips: parse the chunks, inflate the IDAT, compare bytes.
77
+ out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_depth_test.png")
78
+ try:
79
+ write_gray16_png(out, 2, 2, [0, 32768, 65535, 12345])
80
+ with open(out, "rb") as handle:
81
+ blob = handle.read()
82
+ check("png signature", blob[:8], b"\x89PNG\r\n\x1a\n")
83
+ pos = 8
84
+ seen = {}
85
+ while pos < len(blob):
86
+ (length,) = struct.unpack(">I", blob[pos : pos + 4])
87
+ kind = blob[pos + 4 : pos + 8]
88
+ payload = blob[pos + 8 : pos + 8 + length]
89
+ (crc,) = struct.unpack(">I", blob[pos + 8 + length : pos + 12 + length])
90
+ check("crc %s" % (kind,), crc, zlib.crc32(kind + payload) & 0xFFFFFFFF)
91
+ seen[kind] = payload
92
+ pos += 12 + length
93
+ ihdr = seen[b"IHDR"]
94
+ check("ihdr size", struct.unpack(">II", ihdr[:8]), (2, 2))
95
+ check("ihdr depth/type", (ihdr[8], ihdr[9]), (16, 0))
96
+ raw = zlib.decompress(seen[b"IDAT"])
97
+ expect = b"".join(
98
+ [
99
+ b"\x00" + struct.pack(">HH", 0, 32768),
100
+ b"\x00" + struct.pack(">HH", 65535, 12345),
101
+ ]
102
+ )
103
+ check("idat samples", raw, expect)
104
+ finally:
105
+ if os.path.exists(out):
106
+ os.remove(out)
107
+
108
+
109
+ def read_rows(path):
110
+ with open(path, "rb") as handle:
111
+ blob = handle.read()
112
+ pos = 8
113
+ ihdr = None
114
+ idat = b""
115
+ while pos < len(blob):
116
+ (length,) = struct.unpack(">I", blob[pos : pos + 4])
117
+ kind = blob[pos + 4 : pos + 8]
118
+ payload = blob[pos + 8 : pos + 8 + length]
119
+ if kind == b"IHDR":
120
+ ihdr = payload
121
+ elif kind == b"IDAT":
122
+ idat += payload
123
+ pos += 12 + length
124
+ width, height = struct.unpack(">II", ihdr[:8])
125
+ return width, height, ihdr[8], ihdr[9], zlib.decompress(idat)
126
+
127
+
128
+ out8 = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_mask_test.png")
129
+ try:
130
+ write_gray8_png(out8, 3, 1, [0, 255, 128])
131
+ width, height, depth, ctype, raw = read_rows(out8)
132
+ check("gray8 geometry", (width, height, depth, ctype), (3, 1, 8, 0))
133
+ check("gray8 samples", raw, b"\x00\x00\xff\x80")
134
+ finally:
135
+ if os.path.exists(out8):
136
+ os.remove(out8)
137
+
138
+ outn = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_normal_test.png")
139
+ try:
140
+ write_rgb8_png(outn, 2, 1, [128, 128, 255, 255, 0, 0])
141
+ width, height, depth, ctype, raw = read_rows(outn)
142
+ check("rgb8 geometry", (width, height, depth, ctype), (2, 1, 8, 2))
143
+ check("rgb8 samples", raw, b"\x00\x80\x80\xff\xff\x00\x00")
144
+ finally:
145
+ if os.path.exists(outn):
146
+ os.remove(outn)
147
+
148
+ # a short row is an error, never a truncated file.
149
+ try:
150
+ write_gray8_png(out8, 2, 2, [0, 0, 0])
151
+ print("MISMATCH short-row: expected ValueError")
152
+ sys.exit(1)
153
+ except ValueError:
154
+ pass
155
+ if os.path.exists(out8):
156
+ os.remove(out8)
157
+
158
+ print("depth: all checks passed")
@@ -0,0 +1,218 @@
1
+ """Unit tests for `exr.py`: the minimal scanline reader.
2
+
3
+ Plain asserts, no pytest, no `bpy`: runs under system Python (`python3
4
+ tests/test_exr.py`) and under Blender's interpreter alike. Exits nonzero
5
+ on the first mismatch.
6
+
7
+ The synthetic file mirrors exactly what Blender's File Output writes for a
8
+ staged pass: single-part scanline, `NO_COMPRESSION`, `FLOAT` channels named
9
+ `Image.R/G/B/A`, increasing line order.
10
+ """
11
+
12
+ import os
13
+ import struct
14
+ import sys
15
+
16
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
17
+
18
+ from exr import ExrError, read_exr_rgba, rewrite_background_to_inf
19
+
20
+
21
+ def check(label, actual, expected):
22
+ if actual != expected:
23
+ print("MISMATCH %s: got %r want %r" % (label, actual, expected))
24
+ sys.exit(1)
25
+
26
+
27
+ def read_channel(path, wanted):
28
+ """Float samples of one named channel, in row-major order."""
29
+ from exr import _parse_header, _parse_header_channels
30
+
31
+ with open(path, "rb") as handle:
32
+ blob = handle.read()
33
+ attrs, pos = _parse_header(blob)
34
+ (x0, y0, x1, y1) = struct.unpack("<iiii", attrs["dataWindow"])
35
+ width, height = x1 - x0 + 1, y1 - y0 + 1
36
+ channels = _parse_header_channels(attrs)
37
+ offsets = struct.unpack("<%dq" % (height,), blob[pos : pos + 8 * height])
38
+ values = []
39
+ for row, offset in enumerate(offsets):
40
+ cursor = offset + 8
41
+ for name, pixel_type, _ in channels:
42
+ count = width * 4
43
+ if name == wanted:
44
+ values.extend(
45
+ struct.unpack(
46
+ "<%df" % (width,), blob[cursor : cursor + count]
47
+ )
48
+ )
49
+ cursor += count
50
+ return values
51
+
52
+
53
+ def write_exr(path, width, height, channels, compression=0):
54
+ """Minimal uncompressed scanline EXR: `channels` is [(name, [floats])]."""
55
+ blob = bytearray(b"v/1\x01" + struct.pack("<I", 2))
56
+
57
+ def attr(name, typ, payload):
58
+ blob.extend(name + b"\x00" + typ + b"\x00")
59
+ blob.extend(struct.pack("<i", len(payload)) + payload)
60
+
61
+ attr(b"compression", b"compression", struct.pack("B", compression))
62
+ attr(
63
+ b"dataWindow",
64
+ b"box2i",
65
+ struct.pack("<iiii", 0, 0, width - 1, height - 1),
66
+ )
67
+ attr(
68
+ b"displayWindow",
69
+ b"box2i",
70
+ struct.pack("<iiii", 0, 0, width - 1, height - 1),
71
+ )
72
+ attr(b"lineOrder", b"lineOrder", b"\x00")
73
+ chlist = b""
74
+ for name, _values in channels:
75
+ chlist += name + b"\x00" + struct.pack("<i", 2) + b"\x00\x00\x00\x00"
76
+ chlist += struct.pack("<ii", 1, 1)
77
+ chlist += b"\x00"
78
+ attr(b"channels", b"chlist", chlist)
79
+ blob += b"\x00"
80
+ table_at = len(blob)
81
+ blob += b"\x00" * (8 * height)
82
+ offsets = []
83
+ for row in range(height):
84
+ offsets.append(len(blob))
85
+ blob += struct.pack("<i", row)
86
+ body = b"".join(
87
+ struct.pack("<%df" % (width,), *values[row * width : (row + 1) * width])
88
+ for _name, values in channels
89
+ )
90
+ blob += struct.pack("<i", len(body)) + body
91
+ struct.pack_into("<%dq" % (height,), blob, table_at, *offsets)
92
+ with open(path, "wb") as handle:
93
+ handle.write(bytes(blob))
94
+
95
+
96
+ HERE = os.path.dirname(os.path.abspath(__file__))
97
+ GOOD = os.path.join(HERE, "_exr_test.exr")
98
+
99
+ try:
100
+ depth = [float(i) for i in range(6)]
101
+ write_exr(
102
+ GOOD,
103
+ 3,
104
+ 2,
105
+ [
106
+ (b"Image.A", [1.0] * 6),
107
+ (b"Image.B", depth),
108
+ (b"Image.G", depth),
109
+ (b"Image.R", depth),
110
+ ],
111
+ )
112
+ width, height, r, g, b = read_exr_rgba(GOOD)
113
+ check("geometry", (width, height), (3, 2))
114
+ check("r", r, depth)
115
+ check("g", g, depth)
116
+ check("b", b, depth)
117
+ finally:
118
+ if os.path.exists(GOOD):
119
+ os.remove(GOOD)
120
+
121
+ # inf and nan survive the round trip bit-exact (the sentinel gate needs them).
122
+ try:
123
+ write_exr(
124
+ GOOD,
125
+ 2,
126
+ 1,
127
+ [
128
+ (b"L.R", [float("inf"), float("nan")]),
129
+ (b"L.G", [0.0, 0.0]),
130
+ (b"L.B", [0.0, 0.0]),
131
+ ],
132
+ )
133
+ _w, _h, r, _g, _b = read_exr_rgba(GOOD)
134
+ check("inf", r[0] == float("inf"), True)
135
+ check("nan", r[1] != r[1], True)
136
+ finally:
137
+ if os.path.exists(GOOD):
138
+ os.remove(GOOD)
139
+
140
+ # a compressed file is refused, never misread.
141
+ try:
142
+ write_exr(
143
+ GOOD,
144
+ 1,
145
+ 1,
146
+ [(b"L.R", [1.0]), (b"L.G", [1.0]), (b"L.B", [1.0])],
147
+ compression=3,
148
+ )
149
+ read_exr_rgba(GOOD)
150
+ print("MISMATCH compressed: expected ExrError")
151
+ sys.exit(1)
152
+ except ExrError:
153
+ pass
154
+ finally:
155
+ if os.path.exists(GOOD):
156
+ os.remove(GOOD)
157
+
158
+ # not an EXR file at all.
159
+ try:
160
+ with open(GOOD, "wb") as handle:
161
+ handle.write(b"definitely not exr")
162
+ read_exr_rgba(GOOD)
163
+ print("MISMATCH magic: expected ExrError")
164
+ sys.exit(1)
165
+ except ExrError:
166
+ pass
167
+ finally:
168
+ if os.path.exists(GOOD):
169
+ os.remove(GOOD)
170
+
171
+ # background samples become +inf, foreground is bit-exact, alpha is kept.
172
+ try:
173
+ write_exr(
174
+ GOOD,
175
+ 3,
176
+ 2,
177
+ [
178
+ (b"Image.A", [1.0] * 6),
179
+ (b"Image.B", [1e10, 2.0, 1e10, 4.0, 5.0, 1e10]),
180
+ (b"Image.G", [1e10, 2.0, 1e10, 4.0, 5.0, 1e10]),
181
+ (b"Image.R", [1e10, 2.0, 1e10, 4.0, 5.0, 1e10]),
182
+ ],
183
+ )
184
+ rewrite_background_to_inf(
185
+ GOOD, [False, True, False, True, True, False]
186
+ )
187
+ _w, _h, r, g, b = read_exr_rgba(GOOD)
188
+ check("bg", [r[0], r[2], r[5]], [float("inf")] * 3)
189
+ check("fg", [r[1], r[3], r[4]], [2.0, 4.0, 5.0])
190
+ check("g-matches", g, r)
191
+ check("b-matches", b, r)
192
+ check("alpha-kept", read_channel(GOOD, "Image.A"), [1.0] * 6)
193
+ finally:
194
+ if os.path.exists(GOOD):
195
+ os.remove(GOOD)
196
+
197
+ # a mask that does not fill the frame is refused, never half-applied.
198
+ try:
199
+ write_exr(
200
+ GOOD,
201
+ 2,
202
+ 1,
203
+ [
204
+ (b"L.R", [1.0, 2.0]),
205
+ (b"L.G", [1.0, 2.0]),
206
+ (b"L.B", [1.0, 2.0]),
207
+ ],
208
+ )
209
+ rewrite_background_to_inf(GOOD, [True])
210
+ print("MISMATCH mask-size: expected ValueError")
211
+ sys.exit(1)
212
+ except ValueError:
213
+ pass
214
+ finally:
215
+ if os.path.exists(GOOD):
216
+ os.remove(GOOD)
217
+
218
+ print("exr: all checks passed")
@@ -0,0 +1,50 @@
1
+ """T3, Python side: pin the `framing.py` port to the shared goldens.
2
+
3
+ Runs under Blender's own interpreter (`blender -b --python ...`), which the
4
+ TypeScript suite (`tests/framing.test.ts`) drives when Blender is present.
5
+ Plain asserts only: Blender's Python has no pytest. Exits nonzero on the
6
+ first mismatch, printing both values.
7
+
8
+ Goldens: radius 2, fov 40, aspect 4/3, zoom 1.2; azimuth 30, elevation 20;
9
+ bounds center (0.5, 0.25, -1). Same numbers as `framing.test.ts`.
10
+ """
11
+
12
+ import os
13
+ import sys
14
+
15
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
16
+
17
+ from framing import compute_framing, orbit_offset
18
+
19
+ GOLDEN = {
20
+ "distance": 4.8730,
21
+ "near": 0.0487,
22
+ "far": 24.8730,
23
+ "offset": (2.2896, 1.6667, 3.9656),
24
+ "position": (2.7896, 1.9167, 2.9656),
25
+ "center": (0.5, 0.25, -1.0),
26
+ }
27
+
28
+
29
+ def check(label, actual, expected):
30
+ if abs(actual - expected) > 5e-5:
31
+ print("MISMATCH %s: got %r want %r" % (label, actual, expected))
32
+ sys.exit(1)
33
+
34
+
35
+ framing = compute_framing(2, 40, 4.0 / 3.0, 1.2)
36
+ check("distance", framing["distance"], GOLDEN["distance"])
37
+ check("near", framing["near"], GOLDEN["near"])
38
+ check("far", framing["far"], GOLDEN["far"])
39
+
40
+ offset = orbit_offset(30, 20, framing["distance"])
41
+ check("offset.x", offset["x"], GOLDEN["offset"][0])
42
+ check("offset.y", offset["y"], GOLDEN["offset"][1])
43
+ check("offset.z", offset["z"], GOLDEN["offset"][2])
44
+
45
+ cx, cy, cz = GOLDEN["center"]
46
+ check("position.x", cx + offset["x"], GOLDEN["position"][0])
47
+ check("position.y", cy + offset["y"], GOLDEN["position"][1])
48
+ check("position.z", cz + offset["z"], GOLDEN["position"][2])
49
+
50
+ print("framing goldens ok")
@@ -0,0 +1 @@
1
+ 2026-09-28T09:19:11.534Z
package/dist/bake.d.ts ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The timeline's 3D bake: a sampled Blender render muxed into one video
3
+ * (design §D6).
4
+ *
5
+ * The caller hands over one model time and one camera per output frame — the
6
+ * evaluated clip, with its in point, speed, time remap, animation speed and
7
+ * camera curves already applied — and gets back an MP4 the clip can play as
8
+ * an ordinary video layer. Blender renders a still per entry rather than a
9
+ * frame range, because a trimmed, sped-up, reversed or looped clip is not a
10
+ * run of consecutive scene frames.
11
+ *
12
+ * The mux is ffmpeg and not Blender's own writer, which is what
13
+ * `render_animation`'s video mode uses. Two reasons: one sequence feeds both
14
+ * bake formats, and Blender cannot write VP9 with an alpha channel. ffmpeg is
15
+ * therefore a host binary this path needs and the video mode does not; a
16
+ * machine without it fails here with the binary named.
17
+ */
18
+ import type { ProcessingContext } from "@nodetool-ai/runtime";
19
+ import type { BakeCameraParams, BlenderJob, BlenderResultStats, RenderAnimationParams } from "./job.js";
20
+ /**
21
+ * Ceiling on the frames one bake asks for. Every frame is a declared output
22
+ * the runner reads into memory, so this is the bound on that, not a limit on
23
+ * clip length in the abstract: two minutes at 30 fps.
24
+ */
25
+ export declare const MAX_BAKE_FRAMES = 3600;
26
+ /** What one bake renders. Cameras and times are parallel, one pair per frame. */
27
+ export interface Model3DBakeRequest {
28
+ /** Model time in seconds, one entry per output frame. */
29
+ frameTimes: readonly number[];
30
+ /** The camera each of those frames renders through. */
31
+ cameras: readonly BakeCameraParams[];
32
+ /** glTF animation to play; every one plays when absent (the D2 default). */
33
+ animationName?: string;
34
+ width: number;
35
+ height: number;
36
+ fps: number;
37
+ }
38
+ export interface Model3DBakeOptions {
39
+ timeoutMs: number;
40
+ signal?: AbortSignal;
41
+ /** Reported per rendered frame, before the mux. */
42
+ onProgress?: (frame: number, total: number) => void;
43
+ }
44
+ /** The container, encoder and pixel format one bake is written as. */
45
+ export interface BakeVideoFormat {
46
+ /** File extension of the muxed bytes, without the dot. */
47
+ extension: "mp4" | "webm";
48
+ mimeType: string;
49
+ /** ffmpeg output arguments: the encoder and its pixel format. */
50
+ encoderArgs: readonly string[];
51
+ }
52
+ /**
53
+ * How a bake is encoded: opaque to MP4/H.264 `yuv420p`, transparent to WebM
54
+ * VP9 `yuva420p` (design §D6).
55
+ *
56
+ * The alpha pair is the one `resolveTimelineOutput({ format: "webm", alpha:
57
+ * true })` already declares for a timeline render, copied rather than imported
58
+ * because `@nodetool-ai/blender-nodes` does not depend on
59
+ * `@nodetool-ai/video-nodes` and a bake needs four strings from it, not a
60
+ * package. `packages/agents/tests/timeline-bake-format.test.ts` depends on both
61
+ * and fails when the two drift.
62
+ */
63
+ export declare function bakeVideoFormat(alpha: boolean): BakeVideoFormat;
64
+ export interface Model3DBakeResult {
65
+ /** The muxed bytes, in {@link Model3DBakeResult.format}. */
66
+ video: Uint8Array;
67
+ /** What those bytes are, so the caller names and stores them correctly. */
68
+ format: BakeVideoFormat;
69
+ stats: BlenderResultStats;
70
+ }
71
+ /**
72
+ * The `frame_*` outputs a sampled job declares: one per entry, in play order.
73
+ * The op writes the file each name carries and reports the names back.
74
+ */
75
+ export declare function bakeFrameOutputs(count: number): BlenderJob["outputs"];
76
+ /** The sampled `render_animation` params for one bake. */
77
+ export declare function bakeRenderAnimationParams(request: Model3DBakeRequest): RenderAnimationParams;
78
+ /** Render the sequence. Frames come back in play order. */
79
+ export declare function renderModel3DBakeFrames(context: ProcessingContext, modelBytes: Uint8Array, request: Model3DBakeRequest, options: Model3DBakeOptions): Promise<{
80
+ frames: Uint8Array[];
81
+ stats: BlenderResultStats;
82
+ }>;
83
+ /**
84
+ * The ffmpeg call that turns the numbered PNGs into one video.
85
+ *
86
+ * Pure, because the arguments are the whole difference between the two bakes
87
+ * and a test should not need ffmpeg to read them.
88
+ */
89
+ export declare function bakeMuxArgs(format: BakeVideoFormat, fps: number, output: string): string[];
90
+ /**
91
+ * Mux a PNG sequence into one video through ffmpeg's image2 demuxer.
92
+ *
93
+ * The frames are written under the workspace's scratch directory rather than
94
+ * piped: image2 reads a numbered pattern, and the same files reach either
95
+ * encoder unchanged — an alpha bake differs only in the arguments
96
+ * {@link bakeVideoFormat} names, because the PNGs already carry the channel.
97
+ */
98
+ export declare function muxPngSequenceToVideo(context: ProcessingContext, frames: readonly Uint8Array[], fps: number, format: BakeVideoFormat, options: Model3DBakeOptions): Promise<Uint8Array>;
99
+ /**
100
+ * Render the sampled sequence and mux it: the whole bake (§D6).
101
+ *
102
+ * Whether it carries alpha is read off the frames' own camera rather than
103
+ * taken as a second field: `transparent` is what Blender rendered the film
104
+ * under, so a separate flag could only ever disagree with the pixels.
105
+ */
106
+ export declare function bakeModel3DClipToVideo(context: ProcessingContext, modelBytes: Uint8Array, request: Model3DBakeRequest, options: Model3DBakeOptions): Promise<Model3DBakeResult>;
107
+ //# sourceMappingURL=bake.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bake.d.ts","sourceRoot":"","sources":["../src/bake.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,OAAO,KAAK,EACV,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,qBAAqB,EACtB,MAAM,UAAU,CAAC;AAIlB;;;;GAIG;AACH,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,uDAAuD;IACvD,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACrC,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,mDAAmD;IACnD,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAED,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAe/D;AAED,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,KAAK,EAAE,UAAU,CAAC;IAClB,2EAA2E;IAC3E,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,kBAAkB,CAAC;CAC3B;AAOD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,CAMrE;AAyBD,0DAA0D;AAC1D,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,kBAAkB,GAC1B,qBAAqB,CAuBvB;AAED,2DAA2D;AAC3D,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAA;CAAE,CAAC,CA+B9D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GACb,MAAM,EAAE,CAqBV;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAiCrB;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAgB5B"}