reactor-runtime 3.6.0__tar.gz → 3.7.0__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.
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/PKG-INFO +23 -3
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/README.md +20 -1
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/pyproject.toml +4 -3
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/pyproject.toml.orig +4 -3
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/__init__.py +2 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/core/__init__.py +19 -1
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/core/fields.py +70 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/core/model.py +48 -16
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/core/service.py +35 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/core/session.py +8 -3
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/core/transport.py +11 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/core/values.py +132 -1
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/http/__init__.py +2 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/http/routes.py +148 -3
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/http/server.py +14 -4
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/__init__.py +2 -1
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/app/__init__.py +3 -1
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/app/input_state.py +49 -11
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/app/reactor_app.py +132 -63
- reactor_runtime-3.7.0/src/reactor_runtime/interface/app/step_completed.py +59 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/client.py +6 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/events/decorators.py +22 -2
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/events/messages.py +33 -14
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/bridge.py +28 -2
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/reactor_core.py +93 -20
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/model/contract.py +74 -3
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +1 -1
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/manifest.py +51 -7
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/message_gateway.py +5 -3
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/session.py +6 -2
- reactor_runtime-3.7.0/src/reactor_runtime/runner/client_stats.py +110 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/connection_manager.py +48 -12
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/runner.py +457 -23
- reactor_runtime-3.7.0/src/reactor_runtime/runner/session_start.py +162 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/state_machine.py +39 -2
- reactor_runtime-3.7.0/src/reactor_runtime/runner/system_client.py +64 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/serve.py +52 -12
- reactor_runtime-3.7.0/src/reactor_runtime/service.py +291 -0
- reactor_runtime-3.7.0/src/reactor_runtime/step_results/__init__.py +12 -0
- reactor_runtime-3.7.0/src/reactor_runtime/step_results/mp4.py +182 -0
- reactor_runtime-3.7.0/src/reactor_runtime/step_results/store.py +445 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/acceptor.py +4 -1
- reactor_runtime-3.7.0/src/reactor_runtime/transport/webrtc/client_stats.py +114 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/config.py +16 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/connection.py +27 -5
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/peer.py +117 -16
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_wire/v1/control_pb2.py +4 -4
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_wire/v1/control_pb2.pyi +4 -2
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_wire/v1/platform_pb2.py +23 -1
- reactor_runtime-3.7.0/src/reactor_wire/v1/platform_pb2.pyi +179 -0
- reactor_runtime-3.6.0/src/reactor_runtime/service.py +0 -141
- reactor_runtime-3.6.0/src/reactor_wire/v1/platform_pb2.pyi +0 -88
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/LICENSE +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/NOTICE +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/codes.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/core/naming.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/core/typespec.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/errors.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/ipc.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/protocol.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/rank.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/runner.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/worker.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/event_stream.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/http/events.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/http/spec.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/app/outcome.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/events/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/events/errors.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/aliases.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/model/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/model/schema.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/tracks/input.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/tracks/output.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/log.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/command.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/model.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/registry.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/paths.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/base.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/common.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/v0/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/v0/codec.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/v1/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/v1/codec.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/py.typed +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/recording/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/recording/markers.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/recording/recorder.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/offer_epochs.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/upload_resolution.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/schema.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/acceptor.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/router.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/__init__.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/frames.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/metrics.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/pacer.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/router.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/sdp.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/signaling.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/stats.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/version.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_runtime/upload_store.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_wire/v1/common_pb2.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_wire/v1/common_pb2.pyi +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_wire/v1/data_pb2.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_wire/v1/data_pb2.pyi +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_wire/v1/model_pb2.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_wire/v1/model_pb2.pyi +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_wire/v1/track_pb2.py +0 -0
- {reactor_runtime-3.6.0 → reactor_runtime-3.7.0}/src/reactor_wire/v1/track_pb2.pyi +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactor-runtime
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.0
|
|
4
4
|
Summary: A Python framework for building real-time, interactive video models
|
|
5
5
|
Author: Reactor
|
|
6
6
|
Author-email: Reactor <team@reactor.inc>
|
|
@@ -11,6 +11,7 @@ Classifier: Development Status :: 3 - Alpha
|
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
15
|
Classifier: Typing :: Typed
|
|
15
16
|
Requires-Dist: av>=18.0.0
|
|
16
17
|
Requires-Dist: fastapi>=0.139.0
|
|
@@ -18,7 +19,7 @@ Requires-Dist: numpy>=2.1
|
|
|
18
19
|
Requires-Dist: prometheus-client>=0.26.0
|
|
19
20
|
Requires-Dist: protobuf>=7.35.1
|
|
20
21
|
Requires-Dist: pyyaml>=6.0.3
|
|
21
|
-
Requires-Dist: reactor-webrtc==0.
|
|
22
|
+
Requires-Dist: reactor-webrtc==0.21.0
|
|
22
23
|
Requires-Dist: uvicorn>=0.49.0
|
|
23
24
|
Requires-Python: >=3.12
|
|
24
25
|
Project-URL: Source, https://github.com/reactor-team/reactor-runtime
|
|
@@ -78,6 +79,8 @@ class MyModel(ReactorApp):
|
|
|
78
79
|
|
|
79
80
|
That is a complete application. The runtime calls `generate()` in a loop for as long as someone is watching and emits what it returns. Every public field on `MyState` is a command the client can send: here `set_prompt` and `set_paused`, validated from the fields, and the next step reads the new values.
|
|
80
81
|
|
|
82
|
+
State and message classes can be subclassed. A subclass keeps every field of its parents and adds its own, so `class SeededState(MyState): seed: int = InputField(default=0)` exposes `set_prompt`, `set_paused`, and `set_seed`. A field declared again in the subclass replaces the parent's, default and constraints included. The fields a subclass adds are keyword-only in its constructor, which lets a required field follow an inherited one that has a default. A private field (leading underscore) always needs a default, because the state is built from defaults when a session starts.
|
|
83
|
+
|
|
81
84
|
A step is three calls, and `generate()` is the one you must write. `process_input()` runs before it, reading `self.state` and the media tracks, and decides whether a step can happen: return the input `generate()` gets, or raise `ApplicationError("reason")` to skip the step without touching the model. `process_output(outcome)` runs after it, with the result or the error, and returns the media to emit; send a message from there with `await self.send()` and it reaches the client before the step's frames. Both have defaults, so the model above writes neither.
|
|
82
85
|
|
|
83
86
|
```python
|
|
@@ -119,7 +122,24 @@ class MyModel(ReactorApp):
|
|
|
119
122
|
return outcome.to_output()
|
|
120
123
|
```
|
|
121
124
|
|
|
122
|
-
Command handlers and lifecycle hooks run between steps, never during one. The default `run()` is the loop that drives the three hooks. Override
|
|
125
|
+
Command handlers and lifecycle hooks run between steps, never during one. A model has one method per lifecycle hook kind (`@session_started`, `@session_ended`, `@connected`, `@disconnected`, `@file_uploaded`). A subclass extends an inherited hook by overriding that method and calling `super()`; the override runs whether or not it repeats the decorator. Marking a second method with the same kind, in one class or in a subclass, fails when the class is defined, so a hook is never dropped in silence. The default `run()` is the loop that drives the three hooks, and it reports each step that runs with `complete_step()`, which is what the runtime counts a session's steps by. To write that report yourself, for example to keep extra files with the step, return a `StepCompleted(output=..., files=...)` from `process_output()` instead of the `Output`. Override `run()` to write your own loop against `emit()`, `send()`, `complete_step()`, `@event`, `self.connected`, and the tracks; `process_input()`, `generate()`, and `process_output()` are then not called, and your loop calls `complete_step(StepCompleted(output=...))` each time it finishes a unit of work. Do that for a loop that is not one step per emit, such as a renderer that emits several times per step or a model that must block on an input.
|
|
126
|
+
|
|
127
|
+
A session can start with its setup already in hand: `POST /start_session` takes a `starting_input` of state values and commands, which the runtime applies on the normal command path before any client connects. A system client sends them, a connection the runtime opens itself, so a handler that takes `client` gets a `ClientInfo` whose `system` is true. The step loop takes its first step only once every starting command has landed, uploads included, so no step runs on the defaults the starting input replaces. In a live session the system client then leaves and the loop waits for a viewer, as it always does; a model with its own `run()` sees the system client connect like any client. A live session stays `waiting` while its starting input applies, because the system client does not occupy it, and `/events` journals the system client's `connection_opened` and `connection_closed` as `waiting -> waiting`. A session with `steps` is occupied by the system client until it ends, so it streams with no other client. The runtime ends it itself once the model has reported that many steps, with a normal stop whose close reason is "Session ended: the requested steps are complete."
|
|
128
|
+
|
|
129
|
+
A model can keep each step it reports as a folder of files, turned on in `reactor.yaml`:
|
|
130
|
+
|
|
131
|
+
```yaml
|
|
132
|
+
runtime:
|
|
133
|
+
step_results:
|
|
134
|
+
enabled: true
|
|
135
|
+
video: {codec: h264, preset: veryfast, crf: 23} # or codec: h265
|
|
136
|
+
audio: {codec: aac, bitrate_kbps: 128}
|
|
137
|
+
queue: 8
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Every track of the step's output becomes its own stream in one `output.mp4`, beside the extra `files` the step carried and a `result.json` that lists them. `queue` bounds how many steps may wait to be saved; a step that finds it full is not saved, so saving never holds the model up. Each step is its own encode, so this suits a model that finishes a step every few seconds rather than every frame.
|
|
141
|
+
|
|
142
|
+
When step results are on, `GET /sessions/{id}/steps` lists a session's saved folders by the session's own id; each stays readable for five minutes after it is written, after the session ends too.
|
|
123
143
|
|
|
124
144
|
Scaffold, build, and run it with the CLI:
|
|
125
145
|
|
|
@@ -52,6 +52,8 @@ class MyModel(ReactorApp):
|
|
|
52
52
|
|
|
53
53
|
That is a complete application. The runtime calls `generate()` in a loop for as long as someone is watching and emits what it returns. Every public field on `MyState` is a command the client can send: here `set_prompt` and `set_paused`, validated from the fields, and the next step reads the new values.
|
|
54
54
|
|
|
55
|
+
State and message classes can be subclassed. A subclass keeps every field of its parents and adds its own, so `class SeededState(MyState): seed: int = InputField(default=0)` exposes `set_prompt`, `set_paused`, and `set_seed`. A field declared again in the subclass replaces the parent's, default and constraints included. The fields a subclass adds are keyword-only in its constructor, which lets a required field follow an inherited one that has a default. A private field (leading underscore) always needs a default, because the state is built from defaults when a session starts.
|
|
56
|
+
|
|
55
57
|
A step is three calls, and `generate()` is the one you must write. `process_input()` runs before it, reading `self.state` and the media tracks, and decides whether a step can happen: return the input `generate()` gets, or raise `ApplicationError("reason")` to skip the step without touching the model. `process_output(outcome)` runs after it, with the result or the error, and returns the media to emit; send a message from there with `await self.send()` and it reaches the client before the step's frames. Both have defaults, so the model above writes neither.
|
|
56
58
|
|
|
57
59
|
```python
|
|
@@ -93,7 +95,24 @@ class MyModel(ReactorApp):
|
|
|
93
95
|
return outcome.to_output()
|
|
94
96
|
```
|
|
95
97
|
|
|
96
|
-
Command handlers and lifecycle hooks run between steps, never during one. The default `run()` is the loop that drives the three hooks. Override
|
|
98
|
+
Command handlers and lifecycle hooks run between steps, never during one. A model has one method per lifecycle hook kind (`@session_started`, `@session_ended`, `@connected`, `@disconnected`, `@file_uploaded`). A subclass extends an inherited hook by overriding that method and calling `super()`; the override runs whether or not it repeats the decorator. Marking a second method with the same kind, in one class or in a subclass, fails when the class is defined, so a hook is never dropped in silence. The default `run()` is the loop that drives the three hooks, and it reports each step that runs with `complete_step()`, which is what the runtime counts a session's steps by. To write that report yourself, for example to keep extra files with the step, return a `StepCompleted(output=..., files=...)` from `process_output()` instead of the `Output`. Override `run()` to write your own loop against `emit()`, `send()`, `complete_step()`, `@event`, `self.connected`, and the tracks; `process_input()`, `generate()`, and `process_output()` are then not called, and your loop calls `complete_step(StepCompleted(output=...))` each time it finishes a unit of work. Do that for a loop that is not one step per emit, such as a renderer that emits several times per step or a model that must block on an input.
|
|
99
|
+
|
|
100
|
+
A session can start with its setup already in hand: `POST /start_session` takes a `starting_input` of state values and commands, which the runtime applies on the normal command path before any client connects. A system client sends them, a connection the runtime opens itself, so a handler that takes `client` gets a `ClientInfo` whose `system` is true. The step loop takes its first step only once every starting command has landed, uploads included, so no step runs on the defaults the starting input replaces. In a live session the system client then leaves and the loop waits for a viewer, as it always does; a model with its own `run()` sees the system client connect like any client. A live session stays `waiting` while its starting input applies, because the system client does not occupy it, and `/events` journals the system client's `connection_opened` and `connection_closed` as `waiting -> waiting`. A session with `steps` is occupied by the system client until it ends, so it streams with no other client. The runtime ends it itself once the model has reported that many steps, with a normal stop whose close reason is "Session ended: the requested steps are complete."
|
|
101
|
+
|
|
102
|
+
A model can keep each step it reports as a folder of files, turned on in `reactor.yaml`:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
runtime:
|
|
106
|
+
step_results:
|
|
107
|
+
enabled: true
|
|
108
|
+
video: {codec: h264, preset: veryfast, crf: 23} # or codec: h265
|
|
109
|
+
audio: {codec: aac, bitrate_kbps: 128}
|
|
110
|
+
queue: 8
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Every track of the step's output becomes its own stream in one `output.mp4`, beside the extra `files` the step carried and a `result.json` that lists them. `queue` bounds how many steps may wait to be saved; a step that finds it full is not saved, so saving never holds the model up. Each step is its own encode, so this suits a model that finishes a step every few seconds rather than every frame.
|
|
114
|
+
|
|
115
|
+
When step results are on, `GET /sessions/{id}/steps` lists a session's saved folders by the session's own id; each stays readable for five minutes after it is written, after the session ends too.
|
|
97
116
|
|
|
98
117
|
Scaffold, build, and run it with the CLI:
|
|
99
118
|
|
|
@@ -10,7 +10,7 @@ module-name = [
|
|
|
10
10
|
namespace = true
|
|
11
11
|
|
|
12
12
|
[tool.reactor-wire]
|
|
13
|
-
version = "1.
|
|
13
|
+
version = "1.20260929.8"
|
|
14
14
|
|
|
15
15
|
[tool.ruff]
|
|
16
16
|
line-length = 100
|
|
@@ -95,7 +95,7 @@ pythonpath = ["."]
|
|
|
95
95
|
|
|
96
96
|
[project]
|
|
97
97
|
name = "reactor-runtime"
|
|
98
|
-
version = "3.
|
|
98
|
+
version = "3.7.0"
|
|
99
99
|
description = "A Python framework for building real-time, interactive video models"
|
|
100
100
|
readme = "README.md"
|
|
101
101
|
license = "Apache-2.0"
|
|
@@ -109,6 +109,7 @@ classifiers = [
|
|
|
109
109
|
"Intended Audience :: Developers",
|
|
110
110
|
"Programming Language :: Python :: 3.12",
|
|
111
111
|
"Programming Language :: Python :: 3.13",
|
|
112
|
+
"Programming Language :: Python :: 3.14",
|
|
112
113
|
"Typing :: Typed",
|
|
113
114
|
]
|
|
114
115
|
dependencies = [
|
|
@@ -118,7 +119,7 @@ dependencies = [
|
|
|
118
119
|
"prometheus-client>=0.26.0",
|
|
119
120
|
"protobuf>=7.35.1",
|
|
120
121
|
"pyyaml>=6.0.3",
|
|
121
|
-
"reactor-webrtc==0.
|
|
122
|
+
"reactor-webrtc==0.21.0",
|
|
122
123
|
"uvicorn>=0.49.0",
|
|
123
124
|
]
|
|
124
125
|
|
|
@@ -16,11 +16,11 @@ namespace = true
|
|
|
16
16
|
# gates the pin against proto/, so a release cannot ship a schema older than the
|
|
17
17
|
# sources it was built from.
|
|
18
18
|
[tool.reactor-wire]
|
|
19
|
-
version = "1.
|
|
19
|
+
version = "1.20260929.8"
|
|
20
20
|
|
|
21
21
|
[project]
|
|
22
22
|
name = "reactor-runtime"
|
|
23
|
-
version = "3.
|
|
23
|
+
version = "3.7.0"
|
|
24
24
|
description = "A Python framework for building real-time, interactive video models"
|
|
25
25
|
readme = "README.md"
|
|
26
26
|
license = "Apache-2.0"
|
|
@@ -32,6 +32,7 @@ classifiers = [
|
|
|
32
32
|
"Intended Audience :: Developers",
|
|
33
33
|
"Programming Language :: Python :: 3.12",
|
|
34
34
|
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Programming Language :: Python :: 3.14",
|
|
35
36
|
"Typing :: Typed",
|
|
36
37
|
]
|
|
37
38
|
dependencies = [
|
|
@@ -41,7 +42,7 @@ dependencies = [
|
|
|
41
42
|
"prometheus-client>=0.26.0",
|
|
42
43
|
"protobuf>=7.35.1",
|
|
43
44
|
"pyyaml>=6.0.3",
|
|
44
|
-
"reactor-webrtc==0.
|
|
45
|
+
"reactor-webrtc==0.21.0",
|
|
45
46
|
"uvicorn>=0.49.0",
|
|
46
47
|
]
|
|
47
48
|
|
|
@@ -43,6 +43,7 @@ from reactor_runtime.interface import (
|
|
|
43
43
|
OutputStream,
|
|
44
44
|
ReactorApp,
|
|
45
45
|
ReadMode,
|
|
46
|
+
StepCompleted,
|
|
46
47
|
StepOutcome,
|
|
47
48
|
Track,
|
|
48
49
|
TrackPayload,
|
|
@@ -96,6 +97,7 @@ __all__ = [
|
|
|
96
97
|
"OutputStream",
|
|
97
98
|
"ReactorApp",
|
|
98
99
|
"ReadMode",
|
|
100
|
+
"StepCompleted",
|
|
99
101
|
"StepOutcome",
|
|
100
102
|
"Track",
|
|
101
103
|
"TrackPayload",
|
|
@@ -17,10 +17,16 @@ from reactor_runtime.core.model import (
|
|
|
17
17
|
ReactorEvent,
|
|
18
18
|
SessionEnded,
|
|
19
19
|
SessionStarted,
|
|
20
|
+
StartingInputApplied,
|
|
20
21
|
TransitionEvent,
|
|
21
22
|
UploadedFile,
|
|
22
23
|
)
|
|
23
|
-
from reactor_runtime.core.service import
|
|
24
|
+
from reactor_runtime.core.service import (
|
|
25
|
+
RecordingConfig,
|
|
26
|
+
RuntimeConfig,
|
|
27
|
+
ServiceComponent,
|
|
28
|
+
StepResultsConfig,
|
|
29
|
+
)
|
|
24
30
|
from reactor_runtime.core.session import (
|
|
25
31
|
JOURNAL_EVENTS,
|
|
26
32
|
SessionEvent,
|
|
@@ -30,7 +36,12 @@ from reactor_runtime.core.session import (
|
|
|
30
36
|
from reactor_runtime.core.transport import Connection, ConnectionSink
|
|
31
37
|
from reactor_runtime.core.typespec import TypeSpec
|
|
32
38
|
from reactor_runtime.core.values import (
|
|
39
|
+
ClientConnectionStat,
|
|
40
|
+
ClientStatsBatch,
|
|
41
|
+
ClientTrackDirection,
|
|
42
|
+
ClientTrackStat,
|
|
33
43
|
CommandFailure,
|
|
44
|
+
CompletedStep,
|
|
34
45
|
ConnectionCapabilities,
|
|
35
46
|
ConnId,
|
|
36
47
|
Health,
|
|
@@ -48,10 +59,15 @@ from reactor_runtime.core.values import (
|
|
|
48
59
|
__all__ = [
|
|
49
60
|
"JOURNAL_EVENTS",
|
|
50
61
|
"ClientConnected",
|
|
62
|
+
"ClientConnectionStat",
|
|
51
63
|
"ClientDisconnected",
|
|
64
|
+
"ClientStatsBatch",
|
|
65
|
+
"ClientTrackDirection",
|
|
66
|
+
"ClientTrackStat",
|
|
52
67
|
"Command",
|
|
53
68
|
"CommandFailure",
|
|
54
69
|
"CommandField",
|
|
70
|
+
"CompletedStep",
|
|
55
71
|
"ConnId",
|
|
56
72
|
"Connection",
|
|
57
73
|
"ConnectionCapabilities",
|
|
@@ -74,6 +90,8 @@ __all__ = [
|
|
|
74
90
|
"SessionEvent",
|
|
75
91
|
"SessionStarted",
|
|
76
92
|
"SessionState",
|
|
93
|
+
"StartingInputApplied",
|
|
94
|
+
"StepResultsConfig",
|
|
77
95
|
"TrackData",
|
|
78
96
|
"TrackDirection",
|
|
79
97
|
"TrackInfo",
|
|
@@ -13,6 +13,8 @@ graph alongside the neutral value vocabulary.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import dataclasses
|
|
17
|
+
import inspect
|
|
16
18
|
from dataclasses import dataclass
|
|
17
19
|
from typing import Any, Final
|
|
18
20
|
|
|
@@ -121,6 +123,74 @@ def InputField( # noqa: N802 — a capitalised factory reads as a type in field
|
|
|
121
123
|
)
|
|
122
124
|
|
|
123
125
|
|
|
126
|
+
def own_annotations(cls: type) -> dict[str, Any]:
|
|
127
|
+
"""Return the annotations *cls* declares itself, none from its bases.
|
|
128
|
+
|
|
129
|
+
Goes through :func:`inspect.get_annotations` rather than the class
|
|
130
|
+
``__dict__``. From Python 3.14 (PEP 649) a class stores its annotations
|
|
131
|
+
lazily, behind ``__annotate__``, and a direct dict read finds nothing; the
|
|
132
|
+
accessor materialises them on every version. Inherited annotations are
|
|
133
|
+
read from the bases' cached field records, never re-resolved.
|
|
134
|
+
"""
|
|
135
|
+
return inspect.get_annotations(cls)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def inherited_record(cls: type, attribute: str) -> dict[str, Any]:
|
|
139
|
+
"""Merge the field records the bases of *cls* store on themselves.
|
|
140
|
+
|
|
141
|
+
A declaration base (``InputState``, ``ModelMessage``, ``Command``) caches
|
|
142
|
+
the fields each subclass declares in a class-level dict. A subclass starts
|
|
143
|
+
from this merge, so the fields it inherits are part of its own record. Bases
|
|
144
|
+
are read from the most distant to the closest, so a nearer base wins a name
|
|
145
|
+
both declare, and a re-declared name keeps the position the first base gave
|
|
146
|
+
it, which is the order a dataclass lays inherited fields out in.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
cls: The class being declared.
|
|
150
|
+
attribute: The name of the record attribute on each base.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
A new dict; the bases' records are not modified.
|
|
154
|
+
"""
|
|
155
|
+
merged: dict[str, Any] = {}
|
|
156
|
+
for base in reversed(cls.__mro__[1:]):
|
|
157
|
+
merged.update(base.__dict__.get(attribute, {}))
|
|
158
|
+
return merged
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def apply_dataclass(cls: type, *, required: list[str], inherits: bool) -> None:
|
|
162
|
+
"""Turn a freshly declared subclass into a dataclass, once.
|
|
163
|
+
|
|
164
|
+
A class whose parent is already a dataclass is still converted, so the
|
|
165
|
+
fields it declares become fields of its own. Its fields are keyword-only
|
|
166
|
+
when it inherits any, because a dataclass refuses a required field placed
|
|
167
|
+
after an inherited field with a default, and keyword-only fields have no
|
|
168
|
+
such order.
|
|
169
|
+
|
|
170
|
+
A required field hides any class attribute a parent holds under the same
|
|
171
|
+
name for the duration of the conversion. The dataclass reads a field's
|
|
172
|
+
default with ``getattr``, so without this a field declared required again
|
|
173
|
+
would silently inherit the parent's default. Once the placeholder is
|
|
174
|
+
removed the parent's attribute is reachable through the subclass again, so
|
|
175
|
+
``Child.x`` reads the parent's value while ``dataclasses.fields(Child)``
|
|
176
|
+
and the field record both say required. The runtime reads defaults from
|
|
177
|
+
the record, never from the class attribute.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
cls: The class to convert. A class that already carries its own
|
|
181
|
+
``__dataclass_fields__`` is left alone.
|
|
182
|
+
required: The names of the fields *cls* declares without a default.
|
|
183
|
+
inherits: Whether *cls* inherits fields from a declared base.
|
|
184
|
+
"""
|
|
185
|
+
if "__dataclass_fields__" in cls.__dict__:
|
|
186
|
+
return
|
|
187
|
+
for name in required:
|
|
188
|
+
setattr(cls, name, dataclasses.MISSING)
|
|
189
|
+
dataclasses.dataclass(cls, kw_only=inherits)
|
|
190
|
+
for name in required:
|
|
191
|
+
delattr(cls, name)
|
|
192
|
+
|
|
193
|
+
|
|
124
194
|
def raise_if_default_not_static(owner: str, field_name: str, default: Any) -> None:
|
|
125
195
|
"""Raise ``TypeError`` when *default* is a mutable container.
|
|
126
196
|
|
|
@@ -20,6 +20,9 @@ from reactor_runtime.core.fields import (
|
|
|
20
20
|
NO_DEFAULT,
|
|
21
21
|
FieldInfo,
|
|
22
22
|
InputField,
|
|
23
|
+
apply_dataclass,
|
|
24
|
+
inherited_record,
|
|
25
|
+
own_annotations,
|
|
23
26
|
raise_if_default_invalid,
|
|
24
27
|
raise_if_default_not_static,
|
|
25
28
|
)
|
|
@@ -62,6 +65,10 @@ class Command:
|
|
|
62
65
|
invalid defaults at import), turns the class into a dataclass, and caches the
|
|
63
66
|
resolved fields on the class. There is no global registry — each command
|
|
64
67
|
carries its own contract.
|
|
68
|
+
|
|
69
|
+
A subclass of a command inherits every field of its parents, and a field it
|
|
70
|
+
declares again replaces the parent's. The fields a subclass adds are
|
|
71
|
+
keyword-only in its constructor.
|
|
65
72
|
"""
|
|
66
73
|
|
|
67
74
|
name: ClassVar[str]
|
|
@@ -71,8 +78,6 @@ class Command:
|
|
|
71
78
|
def __init_subclass__(cls, **kwargs: object) -> None:
|
|
72
79
|
super().__init_subclass__(**kwargs)
|
|
73
80
|
_build_command(cls)
|
|
74
|
-
if not dataclasses.is_dataclass(cls):
|
|
75
|
-
dataclasses.dataclass(cls)
|
|
76
81
|
if "name" not in cls.__dict__:
|
|
77
82
|
cls.name = pascal_to_snake(cls.__name__)
|
|
78
83
|
|
|
@@ -83,9 +88,9 @@ def _is_classvar(annotation: Any) -> bool:
|
|
|
83
88
|
|
|
84
89
|
|
|
85
90
|
def _build_command(cls: type[Command]) -> None:
|
|
86
|
-
"""Resolve a command class's fields
|
|
91
|
+
"""Resolve a command class's fields, cache them on it, and make it a dataclass.
|
|
87
92
|
|
|
88
|
-
Runs once per subclass
|
|
93
|
+
Runs once per subclass. Starts from the fields the parents declared, unwraps
|
|
89
94
|
``InputField`` defaults so the dataclass sees plain values, resolves each
|
|
90
95
|
field's :class:`TypeSpec` (the support check), records which fields are
|
|
91
96
|
uploads, and reorders so fields without a default come first — the order a
|
|
@@ -103,10 +108,18 @@ def _build_command(cls: type[Command]) -> None:
|
|
|
103
108
|
# deferring the import until a command is actually declared.
|
|
104
109
|
from reactor_runtime.core.typespec import OptionalSpec, TypeSpec, UploadSpec
|
|
105
110
|
|
|
106
|
-
|
|
111
|
+
def is_upload(spec: TypeSpec) -> bool:
|
|
112
|
+
resolved = spec.inner if isinstance(spec, OptionalSpec) else spec
|
|
113
|
+
return isinstance(resolved, UploadSpec)
|
|
114
|
+
|
|
115
|
+
fields = inherited_record(cls, "__command_fields__")
|
|
116
|
+
inherits = bool(fields)
|
|
117
|
+
|
|
118
|
+
annotations = own_annotations(cls)
|
|
107
119
|
if not annotations:
|
|
108
|
-
cls.__command_fields__ =
|
|
109
|
-
cls.__upload_fields__ = frozenset()
|
|
120
|
+
cls.__command_fields__ = fields
|
|
121
|
+
cls.__upload_fields__ = frozenset(n for n, f in fields.items() if is_upload(f.spec))
|
|
122
|
+
apply_dataclass(cls, required=[], inherits=inherits)
|
|
110
123
|
return
|
|
111
124
|
|
|
112
125
|
try:
|
|
@@ -115,14 +128,17 @@ def _build_command(cls: type[Command]) -> None:
|
|
|
115
128
|
hints = {}
|
|
116
129
|
|
|
117
130
|
owner = cls.__qualname__
|
|
118
|
-
fields: dict[str, CommandField] = {}
|
|
119
|
-
uploads: set[str] = set()
|
|
120
131
|
no_default: list[str] = []
|
|
121
132
|
has_default: list[str] = []
|
|
133
|
+
class_vars: list[str] = []
|
|
122
134
|
|
|
123
135
|
for name in list(annotations):
|
|
124
136
|
annotation = hints.get(name, annotations[name])
|
|
125
137
|
if _is_classvar(annotation):
|
|
138
|
+
# A ClassVar is not a field. Redeclaring an inherited field as one
|
|
139
|
+
# takes it out of the dataclass, so it leaves the record too.
|
|
140
|
+
fields.pop(name, None)
|
|
141
|
+
class_vars.append(name)
|
|
126
142
|
continue
|
|
127
143
|
|
|
128
144
|
raw = cls.__dict__.get(name, NO_DEFAULT)
|
|
@@ -138,8 +154,6 @@ def _build_command(cls: type[Command]) -> None:
|
|
|
138
154
|
info = raw
|
|
139
155
|
raise_if_default_invalid(owner, name, info.default, info)
|
|
140
156
|
if info.default is NO_DEFAULT:
|
|
141
|
-
if hasattr(cls, name):
|
|
142
|
-
delattr(cls, name)
|
|
143
157
|
no_default.append(name)
|
|
144
158
|
else:
|
|
145
159
|
setattr(cls, name, info.default)
|
|
@@ -160,15 +174,18 @@ def _build_command(cls: type[Command]) -> None:
|
|
|
160
174
|
)
|
|
161
175
|
|
|
162
176
|
fields[name] = CommandField(spec=spec, info=info)
|
|
163
|
-
resolved = spec.inner if isinstance(spec, OptionalSpec) else spec
|
|
164
|
-
if isinstance(resolved, UploadSpec):
|
|
165
|
-
uploads.add(name)
|
|
166
177
|
|
|
167
178
|
if no_default and has_default:
|
|
168
|
-
cls.__annotations__ = {
|
|
179
|
+
cls.__annotations__ = {
|
|
180
|
+
key: annotations[key] for key in no_default + has_default + class_vars
|
|
181
|
+
}
|
|
169
182
|
|
|
170
183
|
cls.__command_fields__ = fields
|
|
171
|
-
|
|
184
|
+
# Read off the winning definition of every field, so a name one base
|
|
185
|
+
# declares as an upload and a nearer base as something else follows the
|
|
186
|
+
# nearer base.
|
|
187
|
+
cls.__upload_fields__ = frozenset(n for n, f in fields.items() if is_upload(f.spec))
|
|
188
|
+
apply_dataclass(cls, required=no_default, inherits=inherits)
|
|
172
189
|
|
|
173
190
|
|
|
174
191
|
class EndReason(StrEnum):
|
|
@@ -226,9 +243,22 @@ class SessionStarted(ReactorEvent):
|
|
|
226
243
|
|
|
227
244
|
Attributes:
|
|
228
245
|
session_id: Identifier for the session that started.
|
|
246
|
+
starting_input: Whether a starting input follows. When one does, the
|
|
247
|
+
step loop takes no step until :class:`StartingInputApplied`
|
|
248
|
+
arrives, so its first step sees the state the starting input set.
|
|
229
249
|
"""
|
|
230
250
|
|
|
231
251
|
session_id: str
|
|
252
|
+
starting_input: bool = False
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclass(frozen=True)
|
|
256
|
+
class StartingInputApplied(ReactorEvent):
|
|
257
|
+
"""Every command of the session's starting input has been handed to the model.
|
|
258
|
+
|
|
259
|
+
It follows the last starting command on the same ordered queue, so every
|
|
260
|
+
starting handler has returned by the time the model handles it.
|
|
261
|
+
"""
|
|
232
262
|
|
|
233
263
|
|
|
234
264
|
@dataclass(frozen=True)
|
|
@@ -257,10 +287,12 @@ class ClientConnected(ReactorEvent):
|
|
|
257
287
|
Attributes:
|
|
258
288
|
conn_id: The connection that opened.
|
|
259
289
|
total: Live connection count after the open.
|
|
290
|
+
system: Whether the connection is the runtime's own system client.
|
|
260
291
|
"""
|
|
261
292
|
|
|
262
293
|
conn_id: ConnId
|
|
263
294
|
total: int
|
|
295
|
+
system: bool = False
|
|
264
296
|
|
|
265
297
|
|
|
266
298
|
@dataclass(frozen=True)
|
|
@@ -92,6 +92,36 @@ class RecordingConfig:
|
|
|
92
92
|
recording_dir: str | None = None
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class StepResultsConfig:
|
|
97
|
+
"""How each finished step is kept, drawn from the manifest's ``step_results:`` block.
|
|
98
|
+
|
|
99
|
+
A model with no ``step_results:`` block keeps nothing (``enabled`` is
|
|
100
|
+
``False``). When enabled, every step the model reports is saved as a
|
|
101
|
+
folder: its output in one ``output.mp4``, with each track of the step's
|
|
102
|
+
``Output`` as its own stream, beside any extra files and a ``result.json``.
|
|
103
|
+
Turning it on is the model's choice, not a session option.
|
|
104
|
+
|
|
105
|
+
Attributes:
|
|
106
|
+
enabled: Whether the runtime saves each finished step.
|
|
107
|
+
video_codec: ``"h264"`` or ``"h265"``.
|
|
108
|
+
video_preset: The libx264/libx265 preset.
|
|
109
|
+
video_crf: The constant-rate-factor quality target.
|
|
110
|
+
audio_codec: The audio codec, e.g. ``"aac"``.
|
|
111
|
+
audio_bitrate_kbps: The audio bitrate in kilobits per second.
|
|
112
|
+
queue: How many steps may wait to be saved. A step that finds the
|
|
113
|
+
queue full is not saved, so the model never waits on saving.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
enabled: bool = False
|
|
117
|
+
video_codec: str = "h264"
|
|
118
|
+
video_preset: str = "veryfast"
|
|
119
|
+
video_crf: int = 23
|
|
120
|
+
audio_codec: str = "aac"
|
|
121
|
+
audio_bitrate_kbps: int = 128
|
|
122
|
+
queue: int = 8
|
|
123
|
+
|
|
124
|
+
|
|
95
125
|
@dataclass(frozen=True)
|
|
96
126
|
class RuntimeConfig:
|
|
97
127
|
"""The single configuration object threaded through ``serve``.
|
|
@@ -112,8 +142,11 @@ class RuntimeConfig:
|
|
|
112
142
|
host: Address the HTTP ingress binds.
|
|
113
143
|
port: Port the HTTP ingress binds.
|
|
114
144
|
grace_period: Seconds a draining session is given to end before stop.
|
|
145
|
+
exit_timeout: Seconds the orderly shutdown may take, on top of any
|
|
146
|
+
session drain it includes, before the process is forced to exit.
|
|
115
147
|
orphan_timeout: Seconds a session may stay client-less before it closes.
|
|
116
148
|
recording: The recorder's configuration; disabled by default.
|
|
149
|
+
step_results: How each finished step is saved; disabled by default.
|
|
117
150
|
"""
|
|
118
151
|
|
|
119
152
|
model_ref: str
|
|
@@ -122,5 +155,7 @@ class RuntimeConfig:
|
|
|
122
155
|
host: str = "0.0.0.0"
|
|
123
156
|
port: int = 8080
|
|
124
157
|
grace_period: float = 30.0
|
|
158
|
+
exit_timeout: float = 10.0
|
|
125
159
|
orphan_timeout: float = 60.0
|
|
126
160
|
recording: RecordingConfig = field(default_factory=RecordingConfig)
|
|
161
|
+
step_results: StepResultsConfig = field(default_factory=StepResultsConfig)
|
|
@@ -75,9 +75,10 @@ class SessionEvent(Enum):
|
|
|
75
75
|
one phase otherwise silent, since the runner emits nothing until it leaves
|
|
76
76
|
``CREATED`` on ``INITIALIZATION_SUCCESS``.
|
|
77
77
|
|
|
78
|
-
``CHUNK_READY``, ``CLIP_READY``, ``COMMAND``, ``ERROR``,
|
|
79
|
-
the journal-only events
|
|
80
|
-
|
|
78
|
+
``CHUNK_READY``, ``CLIP_READY``, ``COMMAND``, ``ERROR``, ``METRIC``,
|
|
79
|
+
``STEP_COMPLETED``, and ``STEP_RESULT_READY`` are the journal-only events
|
|
80
|
+
(:data:`JOURNAL_EVENTS`): facts recorded for an external consumer rather
|
|
81
|
+
than moves of the lifecycle. Each is a pure
|
|
81
82
|
self-loop legal in **every** state — a journal fact must never be silently
|
|
82
83
|
dropped by the transition table (the final ``CHUNK_READY`` fires during
|
|
83
84
|
``CLOSING``; an ``ERROR`` can fire while ``ORPHANED``) — that changes no
|
|
@@ -101,6 +102,8 @@ class SessionEvent(Enum):
|
|
|
101
102
|
COMMAND = auto()
|
|
102
103
|
ERROR = auto()
|
|
103
104
|
METRIC = auto()
|
|
105
|
+
STEP_COMPLETED = auto()
|
|
106
|
+
STEP_RESULT_READY = auto()
|
|
104
107
|
|
|
105
108
|
|
|
106
109
|
JOURNAL_EVENTS: frozenset[SessionEvent] = frozenset(
|
|
@@ -110,6 +113,8 @@ JOURNAL_EVENTS: frozenset[SessionEvent] = frozenset(
|
|
|
110
113
|
SessionEvent.COMMAND,
|
|
111
114
|
SessionEvent.ERROR,
|
|
112
115
|
SessionEvent.METRIC,
|
|
116
|
+
SessionEvent.STEP_COMPLETED,
|
|
117
|
+
SessionEvent.STEP_RESULT_READY,
|
|
113
118
|
}
|
|
114
119
|
)
|
|
115
120
|
"""The journal-only events: pure self-loops in every state, payload in ``detail``."""
|
|
@@ -13,6 +13,7 @@ from collections.abc import Mapping
|
|
|
13
13
|
from typing import Protocol, runtime_checkable
|
|
14
14
|
|
|
15
15
|
from reactor_runtime.core.values import (
|
|
16
|
+
ClientStatsBatch,
|
|
16
17
|
ConnectionCapabilities,
|
|
17
18
|
ConnId,
|
|
18
19
|
InputFrame,
|
|
@@ -193,6 +194,16 @@ class ConnectionSink(Protocol):
|
|
|
193
194
|
timeline, correlated by *request_id*.
|
|
194
195
|
"""
|
|
195
196
|
|
|
197
|
+
def client_stats_received(self, conn_id: ConnId, batch: ClientStatsBatch) -> None:
|
|
198
|
+
"""Hand up a batch of client-observed quality samples for one connection.
|
|
199
|
+
|
|
200
|
+
The client is the only vantage point onto its own receive-side
|
|
201
|
+
quality, so a transport reports the batch as decoded rather than
|
|
202
|
+
validating it. The connection identity comes from *conn_id*, which the
|
|
203
|
+
transport already holds — never from anything the payload itself
|
|
204
|
+
might claim, because the payload carries none.
|
|
205
|
+
"""
|
|
206
|
+
|
|
196
207
|
def connection_answered(self, conn_id: ConnId, answer: Mapping[str, str]) -> None:
|
|
197
208
|
"""Relay the transport's negotiation answer for one connection.
|
|
198
209
|
|