reactor-runtime 2.7.6__tar.gz → 2.7.8__tar.gz

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 (140) hide show
  1. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/PKG-INFO +1 -1
  2. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/pyproject.toml +1 -1
  3. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/config.py +36 -1
  4. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/defaults.py +12 -0
  5. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/internal/output_buffer.py +46 -87
  6. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/recording/session_recorder.py +14 -2
  7. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/runtime_api.py +262 -14
  8. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/runtimes/headless/headless_runtime.py +35 -17
  9. reactor_runtime-2.7.8/src/reactor_runtime/schema_validator.py +231 -0
  10. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/utils/messages.py +4 -0
  11. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime.egg-info/PKG-INFO +1 -1
  12. reactor_runtime-2.7.6/src/reactor_runtime/schema_validator.py +0 -123
  13. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/README.md +0 -0
  14. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/setup.cfg +0 -0
  15. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/api/__init__.py +0 -0
  16. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/__init__.py +0 -0
  17. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/experiment/__init__.py +0 -0
  18. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/experiment/session.py +0 -0
  19. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/__init__.py +0 -0
  20. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/driver/__init__.py +0 -0
  21. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/driver/pipeline_executor.py +0 -0
  22. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/driver/step_result.py +0 -0
  23. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/events/__init__.py +0 -0
  24. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/events/connected.py +0 -0
  25. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/events/event.py +0 -0
  26. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/events/messages.py +0 -0
  27. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/events/upload.py +0 -0
  28. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/internal/__init__.py +0 -0
  29. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
  30. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
  31. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/model/__init__.py +0 -0
  32. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/model/decorators.py +0 -0
  33. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/model/handlers.py +0 -0
  34. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
  35. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
  36. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
  37. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
  38. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
  39. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
  40. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
  41. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/tracks/input.py +0 -0
  42. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/tracks/output.py +0 -0
  43. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/interface/upload.py +0 -0
  44. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/model_state.py +0 -0
  45. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/profiling/__init__.py +0 -0
  46. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
  47. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/profiling/backends/base.py +0 -0
  48. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/profiling/backends/file.py +0 -0
  49. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
  50. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/profiling/helpers.py +0 -0
  51. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/profiling/nvml_sampler.py +0 -0
  52. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
  53. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
  54. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/profiling/profiler.py +0 -0
  55. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/profiling/singleton.py +0 -0
  56. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/profiling/torch_chunk_profiler.py +0 -0
  57. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/recording/__init__.py +0 -0
  58. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
  59. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/recording/chunk_uploader.py +0 -0
  60. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/recording/config.py +0 -0
  61. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/recording/markers.py +0 -0
  62. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/recording/sinks.py +0 -0
  63. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/recording/track_resolver.py +0 -0
  64. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/runtimes/headless/config.py +0 -0
  65. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/runtimes/headless/input_feeder.py +0 -0
  66. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/runtimes/http/config.py +0 -0
  67. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/runtimes/http/http_runtime.py +0 -0
  68. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/runtimes/http/types.py +0 -0
  69. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/schema.py +0 -0
  70. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/serve/__init__.py +0 -0
  71. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/serve/__main__.py +0 -0
  72. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/serve/commands/__init__.py +0 -0
  73. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/serve/commands/run.py +0 -0
  74. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/serve/commands/schema.py +0 -0
  75. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/serve/main.py +0 -0
  76. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/serve/utils/__init__.py +0 -0
  77. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/serve/utils/config.py +0 -0
  78. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/serve/utils/runtime.py +0 -0
  79. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/__init__.py +0 -0
  80. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/aiortc/__init__.py +0 -0
  81. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/aiortc/audio_track.py +0 -0
  82. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/aiortc/client.py +0 -0
  83. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/aiortc/frame_conversion.py +0 -0
  84. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/aiortc/ice_connection.py +0 -0
  85. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/aiortc/video_track.py +0 -0
  86. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/config.py +0 -0
  87. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/events.py +0 -0
  88. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
  89. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/client.py +0 -0
  90. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +0 -0
  91. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
  92. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
  93. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +0 -0
  94. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
  95. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
  96. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/decoders/opus.py +0 -0
  97. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
  98. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
  99. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
  100. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +0 -0
  101. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
  102. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
  103. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +0 -0
  104. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +0 -0
  105. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +0 -0
  106. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +0 -0
  107. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +0 -0
  108. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
  109. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +0 -0
  110. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/probes/__init__.py +0 -0
  111. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/probes/fps_probe.py +0 -0
  112. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
  113. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +0 -0
  114. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
  115. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
  116. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +0 -0
  117. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +0 -0
  118. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +0 -0
  119. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
  120. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
  121. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
  122. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
  123. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
  124. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/sender/video.py +0 -0
  125. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/settings.py +0 -0
  126. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
  127. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/ice_uris.py +0 -0
  128. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/interface.py +0 -0
  129. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/media.py +0 -0
  130. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/transports/types.py +0 -0
  131. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/utils/launch.py +0 -0
  132. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/utils/loader.py +0 -0
  133. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/utils/log.py +0 -0
  134. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/utils/paths.py +0 -0
  135. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/utils/ports.py +0 -0
  136. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime/utils/typing.py +0 -0
  137. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime.egg-info/SOURCES.txt +0 -0
  138. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
  139. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime.egg-info/requires.txt +0 -0
  140. {reactor_runtime-2.7.6 → reactor_runtime-2.7.8}/src/reactor_runtime.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactor_runtime
