bladeworks 0.1.0__py3-none-any.whl

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 (178) hide show
  1. bladeworks/__init__.py +56 -0
  2. bladeworks/__main__.py +4 -0
  3. bladeworks/cli.py +728 -0
  4. bladeworks/core/__init__.py +34 -0
  5. bladeworks/core/animation.py +893 -0
  6. bladeworks/core/animation_execution.py +817 -0
  7. bladeworks/core/audio_enhancements.py +673 -0
  8. bladeworks/core/audio_execution.py +1848 -0
  9. bladeworks/core/audio_ir.py +1971 -0
  10. bladeworks/core/basic_effects.py +168 -0
  11. bladeworks/core/bindings.py +196 -0
  12. bladeworks/core/capabilities.py +257 -0
  13. bladeworks/core/cohort_effects.py +702 -0
  14. bladeworks/core/color.py +324 -0
  15. bladeworks/core/compiler.py +3142 -0
  16. bladeworks/core/composition_diagnostics.py +629 -0
  17. bladeworks/core/composition_ir.py +3154 -0
  18. bladeworks/core/composition_planning.py +708 -0
  19. bladeworks/core/composition_shadow.py +687 -0
  20. bladeworks/core/compositor.py +1186 -0
  21. bladeworks/core/effect_parameter_semantics.py +165 -0
  22. bladeworks/core/effect_parameters.py +193 -0
  23. bladeworks/core/errors.py +21 -0
  24. bladeworks/core/filter_text.py +48 -0
  25. bladeworks/core/geometry.py +1711 -0
  26. bladeworks/core/group_surface.py +664 -0
  27. bladeworks/core/keyer.py +280 -0
  28. bladeworks/core/masks.py +453 -0
  29. bladeworks/core/missing_media.py +130 -0
  30. bladeworks/core/model.py +985 -0
  31. bladeworks/core/multicam_execution.py +496 -0
  32. bladeworks/core/output_color.py +52 -0
  33. bladeworks/core/parser.py +1257 -0
  34. bladeworks/core/pixel_domains.py +433 -0
  35. bladeworks/core/render_profile.py +311 -0
  36. bladeworks/core/render_sources.py +475 -0
  37. bladeworks/core/report.py +118 -0
  38. bladeworks/core/retime.py +967 -0
  39. bladeworks/core/retime_execution.py +1515 -0
  40. bladeworks/core/spatial_intrinsics.py +1749 -0
  41. bladeworks/core/story_containers.py +524 -0
  42. bladeworks/core/story_ir.py +1042 -0
  43. bladeworks/core/text.py +1032 -0
  44. bladeworks/core/text_templates.py +934 -0
  45. bladeworks/data/FCPXML_RENDER_CAPABILITIES.yaml +1797 -0
  46. bladeworks/effects/__init__.py +1 -0
  47. bladeworks/effects/contract.py +162 -0
  48. bladeworks/examples/__init__.py +80 -0
  49. bladeworks/examples/color.fcpxmld/Info.fcpxml +17 -0
  50. bladeworks/examples/color.fcpxmld/Media/a.mp4 +0 -0
  51. bladeworks/examples/crop_or_transition.fcpxmld/Info.fcpxml +13 -0
  52. bladeworks/examples/crop_or_transition.fcpxmld/Media/a.mp4 +0 -0
  53. bladeworks/examples/crop_or_transition.fcpxmld/Media/b.mp4 +0 -0
  54. bladeworks/examples/manual_qa/generate_2h_qa_fixtures.py +237 -0
  55. bladeworks/examples/manual_qa/missing_media_lab.fcpxmld/Info.fcpxml +28 -0
  56. bladeworks/examples/manual_qa/missing_media_lab.fcpxmld/Media/night_signal.mp4 +0 -0
  57. bladeworks/examples/manual_qa/portrait_social.fcpxmld/Info.fcpxml +44 -0
  58. bladeworks/examples/manual_qa/portrait_social.fcpxmld/Media/city_grid.mp4 +0 -0
  59. bladeworks/examples/manual_qa/portrait_social.fcpxmld/Media/golden_hour.mp4 +0 -0
  60. bladeworks/examples/manual_qa/studio_showcase.fcpxmld/Info.fcpxml +113 -0
  61. bladeworks/examples/manual_qa/studio_showcase.fcpxmld/Media/city_grid.mp4 +0 -0
  62. bladeworks/examples/manual_qa/studio_showcase.fcpxmld/Media/golden_hour.mp4 +0 -0
  63. bladeworks/examples/manual_qa/studio_showcase.fcpxmld/Media/night_signal.mp4 +0 -0
  64. bladeworks/examples/single_clip.fcpxmld/Info.fcpxml +9 -0
  65. bladeworks/examples/single_clip.fcpxmld/Media/a.mp4 +0 -0
  66. bladeworks/examples/spine_and_lane.fcpxmld/Info.fcpxml +13 -0
  67. bladeworks/examples/spine_and_lane.fcpxmld/Media/a.mp4 +0 -0
  68. bladeworks/examples/spine_and_lane.fcpxmld/Media/b.mp4 +0 -0
  69. bladeworks/examples/transform.fcpxmld/Info.fcpxml +11 -0
  70. bladeworks/examples/transform.fcpxmld/Media/a.mp4 +0 -0
  71. bladeworks/executor.py +293 -0
  72. bladeworks/preview/__init__.py +11 -0
  73. bladeworks/preview/application.py +84 -0
  74. bladeworks/preview/audio.py +104 -0
  75. bladeworks/preview/capabilities.py +457 -0
  76. bladeworks/preview/contracts.py +157 -0
  77. bladeworks/preview/events.py +55 -0
  78. bladeworks/preview/export.py +57 -0
  79. bladeworks/preview/history.py +197 -0
  80. bladeworks/preview/main.py +19 -0
  81. bladeworks/preview/media.py +23 -0
  82. bladeworks/preview/media_library.py +381 -0
  83. bladeworks/preview/media_reporting.py +55 -0
  84. bladeworks/preview/media_routes.py +155 -0
  85. bladeworks/preview/media_visuals.py +225 -0
  86. bladeworks/preview/producer.py +211 -0
  87. bladeworks/preview/provider.py +74 -0
  88. bladeworks/preview/rawframe.py +218 -0
  89. bladeworks/preview/render_jobs.py +311 -0
  90. bladeworks/preview/routes.py +375 -0
  91. bladeworks/preview/runner.py +349 -0
  92. bladeworks/preview/security.py +61 -0
  93. bladeworks/preview/service.py +234 -0
  94. bladeworks/preview/session.py +457 -0
  95. bladeworks/preview/source.py +603 -0
  96. bladeworks/preview/source_routes.py +151 -0
  97. bladeworks/preview/studio.py +77 -0
  98. bladeworks/preview/webrtc.py +295 -0
  99. bladeworks/spatial_luts/manifest.json +17 -0
  100. bladeworks/spatial_luts/rec2020_hlg_to_rec709_sdr_v1.cube +4917 -0
  101. bladeworks/spatial_luts/rec2020_pq_to_rec709_sdr_v1.cube +4917 -0
  102. bladeworks/studio_static/__init__.py +1 -0
  103. bladeworks/studio_static/app.js +6519 -0
  104. bladeworks/studio_static/build-id.js +1 -0
  105. bladeworks/studio_static/capability-ui.js +68 -0
  106. bladeworks/studio_static/clip-state.js +335 -0
  107. bladeworks/studio_static/fcpxml.js +3313 -0
  108. bladeworks/studio_static/fixtures.js +393 -0
  109. bladeworks/studio_static/history.js +50 -0
  110. bladeworks/studio_static/index.html +24 -0
  111. bladeworks/studio_static/magnetic-timeline.js +1194 -0
  112. bladeworks/studio_static/mask-ui.js +189 -0
  113. bladeworks/studio_static/media-visuals.js +224 -0
  114. bladeworks/studio_static/mock-capabilities.js +409 -0
  115. bladeworks/studio_static/retime-ui.js +151 -0
  116. bladeworks/studio_static/runtime.js +1652 -0
  117. bladeworks/studio_static/scope-ui.js +150 -0
  118. bladeworks/studio_static/styles.css +1558 -0
  119. bladeworks/studio_static/templates/catalog.js +77 -0
  120. bladeworks/studio_static/templates/icons.js +84 -0
  121. bladeworks/studio_static/templates/inspector.js +553 -0
  122. bladeworks/studio_static/templates/library.js +61 -0
  123. bladeworks/studio_static/templates/media.js +185 -0
  124. bladeworks/studio_static/templates/shared.js +43 -0
  125. bladeworks/studio_static/templates/shell.js +94 -0
  126. bladeworks/studio_static/templates/timeline.js +377 -0
  127. bladeworks/studio_static/templates/topbar.js +83 -0
  128. bladeworks/studio_static/templates/types.js +7 -0
  129. bladeworks/studio_static/templates/viewer.js +174 -0
  130. bladeworks/studio_static/templates.js +31 -0
  131. bladeworks/studio_static/types.js +18 -0
  132. bladeworks/studio_static/ui.js +97 -0
  133. bladeworks/studio_static/viewer-geometry.js +55 -0
  134. bladeworks/tensor/__init__.py +131 -0
  135. bladeworks/tensor/audio_delivery.py +280 -0
  136. bladeworks/tensor/audio_pyav.py +360 -0
  137. bladeworks/tensor/blend.py +217 -0
  138. bladeworks/tensor/color.py +53 -0
  139. bladeworks/tensor/composite.py +33 -0
  140. bladeworks/tensor/decode.py +1309 -0
  141. bladeworks/tensor/decode_policy.py +363 -0
  142. bladeworks/tensor/effects.py +322 -0
  143. bladeworks/tensor/encode.py +614 -0
  144. bladeworks/tensor/errors.py +32 -0
  145. bladeworks/tensor/expr.py +775 -0
  146. bladeworks/tensor/fx_basic.py +946 -0
  147. bladeworks/tensor/fx_branched.py +640 -0
  148. bladeworks/tensor/fx_cohort.py +404 -0
  149. bladeworks/tensor/fx_color.py +851 -0
  150. bladeworks/tensor/fx_keyer.py +140 -0
  151. bladeworks/tensor/fx_mask.py +352 -0
  152. bladeworks/tensor/fx_warp.py +337 -0
  153. bladeworks/tensor/hdr.py +122 -0
  154. bladeworks/tensor/pipeline.py +401 -0
  155. bladeworks/tensor/plan.py +2231 -0
  156. bladeworks/tensor/renderer.py +1199 -0
  157. bladeworks/tensor/requirements.txt +28 -0
  158. bladeworks/tensor/resolution.py +246 -0
  159. bladeworks/tensor/sampler.py +598 -0
  160. bladeworks/tensor/support.py +169 -0
  161. bladeworks/tensor/swscale_fixedpoint.py +181 -0
  162. bladeworks/tensor/tr_equirect.py +420 -0
  163. bladeworks/tensor/tr_handlers.py +443 -0
  164. bladeworks/tensor/tr_phase5.py +697 -0
  165. bladeworks/tensor/transitions.py +424 -0
  166. bladeworks/transitions/__init__.py +10 -0
  167. bladeworks/transitions/carryovers.py +299 -0
  168. bladeworks/transitions/cohort.py +981 -0
  169. bladeworks/transitions/contract.py +490 -0
  170. bladeworks/transitions/equirectangular.py +915 -0
  171. bladeworks/transitions/light_deco.py +141 -0
  172. bladeworks/transitions/panel_motion.py +495 -0
  173. bladeworks/transitions/stock.py +270 -0
  174. bladeworks-0.1.0.dist-info/METADATA +66 -0
  175. bladeworks-0.1.0.dist-info/RECORD +178 -0
  176. bladeworks-0.1.0.dist-info/WHEEL +5 -0
  177. bladeworks-0.1.0.dist-info/entry_points.txt +2 -0
  178. bladeworks-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,490 @@
1
+ """The narrow ``spell-transition-v1`` shader and parameter contract.
2
+
3
+ Architecture map
4
+ ================
5
+
6
+ Registry parameter schema -> strict typed values -> one push-constant ``vec4``
7
+ slot
8
+ per parameter -> immutable hexadecimal parameter blob consumed by the pinned
9
+ FFmpeg filter.
10
+
11
+ The shader side receives exactly two sampled images, one storage output image,
12
+ normalized top-left-origin UV coordinates, exact output dimensions/aspect, and
13
+ progress clamped to ``[0, 1]``. The generated wrapper enforces the endpoints.
14
+
15
+ Why this exists
16
+ ---------------
17
+ GL Transitions provides a useful creative shape, but WebGL shaders are not
18
+ Vulkan compute modules. This product-owned ABI documents the smaller subset we
19
+ can compile, validate, reproduce, and safely select from FCPXML.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import math
25
+ import struct
26
+ from dataclasses import dataclass
27
+ from typing import Any, Mapping, Sequence
28
+
29
+ from ..core.errors import FCPXMLCompileError
30
+ from ..core.model import Parameter
31
+
32
+
33
+ ABI_VERSION = "spell-transition-v1"
34
+ PARAMETER_LAYOUT = "push-constant-vec4-slot-v1"
35
+ MAX_PARAMETER_SLOTS = 4
36
+ EXPECTED_DESCRIPTOR_BINDINGS = {
37
+ 0: "outgoing combined image sampler",
38
+ 1: "incoming combined image sampler",
39
+ 2: "RGBA16 output storage image",
40
+ }
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class WorkingImageContract:
45
+ color_primaries: str = "ITU-R BT.709"
46
+ transfer: str = "linear-light"
47
+ pixel_representation: str = "RGBA 16-bit UNORM"
48
+ alpha: str = "straight"
49
+ uv_origin: str = "top-left"
50
+
51
+
52
+ WORKING_IMAGE = WorkingImageContract()
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class AllowedParameterContext:
57
+ """One exact semantic-key context for a materialized no-op value."""
58
+
59
+ when: tuple[tuple[str, float], ...]
60
+ allowed: tuple[float, ...]
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class SemanticParameterAlias:
65
+ """One exact published value mapped to a proved portable baseline."""
66
+
67
+ source: float
68
+ target: float
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class ParameterSpec:
73
+ key: str
74
+ name: str
75
+ type: str
76
+ default: Any
77
+ minimum: float | None = None
78
+ maximum: float | None = None
79
+ allowed: tuple[float, ...] | None = None
80
+ transport: str = "semantic"
81
+ allowed_contexts: tuple[AllowedParameterContext, ...] = ()
82
+ semantic_aliases: tuple[SemanticParameterAlias, ...] = ()
83
+
84
+ @property
85
+ def components(self) -> int:
86
+ return {"scalar": 1, "bool": 1, "vec2": 2, "vec3": 3, "vec4": 4, "color": 4}[self.type]
87
+
88
+
89
+ def parse_parameter_specs(
90
+ raw: Mapping[str, Any],
91
+ *,
92
+ max_slots: int | None = MAX_PARAMETER_SLOTS,
93
+ ) -> tuple[ParameterSpec, ...]:
94
+ """Validate registry declarations and freeze their serialization order.
95
+
96
+ Main callers:
97
+ - Capability-registry validation.
98
+ - The compiler when it resolves FCPXML values.
99
+ - The offline wrapper builder.
100
+ """
101
+
102
+ specs: list[ParameterSpec] = []
103
+ for key, definition in raw.items():
104
+ if not isinstance(definition, Mapping):
105
+ raise FCPXMLCompileError(f"transition parameter {key!r} must be an object")
106
+ parameter_type = str(definition.get("type", ""))
107
+ if parameter_type not in {"scalar", "vec2", "vec3", "vec4", "bool", "color"}:
108
+ raise FCPXMLCompileError(f"transition parameter {key!r} has unsupported type {parameter_type!r}")
109
+ if "default" not in definition:
110
+ raise FCPXMLCompileError(f"transition parameter {key!r} requires a default")
111
+ transport = str(definition.get("transport", "semantic"))
112
+ if transport not in {"semantic", "ignored"}:
113
+ raise FCPXMLCompileError(
114
+ f"transition parameter {key!r} has unsupported transport {transport!r}"
115
+ )
116
+ raw_contexts = definition.get("allowed_contexts", [])
117
+ if not isinstance(raw_contexts, list):
118
+ raise FCPXMLCompileError(
119
+ f"transition parameter {key!r} allowed_contexts must be a list"
120
+ )
121
+ contexts: list[AllowedParameterContext] = []
122
+ for index, raw_context in enumerate(raw_contexts):
123
+ if not isinstance(raw_context, Mapping):
124
+ raise FCPXMLCompileError(
125
+ f"transition parameter {key!r} context {index} must be an object"
126
+ )
127
+ when = raw_context.get("when")
128
+ allowed = raw_context.get("allowed")
129
+ if not isinstance(when, Mapping) or not when:
130
+ raise FCPXMLCompileError(
131
+ f"transition parameter {key!r} context {index} requires non-empty when"
132
+ )
133
+ if not isinstance(allowed, list) or not allowed:
134
+ raise FCPXMLCompileError(
135
+ f"transition parameter {key!r} context {index} requires allowed values"
136
+ )
137
+ try:
138
+ frozen_when = tuple((str(item), float(value)) for item, value in when.items())
139
+ frozen_allowed = tuple(float(value) for value in allowed)
140
+ except (TypeError, ValueError) as exc:
141
+ raise FCPXMLCompileError(
142
+ f"transition parameter {key!r} context {index} must be numeric"
143
+ ) from exc
144
+ if any(not math.isfinite(value) for _, value in frozen_when) or any(
145
+ not math.isfinite(value) for value in frozen_allowed
146
+ ):
147
+ raise FCPXMLCompileError(
148
+ f"transition parameter {key!r} context {index} must be finite"
149
+ )
150
+ contexts.append(
151
+ AllowedParameterContext(when=frozen_when, allowed=frozen_allowed)
152
+ )
153
+ raw_aliases = definition.get("semantic_aliases", {})
154
+ if not isinstance(raw_aliases, Mapping):
155
+ raise FCPXMLCompileError(
156
+ f"transition parameter {key!r} semantic_aliases must be an object"
157
+ )
158
+ aliases: list[SemanticParameterAlias] = []
159
+ try:
160
+ for source, target in raw_aliases.items():
161
+ aliases.append(
162
+ SemanticParameterAlias(source=float(source), target=float(target))
163
+ )
164
+ except (TypeError, ValueError) as exc:
165
+ raise FCPXMLCompileError(
166
+ f"transition parameter {key!r} semantic aliases must be numeric"
167
+ ) from exc
168
+ if any(
169
+ not math.isfinite(item.source) or not math.isfinite(item.target)
170
+ for item in aliases
171
+ ):
172
+ raise FCPXMLCompileError(
173
+ f"transition parameter {key!r} semantic aliases must be finite"
174
+ )
175
+ spec = ParameterSpec(
176
+ key=str(key),
177
+ name=str(definition.get("name", key)),
178
+ type=parameter_type,
179
+ default=definition["default"],
180
+ minimum=float(definition["minimum"]) if "minimum" in definition else None,
181
+ maximum=float(definition["maximum"]) if "maximum" in definition else None,
182
+ allowed=(
183
+ tuple(float(value) for value in definition["allowed"])
184
+ if "allowed" in definition
185
+ else None
186
+ ),
187
+ transport=transport,
188
+ allowed_contexts=tuple(contexts),
189
+ semantic_aliases=tuple(aliases),
190
+ )
191
+ if spec.allowed is not None and not spec.allowed:
192
+ raise FCPXMLCompileError(
193
+ f"transition parameter {key!r} requires at least one allowed value"
194
+ )
195
+ if spec.transport == "ignored" and spec.allowed is None:
196
+ raise FCPXMLCompileError(
197
+ f"ignored transition parameter {key!r} requires exact allowed values"
198
+ )
199
+ if spec.allowed_contexts and spec.transport != "ignored":
200
+ raise FCPXMLCompileError(
201
+ f"transition parameter {key!r} contexts are reserved for ignored inputs"
202
+ )
203
+ if spec.semantic_aliases and (
204
+ spec.transport != "semantic" or spec.type != "scalar"
205
+ ):
206
+ raise FCPXMLCompileError(
207
+ f"transition parameter {key!r} semantic aliases require a semantic scalar"
208
+ )
209
+ alias_sources = {item.source for item in spec.semantic_aliases}
210
+ if len(alias_sources) != len(spec.semantic_aliases):
211
+ raise FCPXMLCompileError(
212
+ f"transition parameter {key!r} repeats a semantic alias source"
213
+ )
214
+ if any(
215
+ item.source == item.target
216
+ or item.source not in (spec.allowed or ())
217
+ or item.target not in (spec.allowed or ())
218
+ or item.target in alias_sources
219
+ for item in spec.semantic_aliases
220
+ ):
221
+ raise FCPXMLCompileError(
222
+ f"transition parameter {key!r} has an invalid bounded semantic alias"
223
+ )
224
+ if any(
225
+ value not in (spec.allowed or ())
226
+ for context in spec.allowed_contexts
227
+ for value in context.allowed
228
+ ):
229
+ raise FCPXMLCompileError(
230
+ f"transition parameter {key!r} context exceeds its allowed values"
231
+ )
232
+ _coerce_value(spec, spec.default)
233
+ specs.append(spec)
234
+ by_key = {spec.key: spec for spec in specs}
235
+ for spec in specs:
236
+ for context in spec.allowed_contexts:
237
+ for context_key, _ in context.when:
238
+ referenced = by_key.get(context_key)
239
+ if referenced is None:
240
+ raise FCPXMLCompileError(
241
+ f"transition parameter {spec.key!r} context references unknown key {context_key!r}"
242
+ )
243
+ if referenced.components != 1 or referenced.transport != "semantic":
244
+ raise FCPXMLCompileError(
245
+ f"transition parameter {spec.key!r} context key {context_key!r} must be semantic scalar"
246
+ )
247
+ if max_slots is not None and len(specs) > max_slots:
248
+ raise FCPXMLCompileError(
249
+ f"spell-transition-v1 permits at most {max_slots} parameter slots"
250
+ )
251
+ return tuple(specs)
252
+
253
+
254
+ def resolve_parameter_values(
255
+ specs: Sequence[ParameterSpec],
256
+ supplied: tuple[Parameter, ...],
257
+ ) -> dict[str, bool | float | tuple[float, ...]]:
258
+ """Resolve only declared FCPXML parameters and reject invalid values.
259
+
260
+ FCPXML may choose bounded values. It cannot add uniforms or influence the
261
+ binary layout. Unknown, ambiguous, malformed, non-finite, and out-of-range
262
+ values fail compilation instead of being silently ignored or clamped.
263
+ """
264
+
265
+ by_key = {spec.key: spec for spec in specs}
266
+ by_name = {spec.name.casefold(): spec for spec in specs}
267
+ values: dict[str, bool | float | tuple[float, ...]] = {
268
+ spec.key: _apply_semantic_alias(spec, _coerce_value(spec, spec.default))
269
+ for spec in specs
270
+ }
271
+ seen: set[str] = set()
272
+ for item in supplied:
273
+ if item.keyframes:
274
+ raise FCPXMLCompileError(
275
+ f"transition parameter {item.name or item.key or 'unnamed'!r} "
276
+ "uses keyframes, which the stock transition runtime does not support"
277
+ )
278
+ if item.value is None:
279
+ continue
280
+ spec = by_key.get(item.key or "")
281
+ if spec is None and item.name:
282
+ spec = by_name.get(item.name.casefold())
283
+ if spec is None:
284
+ raise FCPXMLCompileError(
285
+ f"arbitrary transition parameter {item.name or item.key or 'unnamed'!r} is not declared"
286
+ )
287
+ if spec.key in seen:
288
+ raise FCPXMLCompileError(f"arbitrary transition parameter {spec.key!r} is supplied more than once")
289
+ seen.add(spec.key)
290
+ values[spec.key] = _apply_semantic_alias(
291
+ spec, _coerce_value(spec, item.value)
292
+ )
293
+ _validate_ignored_contexts(specs, values, seen)
294
+ return values
295
+
296
+
297
+ def semantic_parameter_values(
298
+ specs: Sequence[ParameterSpec],
299
+ values: Mapping[str, bool | float | tuple[float, ...]],
300
+ ) -> dict[str, bool | float | tuple[float, ...]]:
301
+ """Strip exact serialization-only inputs before semantic plan building."""
302
+
303
+ return {
304
+ spec.key: values[spec.key]
305
+ for spec in specs
306
+ if spec.transport == "semantic"
307
+ }
308
+
309
+
310
+ def supplied_ignored_parameter_values(
311
+ specs: Sequence[ParameterSpec],
312
+ supplied: Sequence[Parameter],
313
+ values: Mapping[str, bool | float | tuple[float, ...]],
314
+ ) -> tuple[tuple[ParameterSpec, bool | float | tuple[float, ...]], ...]:
315
+ """Return only explicitly materialized, validated no-op inputs."""
316
+
317
+ by_key = {spec.key: spec for spec in specs}
318
+ by_name = {spec.name.casefold(): spec for spec in specs}
319
+ found: list[tuple[ParameterSpec, bool | float | tuple[float, ...]]] = []
320
+ for item in supplied:
321
+ if item.value is None:
322
+ continue
323
+ spec = by_key.get(item.key or "")
324
+ if spec is None and item.name:
325
+ spec = by_name.get(item.name.casefold())
326
+ if spec is not None and spec.transport == "ignored":
327
+ found.append((spec, values[spec.key]))
328
+ return tuple(found)
329
+
330
+
331
+ def applied_semantic_parameter_aliases(
332
+ specs: Sequence[ParameterSpec],
333
+ supplied: Sequence[Parameter],
334
+ ) -> tuple[tuple[ParameterSpec, float, float], ...]:
335
+ """Return effective values that use a bounded semantic alias.
336
+
337
+ Main callers:
338
+ - The compiler compatibility report, after normal strict resolution.
339
+
340
+ Why this exists:
341
+ Final Cut's adaptive ``Automatic`` direction is a published value, but its
342
+ broader context-selection rule is not reproduced. Reporting the exact
343
+ proved alias keeps that approximation visible instead of silently treating
344
+ Automatic as a fully calibrated independent direction.
345
+ """
346
+
347
+ by_key = {spec.key: spec for spec in specs}
348
+ by_name = {spec.name.casefold(): spec for spec in specs}
349
+ raw_values: dict[str, bool | float | tuple[float, ...]] = {
350
+ spec.key: _coerce_value(spec, spec.default) for spec in specs
351
+ }
352
+ for item in supplied:
353
+ if item.value is None:
354
+ continue
355
+ spec = by_key.get(item.key or "")
356
+ if spec is None and item.name:
357
+ spec = by_name.get(item.name.casefold())
358
+ if spec is None:
359
+ continue
360
+ raw_values[spec.key] = _coerce_value(spec, item.value)
361
+ found: list[tuple[ParameterSpec, float, float]] = []
362
+ for spec in specs:
363
+ if not spec.semantic_aliases:
364
+ continue
365
+ raw = raw_values[spec.key]
366
+ if isinstance(raw, (bool, tuple)):
367
+ continue
368
+ target = {
369
+ alias.source: alias.target for alias in spec.semantic_aliases
370
+ }.get(raw)
371
+ if target is not None:
372
+ found.append((spec, raw, target))
373
+ return tuple(found)
374
+
375
+
376
+ def _validate_ignored_contexts(
377
+ specs: Sequence[ParameterSpec],
378
+ values: Mapping[str, bool | float | tuple[float, ...]],
379
+ supplied_keys: set[str],
380
+ ) -> None:
381
+ """Reject a materialized no-op outside its exact FCP-owned context."""
382
+
383
+ for spec in specs:
384
+ if spec.key not in supplied_keys or not spec.allowed_contexts:
385
+ continue
386
+ value = values[spec.key]
387
+ if isinstance(value, (bool, tuple)):
388
+ raise FCPXMLCompileError(
389
+ f"ignored transition parameter {spec.key!r} must resolve to one scalar"
390
+ )
391
+ matching = [
392
+ context
393
+ for context in spec.allowed_contexts
394
+ if all(values[key] == expected for key, expected in context.when)
395
+ ]
396
+ if not matching or all(value not in context.allowed for context in matching):
397
+ rendered = {key: values[key] for context in spec.allowed_contexts for key, _ in context.when}
398
+ raise FCPXMLCompileError(
399
+ f"ignored transition parameter {spec.key!r} value {value} is invalid "
400
+ f"for semantic context {rendered}"
401
+ )
402
+
403
+
404
+ def _apply_semantic_alias(
405
+ spec: ParameterSpec,
406
+ value: bool | float | tuple[float, ...],
407
+ ) -> bool | float | tuple[float, ...]:
408
+ """Map one exact published alias after normal type/bounds validation."""
409
+
410
+ if isinstance(value, (bool, tuple)):
411
+ return value
412
+ return {
413
+ alias.source: alias.target for alias in spec.semantic_aliases
414
+ }.get(value, value)
415
+
416
+
417
+ def pack_parameter_blob(
418
+ specs: Sequence[ParameterSpec],
419
+ values: Mapping[str, bool | float | tuple[float, ...]],
420
+ ) -> bytes:
421
+ """Pack one little-endian 16-byte push-constant slot per parameter."""
422
+
423
+ output = bytearray()
424
+ for spec in specs:
425
+ raw = values[spec.key]
426
+ if isinstance(raw, bool):
427
+ components = [1.0 if raw else 0.0]
428
+ elif isinstance(raw, tuple):
429
+ components = list(raw)
430
+ else:
431
+ components = [float(raw)]
432
+ components.extend([0.0] * (4 - len(components)))
433
+ output.extend(struct.pack("<4f", *components))
434
+ return bytes(output)
435
+
436
+
437
+ def _coerce_value(spec: ParameterSpec, raw: Any) -> bool | float | tuple[float, ...]:
438
+ if spec.type == "bool":
439
+ if isinstance(raw, bool):
440
+ return raw
441
+ normalized = str(raw).strip().casefold()
442
+ if normalized in {"1", "true", "yes"}:
443
+ return True
444
+ if normalized in {"0", "false", "no"}:
445
+ return False
446
+ raise FCPXMLCompileError(f"transition parameter {spec.key!r} expects a boolean")
447
+
448
+ if isinstance(raw, (list, tuple)):
449
+ pieces = list(raw)
450
+ else:
451
+ pieces = str(raw).replace(",", " ").split()
452
+ if spec.allowed is not None and spec.components == 1 and len(pieces) > 1:
453
+ # Final Cut normalizes menu values to strings such as
454
+ # ``1 (CounterClockwise)``. The label is localized display text;
455
+ # only the leading numeric value is the serialization contract.
456
+ # Accept exactly that one parenthesized suffix shape, then still
457
+ # validate the numeric value against the registry allow-list.
458
+ rendered = str(raw).strip()
459
+ numeric = rendered.split(maxsplit=1)[0]
460
+ suffix = rendered[len(numeric) :].strip()
461
+ if not (suffix.startswith("(") and suffix.endswith(")")):
462
+ raise FCPXMLCompileError(
463
+ f"transition parameter {spec.key!r} has an invalid enum label shape"
464
+ )
465
+ pieces = [numeric]
466
+ if len(pieces) != spec.components:
467
+ raise FCPXMLCompileError(
468
+ f"transition parameter {spec.key!r} expects {spec.components} numeric component(s)"
469
+ )
470
+ try:
471
+ numbers = tuple(float(value) for value in pieces)
472
+ except (TypeError, ValueError) as exc:
473
+ raise FCPXMLCompileError(f"transition parameter {spec.key!r} is not numeric") from exc
474
+ if any(not math.isfinite(value) for value in numbers):
475
+ raise FCPXMLCompileError(f"transition parameter {spec.key!r} must be finite")
476
+ if spec.minimum is not None and any(value < spec.minimum for value in numbers):
477
+ raise FCPXMLCompileError(
478
+ f"transition parameter {spec.key!r} is below minimum {spec.minimum}"
479
+ )
480
+ if spec.maximum is not None and any(value > spec.maximum for value in numbers):
481
+ raise FCPXMLCompileError(
482
+ f"transition parameter {spec.key!r} is above maximum {spec.maximum}"
483
+ )
484
+ if spec.allowed is not None and any(value not in spec.allowed for value in numbers):
485
+ raise FCPXMLCompileError(
486
+ f"transition parameter {spec.key!r} is outside allowed values {spec.allowed}"
487
+ )
488
+ if spec.type == "scalar":
489
+ return numbers[0]
490
+ return numbers