reactor-runtime 3.6.1__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.1 → reactor_runtime-3.7.0}/PKG-INFO +20 -3
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/README.md +18 -1
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/pyproject.toml +2 -2
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/pyproject.toml.orig +2 -2
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/__init__.py +2 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/__init__.py +19 -1
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/model.py +15 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/service.py +32 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/session.py +8 -3
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/transport.py +11 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/values.py +132 -1
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/http/__init__.py +2 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/http/routes.py +148 -3
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/http/server.py +2 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/__init__.py +2 -1
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/app/__init__.py +3 -1
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/app/reactor_app.py +102 -49
- reactor_runtime-3.7.0/src/reactor_runtime/interface/app/step_completed.py +59 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/client.py +6 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/bridge.py +28 -2
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/reactor_core.py +93 -20
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +1 -1
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/manifest.py +51 -7
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/message_gateway.py +5 -3
- {reactor_runtime-3.6.1 → 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.1 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/connection_manager.py +48 -12
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/runner.py +430 -16
- reactor_runtime-3.7.0/src/reactor_runtime/runner/session_start.py +162 -0
- {reactor_runtime-3.6.1 → 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.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.1 → 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.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/config.py +16 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/connection.py +27 -5
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/peer.py +65 -12
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/LICENSE +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/NOTICE +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/codes.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/fields.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/naming.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/typespec.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/errors.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/ipc.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/protocol.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/rank.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/runner.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/worker.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/event_stream.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/http/events.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/http/spec.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/app/input_state.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/app/outcome.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/events/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/events/decorators.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/events/errors.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/events/messages.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/aliases.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/model/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/model/contract.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/model/schema.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/tracks/input.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/tracks/output.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/log.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/command.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/model.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/registry.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/paths.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/base.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/common.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/v0/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/v0/codec.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/v1/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/v1/codec.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/py.typed +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/recording/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/recording/markers.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/recording/recorder.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/offer_epochs.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/upload_resolution.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/schema.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/serve.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/service.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/acceptor.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/router.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/__init__.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/frames.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/metrics.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/pacer.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/router.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/sdp.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/signaling.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/stats.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/version.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/upload_store.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/common_pb2.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/common_pb2.pyi +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/control_pb2.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/control_pb2.pyi +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/data_pb2.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/data_pb2.pyi +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/model_pb2.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/model_pb2.pyi +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/platform_pb2.py +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/platform_pb2.pyi +0 -0
- {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/track_pb2.py +0 -0
- {reactor_runtime-3.6.1 → 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>
|
|
@@ -19,7 +19,7 @@ Requires-Dist: numpy>=2.1
|
|
|
19
19
|
Requires-Dist: prometheus-client>=0.26.0
|
|
20
20
|
Requires-Dist: protobuf>=7.35.1
|
|
21
21
|
Requires-Dist: pyyaml>=6.0.3
|
|
22
|
-
Requires-Dist: reactor-webrtc==0.
|
|
22
|
+
Requires-Dist: reactor-webrtc==0.21.0
|
|
23
23
|
Requires-Dist: uvicorn>=0.49.0
|
|
24
24
|
Requires-Python: >=3.12
|
|
25
25
|
Project-URL: Source, https://github.com/reactor-team/reactor-runtime
|
|
@@ -122,7 +122,24 @@ class MyModel(ReactorApp):
|
|
|
122
122
|
return outcome.to_output()
|
|
123
123
|
```
|
|
124
124
|
|
|
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. 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.
|
|
126
143
|
|
|
127
144
|
Scaffold, build, and run it with the CLI:
|
|
128
145
|
|
|
@@ -95,7 +95,24 @@ class MyModel(ReactorApp):
|
|
|
95
95
|
return outcome.to_output()
|
|
96
96
|
```
|
|
97
97
|
|
|
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. 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.
|
|
99
116
|
|
|
100
117
|
Scaffold, build, and run it with the CLI:
|
|
101
118
|
|
|
@@ -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"
|
|
@@ -119,7 +119,7 @@ dependencies = [
|
|
|
119
119
|
"prometheus-client>=0.26.0",
|
|
120
120
|
"protobuf>=7.35.1",
|
|
121
121
|
"pyyaml>=6.0.3",
|
|
122
|
-
"reactor-webrtc==0.
|
|
122
|
+
"reactor-webrtc==0.21.0",
|
|
123
123
|
"uvicorn>=0.49.0",
|
|
124
124
|
]
|
|
125
125
|
|
|
@@ -20,7 +20,7 @@ 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"
|
|
@@ -42,7 +42,7 @@ dependencies = [
|
|
|
42
42
|
"prometheus-client>=0.26.0",
|
|
43
43
|
"protobuf>=7.35.1",
|
|
44
44
|
"pyyaml>=6.0.3",
|
|
45
|
-
"reactor-webrtc==0.
|
|
45
|
+
"reactor-webrtc==0.21.0",
|
|
46
46
|
"uvicorn>=0.49.0",
|
|
47
47
|
]
|
|
48
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",
|
|
@@ -243,9 +243,22 @@ class SessionStarted(ReactorEvent):
|
|
|
243
243
|
|
|
244
244
|
Attributes:
|
|
245
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.
|
|
246
249
|
"""
|
|
247
250
|
|
|
248
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
|
+
"""
|
|
249
262
|
|
|
250
263
|
|
|
251
264
|
@dataclass(frozen=True)
|
|
@@ -274,10 +287,12 @@ class ClientConnected(ReactorEvent):
|
|
|
274
287
|
Attributes:
|
|
275
288
|
conn_id: The connection that opened.
|
|
276
289
|
total: Live connection count after the open.
|
|
290
|
+
system: Whether the connection is the runtime's own system client.
|
|
277
291
|
"""
|
|
278
292
|
|
|
279
293
|
conn_id: ConnId
|
|
280
294
|
total: int
|
|
295
|
+
system: bool = False
|
|
281
296
|
|
|
282
297
|
|
|
283
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``.
|
|
@@ -116,6 +146,7 @@ class RuntimeConfig:
|
|
|
116
146
|
session drain it includes, before the process is forced to exit.
|
|
117
147
|
orphan_timeout: Seconds a session may stay client-less before it closes.
|
|
118
148
|
recording: The recorder's configuration; disabled by default.
|
|
149
|
+
step_results: How each finished step is saved; disabled by default.
|
|
119
150
|
"""
|
|
120
151
|
|
|
121
152
|
model_ref: str
|
|
@@ -127,3 +158,4 @@ class RuntimeConfig:
|
|
|
127
158
|
exit_timeout: float = 10.0
|
|
128
159
|
orphan_timeout: float = 60.0
|
|
129
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
|
|
|
@@ -9,7 +9,7 @@ the import graph.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
from collections.abc import Iterable
|
|
12
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from enum import Enum, StrEnum
|
|
15
15
|
from typing import Any, NewType
|
|
@@ -44,6 +44,95 @@ class CommandFailure:
|
|
|
44
44
|
message: str
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class ClientTrackStat:
|
|
49
|
+
"""One WebRTC quality reading a client took for one of its tracks.
|
|
50
|
+
|
|
51
|
+
Carries the client's own view of its receive-side quality — bitrate, frame
|
|
52
|
+
rate, packet loss, jitter, round-trip time — sourced from the browser's own
|
|
53
|
+
``getStats()``, which is the only vantage point onto that side of the
|
|
54
|
+
connection. Values are taken as reported; the runtime attaches the
|
|
55
|
+
connection identity itself rather than trusting anything the payload might
|
|
56
|
+
claim, because the payload carries none.
|
|
57
|
+
|
|
58
|
+
``kind``/``direction``/``codec`` are fixed, structural facts about the
|
|
59
|
+
track — never a new value at runtime — so they stay their own fields;
|
|
60
|
+
:attr:`metrics` is where the actual measurements live, because that set
|
|
61
|
+
grows as new stats become worth reporting, without a schema change.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
timestamp: When the client took this reading, on its own clock, in
|
|
65
|
+
epoch milliseconds.
|
|
66
|
+
track_name: The track the reading is for.
|
|
67
|
+
kind: The track's kind; ``None`` when the client did not say.
|
|
68
|
+
direction: The track's direction from the client's own perspective;
|
|
69
|
+
``None`` when the client did not say.
|
|
70
|
+
codec: The negotiated codec, e.g. ``"VP9"``, ``"opus"`` — empty when
|
|
71
|
+
the browser hasn't reported it yet.
|
|
72
|
+
paused: Whether the client has this track paused right now.
|
|
73
|
+
metrics: The reading's measurements, by name — e.g.
|
|
74
|
+
``"bitrate_bps"``, ``"frames_per_second"``, ``"packets_lost"``,
|
|
75
|
+
``"packets_received"``, ``"jitter_ms"``, ``"round_trip_time_ms"``,
|
|
76
|
+
``"frames_decoded"``, ``"frames_dropped"``, ``"frame_width"``,
|
|
77
|
+
``"frame_height"``, ``"nack_count"``, ``"keyframe_requests"``.
|
|
78
|
+
A cumulative counter (e.g. ``packets_lost``) rides here as a raw
|
|
79
|
+
count, not a ratio computed once elsewhere — a ratio from two
|
|
80
|
+
readings with different totals cannot be re-aggregated correctly
|
|
81
|
+
afterward; a raw count can always be summed and divided once, at
|
|
82
|
+
query time.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
timestamp: int
|
|
86
|
+
track_name: str
|
|
87
|
+
kind: TrackKind | None
|
|
88
|
+
direction: ClientTrackDirection | None
|
|
89
|
+
codec: str
|
|
90
|
+
paused: bool
|
|
91
|
+
metrics: Mapping[str, float]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class ClientConnectionStat:
|
|
96
|
+
"""A connection-wide reading, not tied to any one track.
|
|
97
|
+
|
|
98
|
+
e.g. ``"available_outgoing_bitrate_bps"`` (the client engine's own
|
|
99
|
+
estimate of outgoing headroom — the encoder's own belief, not a
|
|
100
|
+
measurement of the user's internet) or ``"time_to_connect_ms"`` (how long
|
|
101
|
+
this connection waited to establish, start to "ready") — the latter a
|
|
102
|
+
one-time fact a client reports once rather than repeating on every batch,
|
|
103
|
+
which is why :class:`ClientStatsBatch` carries this as optional.
|
|
104
|
+
|
|
105
|
+
Attributes:
|
|
106
|
+
timestamp: When the client took this reading, on its own clock, in
|
|
107
|
+
epoch milliseconds.
|
|
108
|
+
metrics: The reading's measurements, by name.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
timestamp: int
|
|
112
|
+
metrics: Mapping[str, float]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(frozen=True)
|
|
116
|
+
class ClientStatsBatch:
|
|
117
|
+
"""A batch of client-observed quality readings, plus connection-wide facts.
|
|
118
|
+
|
|
119
|
+
Mirrors the wire's ``ClientStats`` message: per-track readings alongside
|
|
120
|
+
a fact set that applies to the whole connection rather than any one
|
|
121
|
+
track. A client typically sends ``connection_stat`` once, on its first
|
|
122
|
+
batch after connecting, and omits it on every batch after — most batches
|
|
123
|
+
therefore carry ``track_stats`` only.
|
|
124
|
+
|
|
125
|
+
Attributes:
|
|
126
|
+
track_stats: One reading per track this batch reports on. Empty on a
|
|
127
|
+
batch that carries only ``connection_stat``.
|
|
128
|
+
connection_stat: This batch's connection-wide reading, or ``None`` on
|
|
129
|
+
a batch that doesn't carry one — the common case.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
track_stats: Sequence[ClientTrackStat]
|
|
133
|
+
connection_stat: ClientConnectionStat | None
|
|
134
|
+
|
|
135
|
+
|
|
47
136
|
@dataclass(frozen=True, eq=False)
|
|
48
137
|
class InputFrame:
|
|
49
138
|
"""A single inbound media frame with its presentation timestamp.
|
|
@@ -103,6 +192,18 @@ class TrackDirection(StrEnum):
|
|
|
103
192
|
OUT = "out"
|
|
104
193
|
|
|
105
194
|
|
|
195
|
+
class ClientTrackDirection(StrEnum):
|
|
196
|
+
"""Direction of flow for a track, from the client's perspective.
|
|
197
|
+
|
|
198
|
+
The opposite polarity of :class:`TrackDirection`: a model output track
|
|
199
|
+
(``OUT``) is one the client receives (``RECVONLY``), and a model input
|
|
200
|
+
track (``IN``) is one the client sends (``SENDONLY``).
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
RECVONLY = "recvonly"
|
|
204
|
+
SENDONLY = "sendonly"
|
|
205
|
+
|
|
206
|
+
|
|
106
207
|
@dataclass(frozen=True)
|
|
107
208
|
class TrackInfo:
|
|
108
209
|
"""Immutable metadata describing a single media track.
|
|
@@ -321,6 +422,36 @@ class MediaChunk:
|
|
|
321
422
|
return split_batch(self.bundle)
|
|
322
423
|
|
|
323
424
|
|
|
425
|
+
@dataclass(frozen=True)
|
|
426
|
+
class CompletedStep:
|
|
427
|
+
"""One finished step, as the model reports it to the runtime.
|
|
428
|
+
|
|
429
|
+
The neutral form of a model's step report: the media as a bundle rather
|
|
430
|
+
than the model's own output class, and the extra files as bytes, so the
|
|
431
|
+
runtime can count, journal, and save the step without touching model code.
|
|
432
|
+
|
|
433
|
+
Attributes:
|
|
434
|
+
bundle: The media the step produced, or ``None`` when it produced none.
|
|
435
|
+
files: Extra files the model keeps with the step, keyed by file name.
|
|
436
|
+
error: Why the step failed, or ``None`` when it worked.
|
|
437
|
+
elapsed: Wall-clock seconds the step took, when measured.
|
|
438
|
+
session: The session the step belongs to, as the number of session
|
|
439
|
+
starts the model had received when the step began. The runtime
|
|
440
|
+
counts the session starts it posts the same way, so it can tell a
|
|
441
|
+
late report from an earlier session apart from the current one.
|
|
442
|
+
fps: The rate the step's media was emitted at, which a saved step's
|
|
443
|
+
video plays at: the measured throughput for a model that does not
|
|
444
|
+
pin its rate.
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
bundle: MediaBundle | None
|
|
448
|
+
files: Mapping[str, bytes] = field(default_factory=dict)
|
|
449
|
+
error: str | None = None
|
|
450
|
+
elapsed: float | None = None
|
|
451
|
+
session: int = 0
|
|
452
|
+
fps: float = 30.0
|
|
453
|
+
|
|
454
|
+
|
|
324
455
|
@dataclass(frozen=True)
|
|
325
456
|
class ConnectionCapabilities:
|
|
326
457
|
"""What media a connection's wire can carry.
|
|
@@ -11,6 +11,7 @@ from reactor_runtime.http.routes import (
|
|
|
11
11
|
EgressRoutes,
|
|
12
12
|
RecordingRoutes,
|
|
13
13
|
SessionRoutes,
|
|
14
|
+
StepRoutes,
|
|
14
15
|
UploadRoutes,
|
|
15
16
|
)
|
|
16
17
|
from reactor_runtime.http.server import HttpServer
|
|
@@ -20,5 +21,6 @@ __all__ = [
|
|
|
20
21
|
"HttpServer",
|
|
21
22
|
"RecordingRoutes",
|
|
22
23
|
"SessionRoutes",
|
|
24
|
+
"StepRoutes",
|
|
23
25
|
"UploadRoutes",
|
|
24
26
|
]
|