3
- Version: 2.7.6
3
+ Version: 2.7.8
4
4
  Summary: Reactor runtime with public model API
5
5
  Author-email: Reactor <team@reactor.inc>
6
6
  Requires-Python: >=3.9
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactor_runtime"
7
- version = "2.7.6"
7
+ version = "2.7.8"
8
8
  description = "Reactor runtime with public model API"
9
9
  authors = [
10
10
  { name = "Reactor", email = "team@reactor.inc" }
@@ -81,6 +81,37 @@ def _reset_legacy_warning_for_tests() -> None:
81
81
  _legacy_warned = False
82
82
 
83
83
 
84
+ @dataclass
85
+ class ModerationYamlConfig:
86
+ """Top-level ``moderation:`` block parsed from ``reactor.yaml``.
87
+
88
+ Model authors declare here whether content moderation runs for their
89
+ model. Defaults to ``enabled: false`` so existing models opt in
90
+ explicitly. Operational tuning (thresholds, concurrency, backend
91
+ choice, ``OPENAI_API_KEY``) stays in environment variables — this
92
+ block carries only the policy decision the model author owns.
93
+ """
94
+
95
+ enabled: bool = False
96
+
97
+ @classmethod
98
+ def from_dict(cls, raw: Optional[Dict[str, Any]]) -> "ModerationYamlConfig":
99
+ if not isinstance(raw, dict):
100
+ return cls()
101
+ enabled = raw.get("enabled", False)
102
+ # Strict bool check: `bool("false") is True` would silently flip
103
+ # moderation on for a YAML author who wrote `enabled: "false"`
104
+ # (quoted) or templated a `"0"` value. Safety-sensitive config —
105
+ # fail at parse time with a message that names the offending value.
106
+ if not isinstance(enabled, bool):
107
+ raise TypeError(
108
+ f"moderation.enabled in reactor.yaml must be a boolean, "
109
+ f"got {type(enabled).__name__}: {enabled!r}. "
110
+ 'Did you accidentally quote the value (e.g. "false")?'
111
+ )
112
+ return cls(enabled=enabled)
113
+
114
+
84
115
  @dataclass
85
116
  class ReactorConfig:
86
117
  """Parsed ``reactor.yaml``.
@@ -119,6 +150,7 @@ class ReactorConfig:
119
150
  config_overrides: List[str] = field(default_factory=list)
120
151
  weights_path: Optional[str] = None
121
152
  recording: RecordingConfig = field(default_factory=RecordingConfig)
153
+ moderation: ModerationYamlConfig = field(default_factory=ModerationYamlConfig)
122
154
 
123
155
  @classmethod
124
156
  def from_dict(cls, raw: Dict[str, Any]) -> "ReactorConfig":
@@ -135,8 +167,9 @@ class ReactorConfig:
135
167
  runtime_section = raw.get("runtime")
136
168
  model_section = raw.get("model")
137
169
 
138
- # Recording is a top-level sibling to runtime: / model:.
170
+ # Recording and moderation are top-level siblings to runtime: / model:.
139
171
  recording = RecordingConfig.from_dict(raw.get("recording"))
172
+ moderation = ModerationYamlConfig.from_dict(raw.get("moderation"))
140
173
 
141
174
  modern = isinstance(runtime_section, dict) or isinstance(model_section, dict)
142
175
 
@@ -161,6 +194,7 @@ class ReactorConfig:
161
194
  config=runtime_section.get("config"),
162
195
  weights_path=runtime_section.get("weights_path") or None,
163
196
  recording=recording,
197
+ moderation=moderation,
164
198
  )
165
199
 
166
200
  if "model" not in raw:
@@ -177,4 +211,5 @@ class ReactorConfig:
177
211
  config=raw.get("config"),
178
212
  weights_path=raw.get("weights_path") or None,
179
213
  recording=recording,
214
+ moderation=moderation,
180
215
  )
@@ -51,6 +51,7 @@ class FieldInfo:
51
51
  min_length: Optional[int] = None
52
52
  max_length: Optional[int] = None
53
53
  choices: Optional[List[Any]] = None
54
+ moderate: bool = True
54
55
 
55
56
 
56
57
  def InputField(
@@ -63,6 +64,7 @@ def InputField(
63
64
  min_length: Optional[int] = None,
64
65
  max_length: Optional[int] = None,
65
66
  choices: Optional[List[Any]] = None,
67
+ moderate: bool = True,
66
68
  ) -> Any:
67
69
  """Declare a default value and validation constraints for a field.
68
70
 
@@ -81,6 +83,13 @@ def InputField(
81
83
  min_length: Minimum length for string/sequence values.
82
84
  max_length: Maximum length for string/sequence values.
83
85
  choices: Exhaustive list of allowed values.
86
+ moderate: Whether to submit this field's value to the content
87
+ moderation backend (when moderation is enabled platform-wide).
88
+ Defaults to ``True``. Only effective for free-text ``str``
89
+ fields (those without ``choices=``) and ``UploadedFile``
90
+ fields — typed, enum, and numeric fields are never moderated
91
+ because the schema validator rejects out-of-set payloads
92
+ before they reach the model.
84
93
  """
85
94
  if default_factory is not None:
86
95
  raise TypeError(
@@ -95,6 +104,7 @@ def InputField(
95
104
  min_length=min_length,
96
105
  max_length=max_length,
97
106
  choices=choices,
107
+ moderate=moderate,
98
108
  )
99
109
 
100
110
 
@@ -227,3 +237,5 @@ def field_info_to_json_schema(schema: Dict[str, Any], info: FieldInfo) -> None:
227
237
  schema["maxLength"] = info.max_length
228
238
  if info.choices is not None:
229
239
  schema["enum"] = info.choices
240
+ if not info.moderate:
241
+ schema["x-reactor-moderate"] = False
@@ -105,25 +105,9 @@ def split_batch(bundle: MediaBundle) -> List[MediaBundle]:
105
105
  class _FlushMarker:
106
106
  """Sentinel placed in :class:`OutputBuffer`'s queue by :meth:`flush`.
107
107
 
108
- Carries no payload. When the emission loop dequeues an instance, it
109
- resets ``_last_emitted`` *in-thread* and falls through to this
110
- tick's empty-queue fallback (which dispatches a fresh
111
- ``_create_black_bundle()`` with ``duplicate=True``).
112
-
113
- The point of routing the session-boundary reset through the queue
114
- rather than writing ``_last_emitted = None`` directly from
115
- :meth:`flush` is to make the reset atomic with respect to the
116
- emission thread: only the emission thread reads or writes
117
- ``_last_emitted``, so the loop can never observe a half-applied
118
- "queue drained but cached frame not yet cleared" state. Down-stream
119
- callbacks already drop ``duplicate=True`` bundles (the wire
120
- callback in ``_send_out_app_bundle_sync`` short-circuits, and the
121
- :class:`~reactor_runtime.recording.session_recorder.SessionRecorder`
122
- skips them when ``skip_leading_black`` is on and
123
- ``recording_started`` is still False), so no stale frame would
124
- reach the client or recording even without this guarantee — but
125
- closing the race architecturally is cheaper than re-deriving that
126
- safety argument for every callback added in the future.
108
+ Consumed by the emission thread, which resets ``_last_emitted`` and
109
+ dispatches a fresh black bundle with ``is_fresh_black=True``. Doing
110
+ the reset in-thread keeps it atomic with the per-tick state read.
127
111
  """
128
112
 
129
113
 
@@ -152,13 +136,11 @@ class OutputBuffer:
152
136
  # Ordered list of per-tick observers. Callbacks fire in
153
137
  # registration order on every emission tick; an exception in
154
138
  # one callback is logged and does not block the others.
155
- self._callbacks: List[Callable[[MediaBundle, bool], None]] = []
139
+ self._callbacks: List[Callable[[MediaBundle, bool, bool], None]] = []
156
140
  self._callbacks_lock: threading.Lock = threading.Lock()
157
141
 
158
- # Queue items are normally MediaBundle, but flush() also puts a
159
- # _FlushMarker sentinel here. We type the queue as ``object`` so
160
- # both shapes are valid; the emission loop discriminates with
161
- # ``isinstance``.
142
+ # Holds MediaBundle plus the _FlushMarker sentinel; the
143
+ # emission loop discriminates by isinstance.
162
144
  self._q: queue.Queue[object] = queue.Queue(maxsize=queue_depth)
163
145
 
164
146
  # FPS control — store both rate and period to avoid 1/fps on every tick
@@ -182,29 +164,18 @@ class OutputBuffer:
182
164
  # Callbacks
183
165
  # ------------------------------------------------------------------
184
166
 
185
- def add_callback(self, fn: Callable[[MediaBundle, bool], None]) -> None:
186
- """Register a per-tick callback.
167
+ def add_callback(self, fn: Callable[[MediaBundle, bool, bool], None]) -> None:
168
+ """Register a per-tick callback ``(bundle, duplicate, is_fresh_black)``.
187
169
 
188
- Idempotent: registering the same ``fn`` twice leaves the
189
- callback list with a single entry. This mirrors
190
- :meth:`remove_callback` which has always been a no-op on
191
- already-unregistered functions — callers shouldn't have to
192
- track registration state.
193
-
194
- The callback receives ``(bundle, duplicate)`` on every emission
195
- tick. Callbacks fire in registration order and are isolated
196
- from each other: an exception raised by one is logged and the
197
- remaining callbacks still fire.
170
+ Idempotent — re-registering the same ``fn`` is a no-op.
171
+ Callbacks fire in registration order with exceptions isolated.
198
172
  """
199
173
  with self._callbacks_lock:
200
174
  if fn not in self._callbacks:
201
175
  self._callbacks.append(fn)
202
176
 
203
- def remove_callback(self, fn: Callable[[MediaBundle, bool], None]) -> None:
204
- """Deregister a previously added callback.
205
-
206
- No-op if *fn* was never registered.
207
- """
177
+ def remove_callback(self, fn: Callable[[MediaBundle, bool, bool], None]) -> None:
178
+ """Deregister a previously added callback (no-op if not registered)."""
208
179
  with self._callbacks_lock:
209
180
  try:
210
181
  self._callbacks.remove(fn)
@@ -320,27 +291,30 @@ class OutputBuffer:
320
291
  # Stage 3: Emission loop
321
292
  # ------------------------------------------------------------------
322
293
 
323
- def _dispatch(self, bundle: MediaBundle, duplicate: bool) -> None:
294
+ def _dispatch(
295
+ self,
296
+ bundle: MediaBundle,
297
+ duplicate: bool,
298
+ *,
299
+ is_fresh_black: bool = False,
300
+ ) -> None:
324
301
  """Fire every registered callback in order with error isolation.
325
302
 
326
- We snapshot the callback list under the lock so callbacks that
327
- register/deregister siblings during dispatch don't perturb the
328
- current tick. Each callback is wrapped so an exception in one
329
- does not block the others.
303
+ Callbacks receive ``(bundle, duplicate, is_fresh_black)``.
304
+ ``is_fresh_black`` is ``True`` only on the synthesised black
305
+ the sentinel path emits at session boundaries; callbacks
306
+ choose how to interpret that separately from ``duplicate``.
307
+ Exceptions in one callback are logged and don't block the
308
+ rest.
330
309
  """
331
310
  with self._callbacks_lock:
332
311
  callbacks = list(self._callbacks)
333
312
  for fn in callbacks:
334
313
  try:
335
- fn(bundle, duplicate)
314
+ fn(bundle, duplicate, is_fresh_black)
336
315
  except Exception as ex:
337
- # ``logger.exception`` already attaches sys.exc_info()'s
338
- # traceback; we also surface the type + message as
339
- # structured fields so log queries on ``exc_type`` /
340
- # ``exc_msg`` / ``callback`` work without traceback
341
- # parsing. Without these explicit fields a silent
342
- # malfunction in a downstream callback looks like
343
- # "feature X didn't fire" with no obvious cause.
316
+ # Structured fields so log queries on exc_type /
317
+ # exc_msg / callback work without traceback parsing.
344
318
  logger.exception(
345
319
  "OutputBuffer callback raised; continuing with remaining callbacks",
346
320
  callback=repr(fn),
@@ -361,19 +335,24 @@ class OutputBuffer:
361
335
  item = None
362
336
 
363
337
  bundle: Optional[MediaBundle] = None
338
+ sentinel_consumed = False
364
339
  if isinstance(item, _FlushMarker):
365
- # Session-boundary reset, processed in the emission
366
- # thread so there is no cross-thread race with
367
- # flush(): clear the cached frame and fall through
368
- # to the empty-queue fallback below, which
369
- # dispatches duplicate=True black. mark_first_real_frame()
370
- # is gated on duplicate=False, so the recorder's
371
- # latch (REA-2323 / #2325) stays unset.
372
340
  self._last_emitted = None
341
+ sentinel_consumed = True
373
342
  elif isinstance(item, MediaBundle):
374
343
  bundle = item
375
344
 
376
- if bundle is not None:
345
+ if sentinel_consumed:
346
+ # Session-boundary black: tagged so the wire
347
+ # forwards it (otherwise the client would stay
348
+ # frozen on the previous frame), the recorder
349
+ # ignores it.
350
+ self._dispatch(
351
+ self._create_black_bundle(),
352
+ True,
353
+ is_fresh_black=True,
354
+ )
355
+ elif bundle is not None:
377
356
  vtracks = bundle.get_tracks_by_kind(TrackKind.VIDEO)
378
357
  if vtracks:
379
358
  vd = vtracks[0].data
@@ -511,36 +490,16 @@ class OutputBuffer:
511
490
  def flush(self) -> None:
512
491
  """Drop pending bundles and request a session-boundary reset.
513
492
 
514
- The reset of ``_last_emitted`` is performed by the emission
515
- thread when it dequeues the :class:`_FlushMarker` sentinel,
516
- making the operation race-free with the per-tick
517
- "what do I emit?" decision in :meth:`_emission_loop`. After the
518
- sentinel is consumed, the next emission tick synthesises
519
- ``_create_black_bundle()`` with ``duplicate=True`` — the
520
- correct pre-roll behaviour for a session boundary:
521
-
522
- * the wire callback (``_send_out_app_bundle_sync``) drops
523
- ``duplicate=True`` outright;
524
- * the recorder's ``mark_first_real_frame()`` latch is not
525
- tripped (REA-2323 / #2325), so the recording timeline only
526
- starts at the next real model frame.
527
-
528
- Safe to call from any thread; in practice ``flush()`` is
529
- called from the model thread (``model-run``), the same thread
530
- that calls :meth:`submit`, so there is no submit/flush race
531
- on the producer side either.
493
+ The emission thread performs the actual reset of
494
+ ``_last_emitted`` when it dequeues the sentinel. Then the
495
+ next tick emits a fresh black bundle tagged ``is_fresh_black``
496
+ forwarded by the wire, ignored by the recorder.
532
497
  """
533
498
  self._drain_queue()
534
499
  try:
535
500
  self._q.put_nowait(_FLUSH_MARKER)
536
501
  except queue.Full:
537
- # Unreachable in practice: the queue is bounded and the
538
- # only producer (submit) runs on the same thread as
539
- # flush, so nothing can refill the queue between drain
540
- # and put. Log loudly if it ever happens and continue —
541
- # the next real frame from the new session will set
542
- # _last_emitted correctly even if the sentinel never
543
- # lands.
502
+ # Same thread as submit(), so unreachable in practice.
544
503
  logger.warning(
545
504
  "OutputBuffer.flush: queue full immediately after drain; "
546
505
  "reset sentinel dropped"
@@ -250,10 +250,22 @@ class SessionRecorder:
250
250
  # OutputBuffer integration
251
251
  # ------------------------------------------------------------------
252
252
 
253
- def _on_bundle_sync(self, bundle: MediaBundle, duplicate: bool) -> None:
254
- """Enqueue a bundle for the feed worker; non-blocking."""
253
+ def _on_bundle_sync(
254
+ self,
255
+ bundle: MediaBundle,
256
+ duplicate: bool,
257
+ is_fresh_black: bool = False,
258
+ ) -> None:
259
+ """Enqueue a bundle for the feed worker; non-blocking.
260
+
261
+ Session-boundary synthesised black (``is_fresh_black``) is not
262
+ real model output and is invisible to the recording timeline:
263
+ no feed-queue write, no ``mark_first_real_frame`` consideration.
264
+ """
255
265
  if self._disabled or not self._started:
256
266
  return
267
+ if is_fresh_black:
268
+ return
257
269
  now = time.monotonic()
258
270
  if duplicate:
259
271
  if self._config.skip_leading_black and not self._markers.recording_started: