reactor-runtime 2.3.2__tar.gz → 2.4.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-2.3.2 → reactor_runtime-2.4.0}/PKG-INFO +2 -2
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/pyproject.toml +2 -2
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/__init__.py +2 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/__init__.py +6 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/driver/pipeline_executor.py +24 -6
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/internal/input_buffer.py +33 -20
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/internal/reactor_core.py +11 -3
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/runtimes/headless/input_feeder.py +11 -2
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/runtimes/http/http_runtime.py +23 -8
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/__init__.py +6 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/aiortc/audio_track.py +148 -2
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/aiortc/client.py +102 -4
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/events.py +23 -3
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/client.py +133 -37
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +2 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +5 -1
- reactor_runtime-2.4.0/src/reactor_runtime/transports/gstreamer/decoders/opus.py +54 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +15 -16
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/media.py +43 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime.egg-info/PKG-INFO +2 -2
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime.egg-info/SOURCES.txt +1 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime.egg-info/requires.txt +1 -1
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/README.md +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/setup.cfg +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/api/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_cli/commands/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_cli/commands/init.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_cli/commands/run.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_cli/commands/schema.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_cli/main.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_cli/utils/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_cli/utils/config.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_cli/utils/runtime.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_cli/utils/version.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/config.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/defaults.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/driver/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/driver/step_result.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/events/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/events/connected.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/events/event.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/events/messages.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/events/upload.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/internal/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/internal/output_buffer.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/model/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/model/decorators.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/model/handlers.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/tracks/input.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/tracks/output.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/interface/upload.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/model_state.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/profiling/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/profiling/backends/base.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/profiling/backends/file.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/profiling/helpers.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/profiling/profiler.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/profiling/singleton.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/runtime_api.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/runtimes/headless/config.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/runtimes/headless/headless_runtime.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/runtimes/http/config.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/runtimes/http/types.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/schema.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/schema_validator.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/aiortc/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/aiortc/frame_conversion.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/aiortc/ice_connection.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/aiortc/video_track.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/config.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/probes/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/probes/fps_probe.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/sender/video.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/settings.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/ice_uris.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/interface.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/types.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/utils/launch.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/utils/loader.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/utils/log.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/utils/messages.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/utils/typing.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime.egg-info/entry_points.txt +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime.egg-info/top_level.txt +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/template/README.md +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/template/__init__.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/template/config.yml +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/template/model.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/template/pipeline.py +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/template/reactor.yaml +0 -0
- {reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/template/requirements.txt +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactor_runtime
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Reactor runtime with public model API
|
|
5
5
|
Author-email: Reactor <team@reactor.inc>
|
|
6
6
|
Requires-Python: >=3.9
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist: numpy
|
|
8
|
+
Requires-Dist: numpy>=1.24.0
|
|
9
9
|
Requires-Dist: pydantic>=2.0.0
|
|
10
10
|
Requires-Dist: omegaconf>=2.3.0
|
|
11
11
|
Requires-Dist: av>=14.0.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactor_runtime"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.4.0"
|
|
8
8
|
description = "Reactor runtime with public model API"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Reactor", email = "team@reactor.inc" }
|
|
@@ -13,7 +13,7 @@ readme = "README.md"
|
|
|
13
13
|
requires-python = ">=3.9"
|
|
14
14
|
|
|
15
15
|
dependencies = [
|
|
16
|
-
"numpy
|
|
16
|
+
"numpy>=1.24.0",
|
|
17
17
|
"pydantic>=2.0.0",
|
|
18
18
|
"omegaconf>=2.3.0",
|
|
19
19
|
"av>=14.0.0",
|
|
@@ -14,6 +14,7 @@ from reactor_runtime.interface import (
|
|
|
14
14
|
MESSAGE_REGISTRY,
|
|
15
15
|
FieldInfo,
|
|
16
16
|
InputField,
|
|
17
|
+
InputFrame,
|
|
17
18
|
Output,
|
|
18
19
|
Input,
|
|
19
20
|
Video,
|
|
@@ -45,6 +46,7 @@ __all__ = [
|
|
|
45
46
|
"MESSAGE_REGISTRY",
|
|
46
47
|
"FieldInfo",
|
|
47
48
|
"InputField",
|
|
49
|
+
"InputFrame",
|
|
48
50
|
"Output",
|
|
49
51
|
"Input",
|
|
50
52
|
"Video",
|
|
@@ -54,6 +54,10 @@ from reactor_runtime.interface.internal.input_buffer import (
|
|
|
54
54
|
ReadMode,
|
|
55
55
|
)
|
|
56
56
|
|
|
57
|
+
# Inbound frame with timing metadata (re-exported from transports so model
|
|
58
|
+
# authors can ``from reactor_runtime.interface import InputFrame``).
|
|
59
|
+
from reactor_runtime.transports.media import InputFrame
|
|
60
|
+
|
|
57
61
|
__all__ = [
|
|
58
62
|
# Tracks
|
|
59
63
|
"Output",
|
|
@@ -95,4 +99,6 @@ __all__ = [
|
|
|
95
99
|
"InputBuffer",
|
|
96
100
|
"BufferClosed",
|
|
97
101
|
"ReadMode",
|
|
102
|
+
# Inbound frame
|
|
103
|
+
"InputFrame",
|
|
98
104
|
]
|
|
@@ -6,7 +6,9 @@ from __future__ import annotations
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import dataclasses
|
|
8
8
|
import inspect
|
|
9
|
-
from typing import Any, Dict, Iterator, List, Optional, Type
|
|
9
|
+
from typing import Any, Dict, Iterator, List, Optional, Type, Union
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
10
12
|
|
|
11
13
|
from reactor_runtime.interface.driver.step_result import StepResult
|
|
12
14
|
from reactor_runtime.interface.events.messages import ModelMessage
|
|
@@ -17,6 +19,7 @@ from reactor_runtime.interface.pipeline.reactor_pipeline import (
|
|
|
17
19
|
GeneratorEnded,
|
|
18
20
|
ReactorPipeline,
|
|
19
21
|
)
|
|
22
|
+
from reactor_runtime.transports.media import InputFrame
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
class PipelineExecutor(Iterator[StepResult]):
|
|
@@ -166,14 +169,29 @@ class PipelineExecutor(Iterator[StepResult]):
|
|
|
166
169
|
}
|
|
167
170
|
return self._run(self._send_event(entry, **handler_kwargs))
|
|
168
171
|
|
|
169
|
-
def push_media(self, track_name: str, data:
|
|
172
|
+
def push_media(self, track_name: str, data: Union[np.ndarray, InputFrame]) -> None:
|
|
170
173
|
"""Push a media frame into an input buffer.
|
|
171
174
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
``
|
|
175
|
+
Accepts either a raw ``np.ndarray`` (auto-wrapped into an
|
|
176
|
+
:class:`~reactor_runtime.transports.media.InputFrame` with
|
|
177
|
+
``pts=None``) or a prebuilt :class:`InputFrame` for callers
|
|
178
|
+
that want to stamp the frame with an explicit presentation
|
|
179
|
+
timestamp.
|
|
180
|
+
|
|
181
|
+
The frame becomes available to the model on the next iteration
|
|
182
|
+
when ``inference()`` calls ``try_read()`` / ``read()`` — both
|
|
183
|
+
of which now return ``List[InputFrame]``.
|
|
175
184
|
"""
|
|
176
|
-
|
|
185
|
+
if isinstance(data, InputFrame):
|
|
186
|
+
frame = data
|
|
187
|
+
elif isinstance(data, np.ndarray):
|
|
188
|
+
frame = InputFrame(data=data)
|
|
189
|
+
else:
|
|
190
|
+
raise TypeError(
|
|
191
|
+
"push_media expects an np.ndarray or InputFrame, got "
|
|
192
|
+
f"{type(data).__name__}"
|
|
193
|
+
)
|
|
194
|
+
self._model._push_media(track_name, frame)
|
|
177
195
|
|
|
178
196
|
def disconnect(self) -> List[ModelMessage]:
|
|
179
197
|
"""End the current session.
|
|
@@ -26,7 +26,7 @@ from collections import deque
|
|
|
26
26
|
from enum import Enum, auto
|
|
27
27
|
from typing import List, Optional
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
from reactor_runtime.transports.media import InputFrame
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class ReadMode(Enum):
|
|
@@ -64,12 +64,16 @@ class InputBuffer:
|
|
|
64
64
|
Accessed via :class:`Input` track handles — model authors never
|
|
65
65
|
instantiate this class directly.
|
|
66
66
|
|
|
67
|
-
Read API:
|
|
67
|
+
Read API (returns :class:`~reactor_runtime.transports.media.InputFrame`):
|
|
68
68
|
|
|
69
69
|
- :meth:`read` — async, blocks until *n* frames are available.
|
|
70
70
|
- :meth:`try_read` — sync, returns immediately (latest frame or
|
|
71
71
|
``None``).
|
|
72
72
|
|
|
73
|
+
Both return ``List[InputFrame]``. Callers unwrap the ``.data``
|
|
74
|
+
``np.ndarray`` for numpy operations and ``.pts`` when they need
|
|
75
|
+
the presentation timestamp for cross-track alignment.
|
|
76
|
+
|
|
73
77
|
Lifecycle:
|
|
74
78
|
|
|
75
79
|
- :meth:`close` — signal end-of-input (raises :class:`BufferClosed`
|
|
@@ -93,7 +97,7 @@ class InputBuffer:
|
|
|
93
97
|
self._lock = threading.Lock()
|
|
94
98
|
self._condition = threading.Condition(self._lock)
|
|
95
99
|
|
|
96
|
-
self._buffer: deque[
|
|
100
|
+
self._buffer: deque[InputFrame] = deque(maxlen=maxlen)
|
|
97
101
|
self._total_received: int = 0
|
|
98
102
|
self._closed: bool = False
|
|
99
103
|
|
|
@@ -106,8 +110,8 @@ class InputBuffer:
|
|
|
106
110
|
n: int = 1,
|
|
107
111
|
timeout: Optional[float] = None,
|
|
108
112
|
mode: ReadMode = ReadMode.LATEST,
|
|
109
|
-
) -> List[
|
|
110
|
-
"""Return *n*
|
|
113
|
+
) -> List[InputFrame]:
|
|
114
|
+
"""Return *n* :class:`InputFrame` objects, waiting if necessary.
|
|
111
115
|
|
|
112
116
|
Blocks the calling coroutine (via ``asyncio.to_thread``) until
|
|
113
117
|
at least *n* frames are present in the buffer, then returns
|
|
@@ -118,7 +122,8 @@ class InputBuffer:
|
|
|
118
122
|
Typical usage::
|
|
119
123
|
|
|
120
124
|
frames = await self.input.camera.read(4) # newest 4
|
|
121
|
-
|
|
125
|
+
latest = frames[-1].data # np.ndarray
|
|
126
|
+
pts = frames[-1].pts # seconds or None
|
|
122
127
|
|
|
123
128
|
Args:
|
|
124
129
|
n: Number of frames to return.
|
|
@@ -130,7 +135,11 @@ class InputBuffer:
|
|
|
130
135
|
leaving newer frames in the buffer.
|
|
131
136
|
|
|
132
137
|
Returns:
|
|
133
|
-
A list of *n*
|
|
138
|
+
A list of *n* :class:`InputFrame` objects. Each carries
|
|
139
|
+
``data`` (the numpy payload — HWC uint8 RGB for video,
|
|
140
|
+
``(1, M)`` int16 for audio) and ``pts`` (presentation
|
|
141
|
+
timestamp in seconds, or ``None`` when the transport did
|
|
142
|
+
not provide one).
|
|
134
143
|
|
|
135
144
|
Raises:
|
|
136
145
|
BufferClosed: The client disconnected while waiting.
|
|
@@ -144,7 +153,7 @@ class InputBuffer:
|
|
|
144
153
|
n: int,
|
|
145
154
|
timeout: Optional[float],
|
|
146
155
|
mode: ReadMode = ReadMode.LATEST,
|
|
147
|
-
) -> List[
|
|
156
|
+
) -> List[InputFrame]:
|
|
148
157
|
if self._buffer.maxlen is not None and n > self._buffer.maxlen:
|
|
149
158
|
raise ValueError(
|
|
150
159
|
f"Requested {n} frames but buffer capacity is {self._buffer.maxlen}. "
|
|
@@ -165,24 +174,27 @@ class InputBuffer:
|
|
|
165
174
|
return self._read_latest(n)
|
|
166
175
|
return self._read_fifo(n)
|
|
167
176
|
|
|
168
|
-
def _read_latest(self, n: int) -> List[
|
|
169
|
-
"""Return the *n* newest
|
|
177
|
+
def _read_latest(self, n: int) -> List[InputFrame]:
|
|
178
|
+
"""Return the *n* newest :class:`InputFrame` objects and discard the rest.
|
|
179
|
+
|
|
180
|
+
Caller holds the lock.
|
|
181
|
+
"""
|
|
170
182
|
buf_len = len(self._buffer)
|
|
171
183
|
start = buf_len - n
|
|
172
184
|
result = [self._buffer[start + i] for i in range(n)]
|
|
173
185
|
self._buffer.clear()
|
|
174
186
|
return result
|
|
175
187
|
|
|
176
|
-
def _read_fifo(self, n: int) -> List[
|
|
177
|
-
"""Pop the *n* oldest
|
|
188
|
+
def _read_fifo(self, n: int) -> List[InputFrame]:
|
|
189
|
+
"""Pop the *n* oldest :class:`InputFrame` objects. Caller holds the lock."""
|
|
178
190
|
return [self._buffer.popleft() for _ in range(n)]
|
|
179
191
|
|
|
180
192
|
def try_read(
|
|
181
193
|
self,
|
|
182
194
|
n: int = 1,
|
|
183
195
|
mode: ReadMode = ReadMode.LATEST,
|
|
184
|
-
) -> Optional[List[
|
|
185
|
-
"""Try to read *n*
|
|
196
|
+
) -> Optional[List[InputFrame]]:
|
|
197
|
+
"""Try to read *n* :class:`InputFrame` objects without blocking.
|
|
186
198
|
|
|
187
199
|
Returns ``None`` when fewer than *n* frames are available —
|
|
188
200
|
the buffer is left untouched so nothing is lost.
|
|
@@ -195,7 +207,7 @@ class InputBuffer:
|
|
|
195
207
|
leaving newer frames in the buffer.
|
|
196
208
|
|
|
197
209
|
Returns:
|
|
198
|
-
A list of *n*
|
|
210
|
+
A list of *n* :class:`InputFrame` objects, or ``None`` if
|
|
199
211
|
fewer than *n* are available.
|
|
200
212
|
|
|
201
213
|
Raises:
|
|
@@ -274,14 +286,15 @@ class InputBuffer:
|
|
|
274
286
|
#
|
|
275
287
|
# Call chain:
|
|
276
288
|
# WebRTC transport (on_track / on_frame callback)
|
|
277
|
-
# →
|
|
278
|
-
# →
|
|
289
|
+
# → VideoFrameEvent(frame=InputFrame(data, pts), track_name=...)
|
|
290
|
+
# → ReactorCore._push_media(track_name, frame)
|
|
291
|
+
# → InputBuffer._push(frame)
|
|
279
292
|
#
|
|
280
293
|
# Thread-safe: the model thread may be blocked in read()
|
|
281
294
|
# concurrently — notify_all wakes it.
|
|
282
295
|
|
|
283
|
-
def _push(self,
|
|
284
|
-
"""Append
|
|
296
|
+
def _push(self, frame: InputFrame) -> None:
|
|
297
|
+
"""Append an :class:`InputFrame` (called by the runtime, not model code).
|
|
285
298
|
|
|
286
299
|
Silently drops the frame if the buffer has been closed.
|
|
287
300
|
When full, the oldest frame is evicted (bounded deque).
|
|
@@ -289,7 +302,7 @@ class InputBuffer:
|
|
|
289
302
|
with self._condition:
|
|
290
303
|
if self._closed:
|
|
291
304
|
return
|
|
292
|
-
self._buffer.append(
|
|
305
|
+
self._buffer.append(frame)
|
|
293
306
|
self._total_received += 1
|
|
294
307
|
# Wake _read() if it's sleeping in wait_for() — it will
|
|
295
308
|
# re-acquire the lock and re-check len(buffer) >= n.
|
|
@@ -28,6 +28,7 @@ from reactor_runtime.interface.internal.input_buffer import InputBuffer
|
|
|
28
28
|
from reactor_runtime.interface.tracks.descriptors import TRACK_MARKERS
|
|
29
29
|
from reactor_runtime.interface.tracks.input import INPUT_REGISTRY, Input
|
|
30
30
|
from reactor_runtime.interface.tracks.output import Output
|
|
31
|
+
from reactor_runtime.transports.media import InputFrame
|
|
31
32
|
from reactor_runtime.utils.log import get_logger
|
|
32
33
|
|
|
33
34
|
logger = get_logger(__name__)
|
|
@@ -198,13 +199,20 @@ class ReactorCore:
|
|
|
198
199
|
except RuntimeError:
|
|
199
200
|
pass
|
|
200
201
|
|
|
201
|
-
def _push_media(self, track_name: str,
|
|
202
|
-
"""Thread-safe: push
|
|
202
|
+
def _push_media(self, track_name: str, frame: InputFrame) -> None:
|
|
203
|
+
"""Thread-safe: push an :class:`InputFrame` into the matching InputBuffer.
|
|
204
|
+
|
|
205
|
+
Strict typing: callers must build an :class:`InputFrame` (transports
|
|
206
|
+
do so at the ingress boundary). Permissive auto-wrapping of a bare
|
|
207
|
+
``np.ndarray`` happens at the outer public surface
|
|
208
|
+
(``PipelineExecutor.push_media``) — not here — so this stays a
|
|
209
|
+
simple threadsafe dispatch.
|
|
210
|
+
"""
|
|
203
211
|
if self._loop.is_closed():
|
|
204
212
|
return
|
|
205
213
|
buf = self._input_buffers.get(track_name)
|
|
206
214
|
if buf is not None:
|
|
207
|
-
buf._push(
|
|
215
|
+
buf._push(frame)
|
|
208
216
|
else:
|
|
209
217
|
logger.debug(
|
|
210
218
|
"Received media for unknown input track",
|
|
@@ -17,7 +17,7 @@ from typing import Optional
|
|
|
17
17
|
import cv2
|
|
18
18
|
|
|
19
19
|
from reactor_runtime.interface.internal.reactor_core import ReactorCore
|
|
20
|
-
from reactor_runtime.transports.media import MediaBundle
|
|
20
|
+
from reactor_runtime.transports.media import InputFrame, MediaBundle
|
|
21
21
|
from reactor_runtime.utils.log import get_logger
|
|
22
22
|
|
|
23
23
|
logger = get_logger(__name__)
|
|
@@ -222,9 +222,18 @@ class InputFrameFeeder:
|
|
|
222
222
|
continue
|
|
223
223
|
|
|
224
224
|
frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
|
|
225
|
+
# Synthesise a presentation timestamp from the frame index
|
|
226
|
+
# when running at a fixed input fps, so downstream model
|
|
227
|
+
# code reading via ``read()`` / ``try_read()`` sees
|
|
228
|
+
# monotonically increasing ``pts``. Without an fps target
|
|
229
|
+
# we leave ``pts`` unset (``None``).
|
|
230
|
+
pts = (i / self._input_fps) if self._input_fps else None
|
|
225
231
|
bundle = MediaBundle.from_video_frame(frame_rgb)
|
|
226
232
|
for track_name, track_data in bundle.tracks.items():
|
|
227
|
-
self._model._push_media(
|
|
233
|
+
self._model._push_media(
|
|
234
|
+
track_name,
|
|
235
|
+
InputFrame(data=track_data.data, pts=pts),
|
|
236
|
+
)
|
|
228
237
|
fed += 1
|
|
229
238
|
self.frames_fed = fed
|
|
230
239
|
|
{reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/runtimes/http/http_runtime.py
RENAMED
|
@@ -25,7 +25,7 @@ import asyncio
|
|
|
25
25
|
import os
|
|
26
26
|
import uuid
|
|
27
27
|
from typing import Any, Dict, Optional
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
from fastapi import FastAPI, HTTPException, Request
|
|
30
30
|
from importlib.metadata import version as _pkg_version, PackageNotFoundError
|
|
31
31
|
|
|
@@ -38,6 +38,7 @@ import uvicorn
|
|
|
38
38
|
|
|
39
39
|
from reactor_runtime.runtime_api import Runtime
|
|
40
40
|
from reactor_runtime.transports.media import (
|
|
41
|
+
InputFrame,
|
|
41
42
|
MediaBundle,
|
|
42
43
|
TrackMapping,
|
|
43
44
|
)
|
|
@@ -48,6 +49,7 @@ from reactor_runtime.transports import (
|
|
|
48
49
|
EventType,
|
|
49
50
|
MessageEvent,
|
|
50
51
|
DisconnectedEvent,
|
|
52
|
+
AudioFrameEvent,
|
|
51
53
|
VideoFrameEvent,
|
|
52
54
|
PingTimeoutEvent,
|
|
53
55
|
)
|
|
@@ -471,6 +473,9 @@ class HttpRuntime(Runtime):
|
|
|
471
473
|
def on_video_frame(event: VideoFrameEvent):
|
|
472
474
|
self._on_incoming_video_frame(event.frame, event.track_name)
|
|
473
475
|
|
|
476
|
+
def on_audio_frame(event: AudioFrameEvent):
|
|
477
|
+
self._on_incoming_audio_frame(event.frame, event.track_name)
|
|
478
|
+
|
|
474
479
|
def on_ping_timeout(event: PingTimeoutEvent):
|
|
475
480
|
if not self.loop.is_closed():
|
|
476
481
|
self.loop.call_soon_threadsafe(
|
|
@@ -480,6 +485,7 @@ class HttpRuntime(Runtime):
|
|
|
480
485
|
client.on(EventType.MESSAGE, on_message)
|
|
481
486
|
client.on(EventType.DISCONNECTED, on_disconnect)
|
|
482
487
|
client.on(EventType.VIDEO_FRAME, on_video_frame)
|
|
488
|
+
client.on(EventType.AUDIO_FRAME, on_audio_frame)
|
|
483
489
|
client.on(EventType.PING_TIMEOUT, on_ping_timeout)
|
|
484
490
|
|
|
485
491
|
async def _stop_webrtc_client(self) -> None:
|
|
@@ -512,16 +518,25 @@ class HttpRuntime(Runtime):
|
|
|
512
518
|
self._webrtc_client.notify_ping()
|
|
513
519
|
|
|
514
520
|
def _on_incoming_video_frame(
|
|
515
|
-
self, frame:
|
|
521
|
+
self, frame: InputFrame, track_name: str = "video"
|
|
516
522
|
) -> None:
|
|
517
|
-
"""Handle incoming video
|
|
523
|
+
"""Handle an incoming video :class:`InputFrame` from the WebRTC transport.
|
|
524
|
+
|
|
525
|
+
Routes the frame into the model's per-track :class:`InputBuffer`,
|
|
526
|
+
preserving ``pts`` so model code can read it via
|
|
527
|
+
``(await self.input.<track>.read(n))[i].pts``.
|
|
528
|
+
"""
|
|
529
|
+
if self.model is not None:
|
|
530
|
+
self.model._push_media(track_name, frame)
|
|
518
531
|
|
|
519
|
-
|
|
520
|
-
|
|
532
|
+
def _on_incoming_audio_frame(
|
|
533
|
+
self, frame: InputFrame, track_name: str = "audio"
|
|
534
|
+
) -> None:
|
|
535
|
+
"""Handle an incoming audio :class:`InputFrame` from the WebRTC transport.
|
|
521
536
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
537
|
+
Routes the frame into the model's per-track :class:`InputBuffer`,
|
|
538
|
+
preserving ``pts`` so model code can align audio chunks with
|
|
539
|
+
video frames by timestamp.
|
|
525
540
|
"""
|
|
526
541
|
if self.model is not None:
|
|
527
542
|
self.model._push_media(track_name, frame)
|
|
@@ -23,6 +23,7 @@ Public API re-exported here for convenience::
|
|
|
23
23
|
TrackInfo,
|
|
24
24
|
TrackData,
|
|
25
25
|
MediaBundle,
|
|
26
|
+
InputFrame,
|
|
26
27
|
# Events
|
|
27
28
|
EventType,
|
|
28
29
|
WebRTCNoVideoError,
|
|
@@ -32,6 +33,7 @@ Public API re-exported here for convenience::
|
|
|
32
33
|
DisconnectedEvent,
|
|
33
34
|
MessageEvent,
|
|
34
35
|
VideoFrameEvent,
|
|
36
|
+
AudioFrameEvent,
|
|
35
37
|
MediaBundleEvent,
|
|
36
38
|
PingTimeoutEvent,
|
|
37
39
|
EventHandler,
|
|
@@ -47,6 +49,7 @@ from reactor_runtime.transports.events import (
|
|
|
47
49
|
MessageEvent,
|
|
48
50
|
PingTimeoutEvent,
|
|
49
51
|
StatsMeasuredEvent,
|
|
52
|
+
AudioFrameEvent,
|
|
50
53
|
VideoFrameEvent,
|
|
51
54
|
WebRTCEvent,
|
|
52
55
|
WebRTCNoVideoError,
|
|
@@ -55,6 +58,7 @@ from reactor_runtime.transports.events import (
|
|
|
55
58
|
)
|
|
56
59
|
from reactor_runtime.transports.interface import WebRTCTransport
|
|
57
60
|
from reactor_runtime.transports.media import (
|
|
61
|
+
InputFrame,
|
|
58
62
|
MediaBundle,
|
|
59
63
|
TrackData,
|
|
60
64
|
TrackDirection,
|
|
@@ -100,6 +104,7 @@ __all__ = [
|
|
|
100
104
|
"TrackMapping",
|
|
101
105
|
"TrackData",
|
|
102
106
|
"MediaBundle",
|
|
107
|
+
"InputFrame",
|
|
103
108
|
# Events
|
|
104
109
|
"EventType",
|
|
105
110
|
"WebRTCNoVideoError",
|
|
@@ -110,6 +115,7 @@ __all__ = [
|
|
|
110
115
|
"DisconnectedEvent",
|
|
111
116
|
"MessageEvent",
|
|
112
117
|
"VideoFrameEvent",
|
|
118
|
+
"AudioFrameEvent",
|
|
113
119
|
"MediaBundleEvent",
|
|
114
120
|
"PingTimeoutEvent",
|
|
115
121
|
"StatsMeasuredEvent",
|
{reactor_runtime-2.3.2 → reactor_runtime-2.4.0}/src/reactor_runtime/transports/aiortc/audio_track.py
RENAMED
|
@@ -36,7 +36,7 @@ from fractions import Fraction
|
|
|
36
36
|
from typing import Optional
|
|
37
37
|
|
|
38
38
|
import numpy as np
|
|
39
|
-
from aiortc import
|
|
39
|
+
from aiortc import AudioStreamTrack
|
|
40
40
|
from av import AudioFrame
|
|
41
41
|
|
|
42
42
|
from reactor_runtime.utils.log import get_logger
|
|
@@ -70,8 +70,154 @@ _FADE_SAMPLES = 64
|
|
|
70
70
|
# to cap latency. 1 second at 48 kHz = 48 000.
|
|
71
71
|
MAX_BUFFER_SAMPLES = 48000
|
|
72
72
|
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# NumPy <-> AudioFrame helpers
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _audio_plane_to_bytes(plane) -> bytes:
|
|
79
|
+
"""Raw bytes for a PyAV ``AudioPlane``/``VideoPlane`` (``to_bytes()`` or buffer)."""
|
|
80
|
+
to_b = getattr(plane, "to_bytes", None)
|
|
81
|
+
if callable(to_b):
|
|
82
|
+
return to_b()
|
|
83
|
+
return bytes(plane)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _float_to_s16_samples(arr: np.ndarray) -> np.ndarray:
|
|
87
|
+
"""FFmpeg/ libav ``fltp`` samples in approximately ``[-1.0, 1.0]`` → ``int16``."""
|
|
88
|
+
a = np.asarray(arr, dtype=np.float64)
|
|
89
|
+
a = np.clip(a, -1.0, 1.0)
|
|
90
|
+
return np.rint(a * 32767.0).astype(np.int16)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _frame_channel_count(frame: AudioFrame) -> int:
|
|
94
|
+
"""Best-effort channel count from the frame (layout, else plane count)."""
|
|
95
|
+
layout = getattr(frame, "layout", None)
|
|
96
|
+
if layout is not None:
|
|
97
|
+
nb = getattr(layout, "nb_channels", None)
|
|
98
|
+
if isinstance(nb, int) and nb > 0:
|
|
99
|
+
return nb
|
|
100
|
+
chs = getattr(layout, "channels", None)
|
|
101
|
+
if chs is not None and len(chs) > 0:
|
|
102
|
+
return len(chs)
|
|
103
|
+
n_planes = len(frame.planes)
|
|
104
|
+
return max(1, n_planes) if n_planes else 1
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _s16_to_mono_1d(arr: np.ndarray, frame: AudioFrame) -> np.ndarray:
|
|
108
|
+
"""Collapse any supported layout to one **int16** vector (first channel only)."""
|
|
109
|
+
arr = np.asarray(arr, dtype=np.int16, order="C")
|
|
110
|
+
nch = _frame_channel_count(frame)
|
|
111
|
+
samples = int(getattr(frame, "samples", 0) or 0)
|
|
112
|
+
|
|
113
|
+
if arr.size == 0:
|
|
114
|
+
return arr
|
|
115
|
+
|
|
116
|
+
if arr.ndim == 1:
|
|
117
|
+
if nch <= 1:
|
|
118
|
+
return arr.copy()
|
|
119
|
+
if samples > 0 and arr.size == samples * nch:
|
|
120
|
+
return arr.reshape(-1, nch)[:, 0].copy()
|
|
121
|
+
if samples > 0 and arr.size == samples:
|
|
122
|
+
return arr.copy()
|
|
123
|
+
return arr.copy()
|
|
124
|
+
|
|
125
|
+
if arr.ndim == 2:
|
|
126
|
+
# Planar: (nch, samples) — e.g. s16p, fltp with separate planes per channel.
|
|
127
|
+
if nch > 1 and samples > 0 and arr.shape[0] == nch and arr.shape[1] == samples:
|
|
128
|
+
return arr[0].copy()
|
|
129
|
+
# Transposed planar: (samples, nch).
|
|
130
|
+
if nch > 1 and samples > 0 and arr.shape[1] == nch and arr.shape[0] == samples:
|
|
131
|
+
return arr[:, 0].copy()
|
|
132
|
+
# Packed interleaved: (1, samples * nch) — PyAV's default return shape for
|
|
133
|
+
# s16 / fltp interleaved (one plane holding [L, R, L, R, ...]). Without
|
|
134
|
+
# this case the (1, 1920) mono-ish array for 960-sample stereo falls into
|
|
135
|
+
# the `shape[0] <= shape[1]` heuristic below and is returned as-is, which
|
|
136
|
+
# feeds interleaved stereo into the model as if it were mono — effectively
|
|
137
|
+
# doubling the sample rate and producing unintelligible high-frequency
|
|
138
|
+
# noise on playback.
|
|
139
|
+
if (
|
|
140
|
+
nch > 1
|
|
141
|
+
and samples > 0
|
|
142
|
+
and arr.shape[0] == 1
|
|
143
|
+
and arr.shape[1] == samples * nch
|
|
144
|
+
):
|
|
145
|
+
return arr[0].reshape(-1, nch)[:, 0].copy()
|
|
146
|
+
# Mono packed: (1, samples).
|
|
147
|
+
if nch <= 1 and arr.shape[0] == 1:
|
|
148
|
+
return arr[0].copy()
|
|
149
|
+
# Heuristic fallback for unusual layouts.
|
|
150
|
+
if arr.shape[0] <= arr.shape[1]:
|
|
151
|
+
return arr[0].copy()
|
|
152
|
+
return arr[:, 0].copy()
|
|
153
|
+
|
|
154
|
+
flat = arr.ravel()
|
|
155
|
+
if samples > 0 and nch > 1 and flat.size == samples * nch:
|
|
156
|
+
return flat.reshape(-1, nch)[:, 0].copy()
|
|
157
|
+
return flat.copy()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def audio_frame_to_numpy(frame: AudioFrame) -> np.ndarray:
|
|
161
|
+
"""Convert an :class:`AudioFrame` to a ``(1, N)`` NumPy **int16 (s16, mono)** array.
|
|
162
|
+
|
|
163
|
+
Uses :meth:`~av.audio.frame.AudioFrame.to_ndarray` when available
|
|
164
|
+
(handles ``s16`` / ``s16p`` / ``fltp``, planar vs interleaved), then
|
|
165
|
+
normalizes to ``int16`` and **keeps the first channel only** (stereo
|
|
166
|
+
and multi-plane sources are down-selected, not down-mixed).
|
|
167
|
+
|
|
168
|
+
If ``to_ndarray`` is missing, decodes 16-bit PCM from ``frame.planes``
|
|
169
|
+
(single interleaved buffer or one plane per channel for ``s16p``).
|
|
170
|
+
|
|
171
|
+
The result matches the documented transport audio contract in
|
|
172
|
+
:mod:`reactor_runtime.transports.media` — ``(1, N)`` ``int16`` mono
|
|
173
|
+
— so the GStreamer and aiortc transports produce identically-shaped
|
|
174
|
+
buffers on the model's ``InputBuffer`` (before the runtime, only
|
|
175
|
+
aiortc produced a 1-D ``(N,)`` array, which broke any consumer that
|
|
176
|
+
indexed ``chunk[0]`` or read ``chunk.shape[1]``).
|
|
73
177
|
|
|
74
|
-
|
|
178
|
+
Args:
|
|
179
|
+
frame: The :class:`AudioFrame` to convert.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
``np.ndarray`` of dtype ``int16``, shape ``(1, N)`` — *N* samples
|
|
183
|
+
in one channel.
|
|
184
|
+
"""
|
|
185
|
+
flat = _audio_frame_to_mono_1d(frame)
|
|
186
|
+
return flat.reshape(1, -1)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _audio_frame_to_mono_1d(frame: AudioFrame) -> np.ndarray:
|
|
190
|
+
"""Internal: flat int16 mono extractor (1-D), used by :func:`audio_frame_to_numpy`."""
|
|
191
|
+
to_nd = getattr(frame, "to_ndarray", None)
|
|
192
|
+
if callable(to_nd):
|
|
193
|
+
arr = np.asarray(to_nd())
|
|
194
|
+
if arr.dtype in (np.float32, np.float64):
|
|
195
|
+
return _s16_to_mono_1d(_float_to_s16_samples(arr), frame)
|
|
196
|
+
if arr.dtype == np.int16:
|
|
197
|
+
return _s16_to_mono_1d(arr, frame)
|
|
198
|
+
if arr.dtype in (np.int32, np.int64, np.uint16):
|
|
199
|
+
return _s16_to_mono_1d(np.clip(arr, -32768, 32767).astype(np.int16), frame)
|
|
200
|
+
|
|
201
|
+
# Fallback: 16-bit PCM in one or more planes (or ``to_ndarray`` missing)
|
|
202
|
+
n_planes = len(frame.planes)
|
|
203
|
+
if n_planes == 0:
|
|
204
|
+
return np.empty(0, dtype=np.int16)
|
|
205
|
+
if n_planes == 1:
|
|
206
|
+
raw = np.frombuffer(
|
|
207
|
+
_audio_plane_to_bytes(frame.planes[0]),
|
|
208
|
+
dtype=np.int16,
|
|
209
|
+
)
|
|
210
|
+
return _s16_to_mono_1d(raw, frame)
|
|
211
|
+
chans = [
|
|
212
|
+
np.frombuffer(_audio_plane_to_bytes(p), dtype=np.int16) for p in frame.planes
|
|
213
|
+
]
|
|
214
|
+
n = min(c.size for c in chans)
|
|
215
|
+
if n == 0:
|
|
216
|
+
return np.empty(0, dtype=np.int16)
|
|
217
|
+
return chans[0][:n].copy()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class OutputAudioTrack(AudioStreamTrack):
|
|
75
221
|
"""Audio track that outputs samples provided via :meth:`push_samples`.
|
|
76
222
|
|
|
77
223
|
Designed to be fed audio data from the model's output. Thread-safe
|