reactor-runtime 0.0.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-0.0.0/PKG-INFO +131 -0
- reactor_runtime-0.0.0/README.md +105 -0
- reactor_runtime-0.0.0/pyproject.toml +37 -0
- reactor_runtime-0.0.0/setup.cfg +4 -0
- reactor_runtime-0.0.0/src/reactor_cli/commands/__init__.py +14 -0
- reactor_runtime-0.0.0/src/reactor_cli/commands/capabilities.py +118 -0
- reactor_runtime-0.0.0/src/reactor_cli/commands/download.py +217 -0
- reactor_runtime-0.0.0/src/reactor_cli/commands/init.py +86 -0
- reactor_runtime-0.0.0/src/reactor_cli/commands/run.py +118 -0
- reactor_runtime-0.0.0/src/reactor_cli/commands/setup.py +103 -0
- reactor_runtime-0.0.0/src/reactor_cli/commands/upload.py +104 -0
- reactor_runtime-0.0.0/src/reactor_cli/main.py +113 -0
- reactor_runtime-0.0.0/src/reactor_cli/utils.py +374 -0
- reactor_runtime-0.0.0/src/reactor_runtime/__init__.py +4 -0
- reactor_runtime-0.0.0/src/reactor_runtime/context/abstract_runtime.py +216 -0
- reactor_runtime-0.0.0/src/reactor_runtime/context/context.py +59 -0
- reactor_runtime-0.0.0/src/reactor_runtime/context/local/local_coordinator.py +247 -0
- reactor_runtime-0.0.0/src/reactor_runtime/context/local/local_runtime.py +208 -0
- reactor_runtime-0.0.0/src/reactor_runtime/context/local/utils.py +30 -0
- reactor_runtime-0.0.0/src/reactor_runtime/input/input_video.py +96 -0
- reactor_runtime-0.0.0/src/reactor_runtime/livekit/livekit.py +123 -0
- reactor_runtime-0.0.0/src/reactor_runtime/model_api.py +177 -0
- reactor_runtime-0.0.0/src/reactor_runtime/output/frame_buffer.py +116 -0
- reactor_runtime-0.0.0/src/reactor_runtime/output/streamer.py +32 -0
- reactor_runtime-0.0.0/src/reactor_runtime/output/video_streamer.py +172 -0
- reactor_runtime-0.0.0/src/reactor_runtime/utils/launch.py +102 -0
- reactor_runtime-0.0.0/src/reactor_runtime/utils/loader.py +87 -0
- reactor_runtime-0.0.0/src/reactor_runtime/utils/messages.py +49 -0
- reactor_runtime-0.0.0/src/reactor_runtime/utils/schema.py +24 -0
- reactor_runtime-0.0.0/src/reactor_runtime.egg-info/PKG-INFO +131 -0
- reactor_runtime-0.0.0/src/reactor_runtime.egg-info/SOURCES.txt +34 -0
- reactor_runtime-0.0.0/src/reactor_runtime.egg-info/dependency_links.txt +1 -0
- reactor_runtime-0.0.0/src/reactor_runtime.egg-info/entry_points.txt +2 -0
- reactor_runtime-0.0.0/src/reactor_runtime.egg-info/requires.txt +18 -0
- reactor_runtime-0.0.0/src/reactor_runtime.egg-info/top_level.txt +3 -0
- reactor_runtime-0.0.0/src/template/model_template.py +244 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reactor_runtime
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Reactor runtime with public model API
|
|
5
|
+
Author-email: Reactor <team@reactor.inc>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: websockets>=13.0
|
|
9
|
+
Requires-Dist: numpy<2.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
Requires-Dist: pyyaml
|
|
12
|
+
Requires-Dist: av>=12.0.0
|
|
13
|
+
Requires-Dist: aiortc>=1.10.0
|
|
14
|
+
Requires-Dist: fastapi>=0.100.0
|
|
15
|
+
Requires-Dist: uvicorn[standard]>=0.23.0
|
|
16
|
+
Requires-Dist: livekit-api>=1.0.5
|
|
17
|
+
Requires-Dist: livekit==1.0.12
|
|
18
|
+
Requires-Dist: aiohttp>=3.9.3
|
|
19
|
+
Requires-Dist: redis
|
|
20
|
+
Requires-Dist: supabase>=2.0.0
|
|
21
|
+
Requires-Dist: boto3>=1.28.0
|
|
22
|
+
Requires-Dist: opentelemetry-api
|
|
23
|
+
Requires-Dist: opentelemetry-sdk
|
|
24
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http
|
|
25
|
+
Requires-Dist: tqdm
|
|
26
|
+
|
|
27
|
+
# Reactor Runtime
|
|
28
|
+
|
|
29
|
+
A Python runtime for building real-time video processing models. This runtime abstracts all the techincal implementations of real-time networking, allowing
|
|
30
|
+
researchers and models developers to run their model focusing only on the ML code.
|
|
31
|
+
|
|
32
|
+
You can think of this similarly to the way you write Telegram/Discord applications or bots using SDKs. You don't have to worry about the networking and the protocols of the medium. Instead, you can put all your effort in writing your application code, which in this case is ML code.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install reactor-runtime
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## CLI Reference
|
|
41
|
+
|
|
42
|
+
The reactor-runtime library offers a set of commands that help you getting started with development.
|
|
43
|
+
|
|
44
|
+
### `reactor init <name>`
|
|
45
|
+
|
|
46
|
+
Initialize a new model workspace from a template.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
reactor init my-model
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Creates a directory `my-model/` with:
|
|
53
|
+
|
|
54
|
+
- `model_template.py` - Example VideoModel implementation
|
|
55
|
+
- `manifest.json` - Model configuration
|
|
56
|
+
- `requirements.txt` - Python dependencies
|
|
57
|
+
- `README.md` - Documentation Template
|
|
58
|
+
|
|
59
|
+
Once you have created your workspace, you're ready to start implementing your model.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### `reactor run`
|
|
64
|
+
|
|
65
|
+
Run your model with the Reactor runtime.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
reactor run [--host HOST] [--port PORT] [--log-level LEVEL]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Arguments:**
|
|
72
|
+
|
|
73
|
+
- `--host` - Server host (default: `0.0.0.0`)
|
|
74
|
+
- `--port` - Server port (default: `8081`)
|
|
75
|
+
- `--log-level` - Log level: `CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG` (default: `INFO`)
|
|
76
|
+
|
|
77
|
+
The `reactor run` command, will use the `manifest.json` file to infer the basic information and properties of your model.
|
|
78
|
+
Read the next section in order to understand what the `manifest.json` is, and how to customize it for your needs.
|
|
79
|
+
|
|
80
|
+
After running `reactor run`, you might encounter an error if you don't have the livekit local server installed. Follow
|
|
81
|
+
this link (which is also shown in the error) to install livekit on your OS: https://docs.livekit.io/home/self-hosting/local/
|
|
82
|
+
|
|
83
|
+
**Example:**
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
reactor run
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### `reactor capabilities`
|
|
92
|
+
|
|
93
|
+
Print the command capabilities of a VideoModel.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
reactor capabilities
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Outputs JSON schema of all available commands defined in your model (via `@command` decorators). When implementing your model, you will be able
|
|
100
|
+
to specify specific messages you want to "react" to, in real-time.
|
|
101
|
+
|
|
102
|
+
After these messages have been defined in your code, the runtime will generate a schema automatically, that clients will be able to use to infer what messages to send to the model.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Manifest Format
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"reactor-runtime": "0.0.0",
|
|
111
|
+
"model_name": "my-model",
|
|
112
|
+
"model_version": "1.0.0",
|
|
113
|
+
"class": "model_file:ModelClass",
|
|
114
|
+
"args": {
|
|
115
|
+
"fps": 30,
|
|
116
|
+
"size": [480, 640]
|
|
117
|
+
},
|
|
118
|
+
"weights": ["sam2_hiera_large", "dinov2_vitl14"],
|
|
119
|
+
"video_input": false
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The manifest is what defines your model. It defines the entrypoint, the version, the arguments, and so on. It is basically as the ID card of the model.
|
|
124
|
+
|
|
125
|
+
- reactor-runtime -> the version of the runtime used for development, so that it can be used for deployments and guarantee compatibility
|
|
126
|
+
- model_name -> the name of the model. This will identify the model on the Reactor ecosystem. (Multiple versions of the models should be specified using the version parameter.)
|
|
127
|
+
- model_version -> the version of the model.
|
|
128
|
+
- class -> Really important, it should be a pointer to the VideoModel class your model implements. For example, if you have implemented VideoModel in a file called `magic-model`, calling the model `MyMagicModel`, the value should be ``magic-model:MyMagicModel`
|
|
129
|
+
- args: the arguments with which the model will be started. You'll be able to access these arguments in the **init** call of the VideoModel, through the `kwargs`
|
|
130
|
+
- weights: not needed for local development (for now). Specify your weights as you normally would, when deploying the Reactor Team will manually optimize the weights path.
|
|
131
|
+
- video_input: if enabled, your model will be able to accept video input during a session. This means that whenever a user will stream their own video stream, you'll receive each frame call on a method called `on_frame`. You'll receive frames as numpy ndarrays.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Reactor Runtime
|
|
2
|
+
|
|
3
|
+
A Python runtime for building real-time video processing models. This runtime abstracts all the techincal implementations of real-time networking, allowing
|
|
4
|
+
researchers and models developers to run their model focusing only on the ML code.
|
|
5
|
+
|
|
6
|
+
You can think of this similarly to the way you write Telegram/Discord applications or bots using SDKs. You don't have to worry about the networking and the protocols of the medium. Instead, you can put all your effort in writing your application code, which in this case is ML code.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install reactor-runtime
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## CLI Reference
|
|
15
|
+
|
|
16
|
+
The reactor-runtime library offers a set of commands that help you getting started with development.
|
|
17
|
+
|
|
18
|
+
### `reactor init <name>`
|
|
19
|
+
|
|
20
|
+
Initialize a new model workspace from a template.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
reactor init my-model
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Creates a directory `my-model/` with:
|
|
27
|
+
|
|
28
|
+
- `model_template.py` - Example VideoModel implementation
|
|
29
|
+
- `manifest.json` - Model configuration
|
|
30
|
+
- `requirements.txt` - Python dependencies
|
|
31
|
+
- `README.md` - Documentation Template
|
|
32
|
+
|
|
33
|
+
Once you have created your workspace, you're ready to start implementing your model.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
### `reactor run`
|
|
38
|
+
|
|
39
|
+
Run your model with the Reactor runtime.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
reactor run [--host HOST] [--port PORT] [--log-level LEVEL]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Arguments:**
|
|
46
|
+
|
|
47
|
+
- `--host` - Server host (default: `0.0.0.0`)
|
|
48
|
+
- `--port` - Server port (default: `8081`)
|
|
49
|
+
- `--log-level` - Log level: `CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG` (default: `INFO`)
|
|
50
|
+
|
|
51
|
+
The `reactor run` command, will use the `manifest.json` file to infer the basic information and properties of your model.
|
|
52
|
+
Read the next section in order to understand what the `manifest.json` is, and how to customize it for your needs.
|
|
53
|
+
|
|
54
|
+
After running `reactor run`, you might encounter an error if you don't have the livekit local server installed. Follow
|
|
55
|
+
this link (which is also shown in the error) to install livekit on your OS: https://docs.livekit.io/home/self-hosting/local/
|
|
56
|
+
|
|
57
|
+
**Example:**
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
reactor run
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### `reactor capabilities`
|
|
66
|
+
|
|
67
|
+
Print the command capabilities of a VideoModel.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
reactor capabilities
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Outputs JSON schema of all available commands defined in your model (via `@command` decorators). When implementing your model, you will be able
|
|
74
|
+
to specify specific messages you want to "react" to, in real-time.
|
|
75
|
+
|
|
76
|
+
After these messages have been defined in your code, the runtime will generate a schema automatically, that clients will be able to use to infer what messages to send to the model.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Manifest Format
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"reactor-runtime": "0.0.0",
|
|
85
|
+
"model_name": "my-model",
|
|
86
|
+
"model_version": "1.0.0",
|
|
87
|
+
"class": "model_file:ModelClass",
|
|
88
|
+
"args": {
|
|
89
|
+
"fps": 30,
|
|
90
|
+
"size": [480, 640]
|
|
91
|
+
},
|
|
92
|
+
"weights": ["sam2_hiera_large", "dinov2_vitl14"],
|
|
93
|
+
"video_input": false
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The manifest is what defines your model. It defines the entrypoint, the version, the arguments, and so on. It is basically as the ID card of the model.
|
|
98
|
+
|
|
99
|
+
- reactor-runtime -> the version of the runtime used for development, so that it can be used for deployments and guarantee compatibility
|
|
100
|
+
- model_name -> the name of the model. This will identify the model on the Reactor ecosystem. (Multiple versions of the models should be specified using the version parameter.)
|
|
101
|
+
- model_version -> the version of the model.
|
|
102
|
+
- class -> Really important, it should be a pointer to the VideoModel class your model implements. For example, if you have implemented VideoModel in a file called `magic-model`, calling the model `MyMagicModel`, the value should be ``magic-model:MyMagicModel`
|
|
103
|
+
- args: the arguments with which the model will be started. You'll be able to access these arguments in the **init** call of the VideoModel, through the `kwargs`
|
|
104
|
+
- weights: not needed for local development (for now). Specify your weights as you normally would, when deploying the Reactor Team will manually optimize the weights path.
|
|
105
|
+
- video_input: if enabled, your model will be able to accept video input during a session. This means that whenever a user will stream their own video stream, you'll receive each frame call on a method called `on_frame`. You'll receive frames as numpy ndarrays.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "reactor_runtime"
|
|
7
|
+
version = "0.0.0"
|
|
8
|
+
description = "Reactor runtime with public model API"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Reactor", email = "team@reactor.inc" }
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
|
|
15
|
+
dependencies = [
|
|
16
|
+
"websockets>=13.0",
|
|
17
|
+
"numpy<2.0",
|
|
18
|
+
"pydantic>=2.0.0",
|
|
19
|
+
"pyyaml",
|
|
20
|
+
"av>=12.0.0",
|
|
21
|
+
"aiortc>=1.10.0",
|
|
22
|
+
"fastapi>=0.100.0",
|
|
23
|
+
"uvicorn[standard]>=0.23.0",
|
|
24
|
+
"livekit-api>=1.0.5",
|
|
25
|
+
"livekit==1.0.12",
|
|
26
|
+
"aiohttp>=3.9.3",
|
|
27
|
+
"redis",
|
|
28
|
+
"supabase>=2.0.0",
|
|
29
|
+
"boto3>=1.28.0",
|
|
30
|
+
"opentelemetry-api",
|
|
31
|
+
"opentelemetry-sdk",
|
|
32
|
+
"opentelemetry-exporter-otlp-proto-http",
|
|
33
|
+
"tqdm"
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
reactor = "reactor_cli.main:main"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Reactor CLI Commands
|
|
2
|
+
|
|
3
|
+
This module contains all command implementations for the reactor CLI.
|
|
4
|
+
Each command is implemented as a class following the HuggingFace pattern.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .run import RunCommand
|
|
8
|
+
from .init import InitCommand
|
|
9
|
+
from .download import DownloadCommand
|
|
10
|
+
from .upload import UploadCommand
|
|
11
|
+
from .setup import SetupCommand
|
|
12
|
+
from .capabilities import CapabilitiesCommand
|
|
13
|
+
|
|
14
|
+
__all__ = ['RunCommand', 'InitCommand', 'DownloadCommand', 'UploadCommand', 'SetupCommand', 'CapabilitiesCommand']
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Run command implementation."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import json
|
|
5
|
+
from reactor_runtime.model_api import VideoModel, command
|
|
6
|
+
import ast
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
def load_class_without_init(file_path: str, class_name: str):
|
|
10
|
+
"""
|
|
11
|
+
Load only the subclass definition from a Python file (without executing imports or __init__),
|
|
12
|
+
recreate it with CommandBase as its parent, and call its inherited commands() method.
|
|
13
|
+
All imports from the source file are detected via AST and replicated in the namespace.
|
|
14
|
+
"""
|
|
15
|
+
source = Path(file_path).read_text(encoding="utf-8")
|
|
16
|
+
tree = ast.parse(source, filename=file_path)
|
|
17
|
+
|
|
18
|
+
def extract_imports(tree):
|
|
19
|
+
"""Return a dict of {imported_name: module_object or attribute} from the AST."""
|
|
20
|
+
imports = {}
|
|
21
|
+
|
|
22
|
+
for node in tree.body:
|
|
23
|
+
if isinstance(node, ast.Import):
|
|
24
|
+
for alias in node.names:
|
|
25
|
+
mod_name = alias.name
|
|
26
|
+
as_name = alias.asname or mod_name.split(".")[0]
|
|
27
|
+
try:
|
|
28
|
+
imports[as_name] = importlib.import_module(mod_name)
|
|
29
|
+
except ImportError:
|
|
30
|
+
pass # Skip missing modules safely
|
|
31
|
+
|
|
32
|
+
elif isinstance(node, ast.ImportFrom):
|
|
33
|
+
if node.module is None:
|
|
34
|
+
continue
|
|
35
|
+
try:
|
|
36
|
+
mod = importlib.import_module(node.module)
|
|
37
|
+
except ImportError:
|
|
38
|
+
continue
|
|
39
|
+
for alias in node.names:
|
|
40
|
+
as_name = alias.asname or alias.name
|
|
41
|
+
if alias.name == "*":
|
|
42
|
+
# Handle "from X import *" by copying all public symbols
|
|
43
|
+
for name in dir(mod):
|
|
44
|
+
if not name.startswith("_"):
|
|
45
|
+
imports[name] = getattr(mod, name)
|
|
46
|
+
else:
|
|
47
|
+
try:
|
|
48
|
+
imports[as_name] = getattr(mod, alias.name)
|
|
49
|
+
except AttributeError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
return imports
|
|
53
|
+
|
|
54
|
+
# Detect and import all modules referenced in the source
|
|
55
|
+
imported_symbols = extract_imports(tree)
|
|
56
|
+
|
|
57
|
+
for node in tree.body:
|
|
58
|
+
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
59
|
+
# Replace base classes with CommandBase
|
|
60
|
+
node.bases = [ast.Name(id='CommandBase', ctx=ast.Load())]
|
|
61
|
+
ast.fix_missing_locations(node)
|
|
62
|
+
|
|
63
|
+
# Compile a module containing only that class
|
|
64
|
+
class_module = ast.Module(body=[node], type_ignores=[])
|
|
65
|
+
ast.fix_missing_locations(class_module)
|
|
66
|
+
code = compile(class_module, filename=file_path, mode="exec")
|
|
67
|
+
|
|
68
|
+
# Build isolated namespace
|
|
69
|
+
ns = {
|
|
70
|
+
"CommandBase": VideoModel,
|
|
71
|
+
"command": command,
|
|
72
|
+
**imported_symbols
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
exec(code, ns)
|
|
76
|
+
subcls = ns[class_name]
|
|
77
|
+
obj = subcls.__new__(subcls)
|
|
78
|
+
return obj
|
|
79
|
+
|
|
80
|
+
raise ValueError(f"Class '{class_name}' not found in {file_path}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CapabilitiesCommand:
|
|
84
|
+
@staticmethod
|
|
85
|
+
def register_subcommand(subparsers):
|
|
86
|
+
"""Register capabilities command"""
|
|
87
|
+
run_parser = subparsers.add_parser("capabilities", help="Print the capabilities of a reactor VideoModel.")
|
|
88
|
+
run_parser.set_defaults(func=CapabilitiesCommand)
|
|
89
|
+
|
|
90
|
+
def __init__(self, args):
|
|
91
|
+
"""Initialize with parsed arguments"""
|
|
92
|
+
self.args = args
|
|
93
|
+
|
|
94
|
+
def run(self):
|
|
95
|
+
"""Print the capabilities of a reactor VideoModel."""
|
|
96
|
+
from ..main import verify_reactor_workspace
|
|
97
|
+
|
|
98
|
+
# Verify workspace and get manifest data
|
|
99
|
+
manifest_data = verify_reactor_workspace()
|
|
100
|
+
if manifest_data is None:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Extract model information from manifest
|
|
104
|
+
model_class_name: str = manifest_data["class"]
|
|
105
|
+
model_file, model_class = model_class_name.split(":")
|
|
106
|
+
model_class: VideoModel = load_class_without_init(model_file+".py", model_class)
|
|
107
|
+
|
|
108
|
+
if "model_name" not in manifest_data.keys():
|
|
109
|
+
print("Error: manifest.json is missing required 'model_name' field.")
|
|
110
|
+
print("Please add a 'model_name' field specifying the model name.")
|
|
111
|
+
return
|
|
112
|
+
if "model_version" not in manifest_data.keys():
|
|
113
|
+
print("Error: manifest.json is missing required 'model_version' field.")
|
|
114
|
+
print("Please add a 'model_version' field specifying the model version.")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
print(json.dumps(model_class.commands(), indent=4))
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Download command implementation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from supabase import create_client
|
|
9
|
+
from ..utils import get_weights_parallel, get_latest_version
|
|
10
|
+
from ..main import verify_reactor_workspace
|
|
11
|
+
|
|
12
|
+
# Set up logger
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_manifest(manifest_str: str) -> dict:
|
|
17
|
+
"""Parse a manifest string into a dictionary.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
manifest_str: JSON string containing the manifest
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Parsed manifest dictionary
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
return json.loads(manifest_str)
|
|
27
|
+
except json.JSONDecodeError as e:
|
|
28
|
+
logger.error(f"Failed to parse manifest JSON: {e}")
|
|
29
|
+
raise ValueError(f"Invalid manifest JSON: {e}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_weights_from_manifest(manifest: dict) -> List[str]:
|
|
33
|
+
"""Extract the weights list from a manifest dictionary.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
manifest: Dictionary containing manifest data
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List of weight folder names
|
|
40
|
+
"""
|
|
41
|
+
if "weights" not in manifest:
|
|
42
|
+
logger.warning("Manifest does not contain a 'weights' field")
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
weights = manifest["weights"]
|
|
46
|
+
if not isinstance(weights, list):
|
|
47
|
+
logger.error(f"'weights' field must be a list, got {type(weights)}")
|
|
48
|
+
raise ValueError("'weights' field in manifest must be a list")
|
|
49
|
+
|
|
50
|
+
return weights
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_weights_from_local_manifest() -> List[str]:
|
|
54
|
+
"""Read weights from local manifest.json file.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of weight folder names from local manifest
|
|
58
|
+
"""
|
|
59
|
+
manifest = verify_reactor_workspace()
|
|
60
|
+
if not manifest:
|
|
61
|
+
raise RuntimeError("Failed to verify reactor workspace or read manifest.json")
|
|
62
|
+
|
|
63
|
+
return get_weights_from_manifest(manifest)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_weights_from_model_id(model_id: str) -> List[str]:
|
|
67
|
+
"""Fetch model manifest from Supabase and extract weights.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
model_id: Model identifier with optional version (e.g., "matrix-2" or "matrix-2@v1.0")
|
|
71
|
+
If no version is specified, defaults to "latest"
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of weight folder names from the model's manifest
|
|
75
|
+
"""
|
|
76
|
+
# Parse model_id and version from format: model@version or just model
|
|
77
|
+
if '@' in model_id:
|
|
78
|
+
model_name, version = model_id.split('@', 1)
|
|
79
|
+
else:
|
|
80
|
+
model_name = model_id
|
|
81
|
+
version = "latest"
|
|
82
|
+
|
|
83
|
+
supabase = create_client(
|
|
84
|
+
os.getenv('SUPABASE_URL'),
|
|
85
|
+
os.getenv('SUPABASE_SERVICE_KEY')
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# If version is "latest", we need to find the actual latest version
|
|
89
|
+
if version == "latest":
|
|
90
|
+
logger.info(f"Resolving latest version for model '{model_name}'...")
|
|
91
|
+
|
|
92
|
+
# Get the latest version using our utility function
|
|
93
|
+
try:
|
|
94
|
+
version = get_latest_version(model_name)
|
|
95
|
+
logger.info(f"Resolved latest version for '{model_name}': {version}")
|
|
96
|
+
except (ValueError, RuntimeError) as e:
|
|
97
|
+
raise ValueError(f"Failed to determine latest version for model '{model_name}': {e}")
|
|
98
|
+
|
|
99
|
+
logger.info(f"Fetching manifest for model '{model_name}' version '{version}'...")
|
|
100
|
+
|
|
101
|
+
result = supabase.table('models').select('manifest').eq(
|
|
102
|
+
'model_id', model_name
|
|
103
|
+
).eq('version', version).execute()
|
|
104
|
+
|
|
105
|
+
if not result.data:
|
|
106
|
+
raise ValueError(f"Model '{model_name}' version '{version}' not found in database")
|
|
107
|
+
|
|
108
|
+
manifest = result.data[0]['manifest']
|
|
109
|
+
|
|
110
|
+
if not manifest:
|
|
111
|
+
raise ValueError(f"Model '{model_name}' version '{version}' doesn't have a manifest in database.")
|
|
112
|
+
|
|
113
|
+
return get_weights_from_manifest(manifest)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_weights_from_model_ids(model_ids: List[str]) -> List[str]:
|
|
117
|
+
"""Fetch manifests for multiple models and extract unique weights.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
model_ids: List of model identifiers with optional versions
|
|
121
|
+
(e.g., ["matrix-2", "longlive@v1.0", "mk64"])
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Deduplicated list of weight folder names from all models
|
|
125
|
+
"""
|
|
126
|
+
all_weights = set()
|
|
127
|
+
|
|
128
|
+
for model_id in model_ids:
|
|
129
|
+
try:
|
|
130
|
+
weights = get_weights_from_model_id(model_id)
|
|
131
|
+
all_weights.update(weights)
|
|
132
|
+
logger.info(f"Model '{model_id}' requires weights: {weights}")
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"Failed to fetch weights for model '{model_id}': {e}")
|
|
135
|
+
raise
|
|
136
|
+
|
|
137
|
+
unique_weights = list(all_weights)
|
|
138
|
+
logger.info(f"Total unique weights across all models: {unique_weights}")
|
|
139
|
+
return unique_weights
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class DownloadCommand:
|
|
143
|
+
@staticmethod
|
|
144
|
+
def register_subcommand(subparsers):
|
|
145
|
+
"""Register download command"""
|
|
146
|
+
download_parser = subparsers.add_parser(
|
|
147
|
+
"download",
|
|
148
|
+
help="Download model weights"
|
|
149
|
+
)
|
|
150
|
+
download_parser.add_argument(
|
|
151
|
+
"--weights",
|
|
152
|
+
nargs='+',
|
|
153
|
+
help="List of weight folder names to download"
|
|
154
|
+
)
|
|
155
|
+
download_parser.add_argument(
|
|
156
|
+
"--models",
|
|
157
|
+
nargs='+',
|
|
158
|
+
help="List of model identifiers to fetch weights from (e.g., matrix-2 longlive@v1.0). Use model@version format to specify version, defaults to 'latest'"
|
|
159
|
+
)
|
|
160
|
+
download_parser.add_argument(
|
|
161
|
+
"--no-cache",
|
|
162
|
+
action="store_true",
|
|
163
|
+
help="Force re-download even if weights exist locally"
|
|
164
|
+
)
|
|
165
|
+
download_parser.set_defaults(func=DownloadCommand)
|
|
166
|
+
|
|
167
|
+
def __init__(self, args):
|
|
168
|
+
"""Initialize command with parsed arguments"""
|
|
169
|
+
self.args = args
|
|
170
|
+
|
|
171
|
+
def run(self):
|
|
172
|
+
"""Download model weights from registry"""
|
|
173
|
+
try:
|
|
174
|
+
# Determine which route to take based on arguments
|
|
175
|
+
weights_list: Optional[List[str]] = None
|
|
176
|
+
|
|
177
|
+
if self.args.weights:
|
|
178
|
+
# Route 1: Explicit weights list provided
|
|
179
|
+
weights_list = self.args.weights
|
|
180
|
+
logger.info(f"Downloading specified weights: {weights_list}")
|
|
181
|
+
|
|
182
|
+
elif self.args.models:
|
|
183
|
+
# Route 2: Fetch from model IDs in Supabase and deduplicate
|
|
184
|
+
print(f"Fetching manifests for {len(self.args.models)} model(s)...")
|
|
185
|
+
weights_list = get_weights_from_model_ids(self.args.models)
|
|
186
|
+
print(f"Found {len(weights_list)} unique weight(s) across all models")
|
|
187
|
+
logger.info(f"Downloaded manifests for models {self.args.models}, unique weights: {weights_list}")
|
|
188
|
+
|
|
189
|
+
else:
|
|
190
|
+
# Route 3: Default - read from local manifest.json
|
|
191
|
+
logger.info("No arguments specified, reading from local manifest.json...")
|
|
192
|
+
weights_list = get_weights_from_local_manifest()
|
|
193
|
+
logger.info(f"Found weights in local manifest: {weights_list}")
|
|
194
|
+
|
|
195
|
+
if not weights_list:
|
|
196
|
+
print("No weights to download.")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
# Download weights in parallel
|
|
200
|
+
if self.args.no_cache:
|
|
201
|
+
print(f"Force downloading {len(weights_list)} weight folder(s) (no-cache mode)...")
|
|
202
|
+
else:
|
|
203
|
+
print(f"Downloading {len(weights_list)} weight folder(s)...")
|
|
204
|
+
weight_paths = get_weights_parallel(weights_list, no_cache=self.args.no_cache)
|
|
205
|
+
|
|
206
|
+
# Print results
|
|
207
|
+
print("\nDownload complete! Weight paths:")
|
|
208
|
+
for i, (weight_name, weight_path) in enumerate(zip(weights_list, weight_paths), 1):
|
|
209
|
+
if weight_path:
|
|
210
|
+
print(f" {i}. {weight_name}: {weight_path}")
|
|
211
|
+
else:
|
|
212
|
+
print(f" {i}. {weight_name}: FAILED")
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.error(f"Download command failed: {e}")
|
|
216
|
+
print(f"Error: {e}")
|
|
217
|
+
return
|