frutebox-node 0.1.0__py3-none-any.whl
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.
- frutebox/adminheader.py +59 -0
- frutebox/attest.py +43 -0
- frutebox/audiogen.py +364 -0
- frutebox/config.py +367 -0
- frutebox/deltasync.py +211 -0
- frutebox/economics.py +270 -0
- frutebox/engine_guard.py +718 -0
- frutebox/envelope.py +77 -0
- frutebox/envvars.py +167 -0
- frutebox/gpuprices.py +641 -0
- frutebox/gpuproviders.py +4489 -0
- frutebox/gui.py +270 -0
- frutebox/imagegen.py +152 -0
- frutebox/inference.py +624 -0
- frutebox/manifest.py +218 -0
- frutebox/mediaserving.py +603 -0
- frutebox/packaging.py +542 -0
- frutebox/pqctransport.py +660 -0
- frutebox/provider.py +1867 -0
- frutebox/providers.py +942 -0
- frutebox/registry/__init__.py +189 -0
- frutebox/registry/generate.py +311 -0
- frutebox/registry/generate_providers.py +120 -0
- frutebox/registry/models.yaml +1759 -0
- frutebox/registry/providers.yaml +518 -0
- frutebox/registry/registry.gen.json +2043 -0
- frutebox/registry_gen.py +3 -0
- frutebox/runpod_templates.py +143 -0
- frutebox/serving.py +691 -0
- frutebox/signedconfig.py +259 -0
- frutebox/signing.py +208 -0
- frutebox/thinkbudget.py +155 -0
- frutebox/videogen.py +118 -0
- frutebox_node-0.1.0.dist-info/METADATA +100 -0
- frutebox_node-0.1.0.dist-info/RECORD +36 -0
- frutebox_node-0.1.0.dist-info/WHEEL +4 -0
frutebox/videogen.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Video generation WITH audio — runs on a GPU node. Text-to-video (diffusers) produces the
|
|
2
|
+
frames; an audio track (via `audiogen`) is muxed in with ffmpeg so the delivered MP4 carries
|
|
3
|
+
sound. Heavy libs are imported lazily and only present on a video-capable node, so
|
|
4
|
+
`available()` is False elsewhere and the gateway never routes video jobs here.
|
|
5
|
+
|
|
6
|
+
This is the heaviest modality (a video diffusion model + ffmpeg), which is why plan 23 §B2
|
|
7
|
+
keeps it async and off Tier-C entirely.
|
|
8
|
+
|
|
9
|
+
WHICH model runs comes from `mediaserving.resolve_media_model("video", ...)`: plan 23 §A's
|
|
10
|
+
primary is Wan 2.2, and the resolver returns the T2V (text-to-video) build, because that is
|
|
11
|
+
what this entry point is — the I2V/TI2V/S2V builds in the same org need an input image or audio
|
|
12
|
+
and would reject the request. The previous default, `THUDM/CogVideoX-2b`, was not declared in
|
|
13
|
+
`registry/models.yaml` at all, so it reached a GPU without passing any licence, territory or
|
|
14
|
+
tier gate; §A also flags CogVideoX-5B's licence as conditional.
|
|
15
|
+
|
|
16
|
+
`DiffusionPipeline`, not `CogVideoXPipeline`: the resolver may return Wan, Mochi or a later
|
|
17
|
+
primary and each has its own pipeline class. `from_pretrained` reads the repo's
|
|
18
|
+
`model_index.json` and instantiates the right one.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
import tempfile
|
|
26
|
+
|
|
27
|
+
from . import mediaserving
|
|
28
|
+
from .envvars import env as envvar
|
|
29
|
+
|
|
30
|
+
_FPS = int(envvar("VIDEO_FPS", "8"))
|
|
31
|
+
|
|
32
|
+
_pipe = None
|
|
33
|
+
_loaded_ref = ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def available() -> bool:
|
|
37
|
+
"""True iff this node can generate video (diffusers + torch + CUDA + ffmpeg + a model it
|
|
38
|
+
is allowed to run)."""
|
|
39
|
+
try:
|
|
40
|
+
import torch # type: ignore
|
|
41
|
+
import diffusers # type: ignore # noqa: F401
|
|
42
|
+
if not (torch.cuda.is_available() and shutil.which("ffmpeg")):
|
|
43
|
+
return False
|
|
44
|
+
except Exception:
|
|
45
|
+
return False
|
|
46
|
+
try:
|
|
47
|
+
resolved_model()
|
|
48
|
+
return True
|
|
49
|
+
except mediaserving.MediaUnavailable:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def resolved_model(*, node_region: str = "", user_region: str = ""):
|
|
54
|
+
"""The registry-declared, licence-gated video model this node will run, sized to its card."""
|
|
55
|
+
from .imagegen import device_vram_gb
|
|
56
|
+
return mediaserving.resolve_media_model(
|
|
57
|
+
"video",
|
|
58
|
+
node_region=node_region, user_region=user_region,
|
|
59
|
+
override=mediaserving.model_override("VIDEO_MODEL"),
|
|
60
|
+
max_vram_gb=device_vram_gb(),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _load(spec):
|
|
65
|
+
global _pipe, _loaded_ref
|
|
66
|
+
if _pipe is None or _loaded_ref != spec.ref:
|
|
67
|
+
import torch # type: ignore
|
|
68
|
+
from diffusers import DiffusionPipeline # type: ignore
|
|
69
|
+
_pipe = DiffusionPipeline.from_pretrained(
|
|
70
|
+
spec.repo, revision=spec.revision, torch_dtype=torch.float16).to("cuda")
|
|
71
|
+
_loaded_ref = spec.ref
|
|
72
|
+
return _pipe
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def generate_video(prompt: str, *, num_frames: int = 48, with_audio: bool = True,
|
|
76
|
+
node_region: str = "", user_region: str = "") -> bytes:
|
|
77
|
+
"""Generate an MP4 (bytes) from a text prompt, with a generated audio track muxed in.
|
|
78
|
+
Raises RuntimeError where the models/ffmpeg aren't available."""
|
|
79
|
+
if not prompt or not prompt.strip():
|
|
80
|
+
raise ValueError("empty video prompt")
|
|
81
|
+
try:
|
|
82
|
+
import torch # type: ignore
|
|
83
|
+
import diffusers # type: ignore # noqa: F401
|
|
84
|
+
if not torch.cuda.is_available():
|
|
85
|
+
raise RuntimeError("video generation is not available on this node: no CUDA GPU")
|
|
86
|
+
except ImportError as e:
|
|
87
|
+
raise RuntimeError(f"video generation is not available on this node: {e}") from e
|
|
88
|
+
if not shutil.which("ffmpeg"):
|
|
89
|
+
raise RuntimeError("video generation is not available on this node: ffmpeg is missing")
|
|
90
|
+
spec = resolved_model(node_region=node_region, user_region=user_region)
|
|
91
|
+
|
|
92
|
+
from diffusers.utils import export_to_video # type: ignore
|
|
93
|
+
frames = _load(spec)(prompt=prompt, num_frames=max(8, min(num_frames, 96))).frames[0]
|
|
94
|
+
|
|
95
|
+
with tempfile.TemporaryDirectory() as d:
|
|
96
|
+
silent = os.path.join(d, "v.mp4")
|
|
97
|
+
export_to_video(frames, silent, fps=_FPS)
|
|
98
|
+
if not with_audio:
|
|
99
|
+
with open(silent, "rb") as f:
|
|
100
|
+
return f.read()
|
|
101
|
+
# Generate a soundtrack and mux it in with ffmpeg (best-effort — a muxing failure
|
|
102
|
+
# still returns the silent clip rather than nothing).
|
|
103
|
+
try:
|
|
104
|
+
from . import audiogen
|
|
105
|
+
wav = os.path.join(d, "a.wav")
|
|
106
|
+
with open(wav, "wb") as f:
|
|
107
|
+
f.write(audiogen.generate_audio(prompt, kind="music"))
|
|
108
|
+
out = os.path.join(d, "out.mp4")
|
|
109
|
+
subprocess.run(
|
|
110
|
+
["ffmpeg", "-y", "-i", silent, "-i", wav, "-c:v", "copy", "-c:a", "aac",
|
|
111
|
+
"-shortest", out],
|
|
112
|
+
check=True, capture_output=True, timeout=120,
|
|
113
|
+
)
|
|
114
|
+
with open(out, "rb") as f:
|
|
115
|
+
return f.read()
|
|
116
|
+
except Exception:
|
|
117
|
+
with open(silent, "rb") as f:
|
|
118
|
+
return f.read()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: frutebox-node
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The Frutebox node runtime — share your hardware with the Frutebox network.
|
|
5
|
+
Project-URL: Homepage, https://frutebox.com
|
|
6
|
+
Project-URL: Documentation, https://frutebox.com/docs
|
|
7
|
+
Author: Frutebox
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: distributed,frutebox,gpu,inference,node
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Requires-Dist: cryptography>=42.0
|
|
18
|
+
Requires-Dist: dilithium-py==1.4.0
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Requires-Dist: pyyaml>=6.0
|
|
21
|
+
Requires-Dist: websockets>=12.0
|
|
22
|
+
Provides-Extra: cloud
|
|
23
|
+
Requires-Dist: runpod>=1.12.0; extra == 'cloud'
|
|
24
|
+
Provides-Extra: media
|
|
25
|
+
Requires-Dist: diffusers>=0.30; extra == 'media'
|
|
26
|
+
Requires-Dist: numpy>=1.24; extra == 'media'
|
|
27
|
+
Requires-Dist: pillow>=10.0; extra == 'media'
|
|
28
|
+
Requires-Dist: torch>=2.0; extra == 'media'
|
|
29
|
+
Provides-Extra: speech
|
|
30
|
+
Requires-Dist: chatterbox-tts; extra == 'speech'
|
|
31
|
+
Requires-Dist: kokoro; extra == 'speech'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# frutebox-node
|
|
35
|
+
|
|
36
|
+
The Frutebox **node runtime**: share your hardware with the Frutebox network and get paid for the
|
|
37
|
+
work it does.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install "frutebox[node]" # the SDK plus this runtime
|
|
41
|
+
pip install frutebox-node # just the runtime
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then, from the SDK's CLI:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pbpaste | frutebox login
|
|
48
|
+
frutebox share --join # register this machine and start the node agent
|
|
49
|
+
frutebox share --status # state and earnings
|
|
50
|
+
frutebox share --limits # when this machine will and will not take work
|
|
51
|
+
frutebox share --leave # stop taking work
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or run the agent directly:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -m frutebox.provider --region eu-west --credential-file ~/.config/frutebox/node-credential
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Pass the credential **by file, never `--credential`**: `ps` shows argv to every user on the machine,
|
|
61
|
+
and a non-zero exit serialises the whole command line into the exception.
|
|
62
|
+
|
|
63
|
+
## What it installs, and what it does not
|
|
64
|
+
|
|
65
|
+
This distribution ships 28 modules into the `frutebox` package — the transitive import closure of
|
|
66
|
+
`provider.py`, out of the backend package's 100. It is the serving node, not the gateway.
|
|
67
|
+
|
|
68
|
+
The install is **small on purpose**. Of the twelve third-party packages the runtime mentions, only
|
|
69
|
+
four are imported at module level (`cryptography`, `dilithium-py`, `httpx`, `websockets`, plus
|
|
70
|
+
`pyyaml` for the registry). Everything heavy is imported inside the function that needs it, so a
|
|
71
|
+
node that serves chat never downloads a deep-learning stack:
|
|
72
|
+
|
|
73
|
+
| extra | pulls in | needed for |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| *(none)* | the five above | chat inference, the gateway socket, attestation, earnings |
|
|
76
|
+
| `[media]` | torch, diffusers, numpy, pillow | serving image and video jobs |
|
|
77
|
+
| `[speech]` | chatterbox-tts, kokoro | serving speech jobs |
|
|
78
|
+
| `[cloud]` | runpod | renting capacity rather than offering your own machine |
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install "frutebox-node[media]"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## It shares the `frutebox` package with the SDK
|
|
85
|
+
|
|
86
|
+
Both distributions install into `frutebox/`, with disjoint filenames — the SDK owns `__init__.py`,
|
|
87
|
+
`_client.py`, `_errors.py`, `_spec.py` and `cli.py`; this one owns `provider.py`, `config.py` and
|
|
88
|
+
the rest. That is deliberate: two modules in the runtime use absolute self-imports
|
|
89
|
+
(`from frutebox.envvars import …`), so publishing them under a different package name would break
|
|
90
|
+
them.
|
|
91
|
+
|
|
92
|
+
One consequence worth knowing: `pip uninstall frutebox` removes the SDK's files including
|
|
93
|
+
`__init__.py`, leaving this runtime in place as a namespace package. `frutebox.provider` still
|
|
94
|
+
imports; `frutebox.Frutebox` does not. Reinstall the SDK, or uninstall both.
|
|
95
|
+
|
|
96
|
+
## The source is not duplicated
|
|
97
|
+
|
|
98
|
+
There is one copy of the node runtime in the repository. `pyproject.toml` here is a **manifest** over
|
|
99
|
+
`../frutebox/`, read directly at build time — not a fork of it. A test recomputes the import closure
|
|
100
|
+
from the source and fails if the manifest has drifted, so the two cannot diverge quietly.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
frutebox/adminheader.py,sha256=XCWSRkrXu2yGlfGEQwY-7BusVa_XgVJqijp5DBUecuU,2581
|
|
2
|
+
frutebox/attest.py,sha256=SnhhyKgO-5IdPWe28jQa-CLqerx1EY_GwX_J5vkz87I,1951
|
|
3
|
+
frutebox/audiogen.py,sha256=WyROazI59-ymSGylBG6ar4IpyXnnEUS-9_bj9GR1HRs,18571
|
|
4
|
+
frutebox/config.py,sha256=AxA6BWs658MCRqPF5cX1utFguXGnu-HcyIJevCp9PyQ,20940
|
|
5
|
+
frutebox/deltasync.py,sha256=XN0krc5FXkxNlWwK77H2TDSUHZy3BkD4ISAuI039R1A,9819
|
|
6
|
+
frutebox/economics.py,sha256=25GO6pwwYiQFibXvRm62IMnxutb5ls8Jq6-VWCEc3a8,12160
|
|
7
|
+
frutebox/engine_guard.py,sha256=eSclpbFFpagyCMaOSoeKV756RhIrfmVDfUWBYA1zSnc,33183
|
|
8
|
+
frutebox/envelope.py,sha256=_h03HtBitvb8Td-X0hECyp6ophGV2LQiBXfUSeICFsw,4153
|
|
9
|
+
frutebox/envvars.py,sha256=neNcSulrzlq55QX95TBkU9iXDpOMMpSpgHvRRkzyVqs,6679
|
|
10
|
+
frutebox/gpuprices.py,sha256=sakD4tn0B3-W-zDyLawRMdDLk0Bxe3sj86Klp3Mxg3c,31948
|
|
11
|
+
frutebox/gpuproviders.py,sha256=S5PDwuZrFY0Jq8LpgbMUlqtF88wJgkWZ2o68GkJN170,249528
|
|
12
|
+
frutebox/gui.py,sha256=Y6HAaYuW4F6g6FgM4Y_A5bsa-ftZPlRWKgmDN_0I_6s,14057
|
|
13
|
+
frutebox/imagegen.py,sha256=fqyp-CjCAPaAn7fIXnNTvRloYUOJNxw8ybLN_U7KdFw,7099
|
|
14
|
+
frutebox/inference.py,sha256=oOAJD7zr63_n6xvIzShqhRAdufZ9txgVEiVdiKGNZDA,31769
|
|
15
|
+
frutebox/manifest.py,sha256=tOTMIOyypoC2jwj_0TtKOm8xq3GoizeAz_NCqBoBfk8,11365
|
|
16
|
+
frutebox/mediaserving.py,sha256=uh2zTKo-2kacjXvJ0mjCopPGv39dqF1XuEgIY8uiLCc,34094
|
|
17
|
+
frutebox/packaging.py,sha256=7phMmrYMXo7LWWSLqrs8gxXP_YN-doxz0C21ZjS694o,28320
|
|
18
|
+
frutebox/pqctransport.py,sha256=Fnv5dUMmAbcgLO1eWL_boNz82nFPxacXFSO8-ZUwpHk,33820
|
|
19
|
+
frutebox/provider.py,sha256=dUlBbxj6-MIkNmTRCB2BxY-DUIf-PyR2OGWMC8Akesk,108861
|
|
20
|
+
frutebox/providers.py,sha256=ecVpylVem6L-bDt1fWSwb78JdkGaWscoq3J4ZykqX3A,50434
|
|
21
|
+
frutebox/registry/__init__.py,sha256=C141B2yNQF4LNpwYSPhE3taKebmphx92yLKGOQOodW4,8772
|
|
22
|
+
frutebox/registry/generate.py,sha256=DGz4mR4yZ6cRHhNFOzQ-Q34VHCFXl7Ulgdvtvv4EGXk,17309
|
|
23
|
+
frutebox/registry/generate_providers.py,sha256=Xj69i00rT8XfgXcv1DzgzAiXeC6OuVb2RkHs7UljAOk,6081
|
|
24
|
+
frutebox/registry/models.yaml,sha256=2dam8YRRRtFmHDtkhYQLrkbcOyNkgCNWyMBJClJEYTk,53317
|
|
25
|
+
frutebox/registry/providers.yaml,sha256=rb_qSYbDRE7CswaISZyvjZIOqdI89bAC92koxIbnWAQ,33332
|
|
26
|
+
frutebox/registry/registry.gen.json,sha256=pZNwzWwtA5Di4qc9XjJ4Lfe74bbCdDaGPUCJz0ZYuPs,56662
|
|
27
|
+
frutebox/registry_gen.py,sha256=8GC1qpJwoyCXcEXATEOoEwxLAfR2QQXvl6cm8a7FMFU,47987
|
|
28
|
+
frutebox/runpod_templates.py,sha256=wVz0uc4MFZ7XPsecmD4-cVK1uVB8ciFP39X7R_UrHMs,6873
|
|
29
|
+
frutebox/serving.py,sha256=rIt61rtcozdf_cDodQhxtoZYzvmif_5gZGzAtFUWVW8,44485
|
|
30
|
+
frutebox/signedconfig.py,sha256=ScpCPZIdh3eblfHYaaXVW32lUZ0B_mQ3n2huFPnBY9k,12498
|
|
31
|
+
frutebox/signing.py,sha256=PJz82ZFPPkvSvEXBTP1fmYdDyNdNKhNCZBeneuWs59U,8589
|
|
32
|
+
frutebox/thinkbudget.py,sha256=vWbVQ2D789LOsZzmxHarHo-uUD-jI2j_YAbr9rWFHj4,8668
|
|
33
|
+
frutebox/videogen.py,sha256=jp89Ut6Ti1Af1CNxnJgRqJKc44mP-GLnJPXa9cIa5NU,4956
|
|
34
|
+
frutebox_node-0.1.0.dist-info/METADATA,sha256=rK-iRunY-8BOa6WPV9bf-5xviaQolkNMR6Ovf_RyanY,4182
|
|
35
|
+
frutebox_node-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
36
|
+
frutebox_node-0.1.0.dist-info/RECORD,,
|