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/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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any