inline-core 1.2.31__tar.gz → 1.2.41__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.
- {inline_core-1.2.31 → inline_core-1.2.41}/PKG-INFO +9 -1
- {inline_core-1.2.31 → inline_core-1.2.41}/pyproject.toml +15 -1
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/server/app.py +9 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/server/rpc.py +5 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/handlers.py +35 -2
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/moodboard.py +106 -23
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/schema.py +59 -3
- inline_core-1.2.41/src/inline_core/studio/system_stats.py +92 -0
- inline_core-1.2.41/src/inline_core/studio/training.py +458 -0
- inline_core-1.2.41/src/inline_core/studio/training_store.py +268 -0
- inline_core-1.2.41/src/inline_core/training/__init__.py +10 -0
- inline_core-1.2.41/src/inline_core/training/__main__.py +67 -0
- inline_core-1.2.41/src/inline_core/training/caption.py +127 -0
- inline_core-1.2.41/src/inline_core/training/dataset.py +100 -0
- inline_core-1.2.41/src/inline_core/training/models.py +125 -0
- inline_core-1.2.41/src/inline_core/training/protocol.py +49 -0
- inline_core-1.2.41/src/inline_core/training/trainer.py +236 -0
- inline_core-1.2.41/tests/test_studio_training.py +81 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/uv.lock +171 -45
- {inline_core-1.2.31 → inline_core-1.2.41}/webui.sh +1 -1
- {inline_core-1.2.31 → inline_core-1.2.41}/.gitignore +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/.python-version +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/CLAUDE.md +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/README.md +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/main.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/scripts/reference.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/components/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/components/conditioning.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/components/interfaces.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/config.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/device/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/device/auto.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/device/detect.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/device/memory.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/device/policy.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/device/types.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/errors.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/api.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/constraints.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/fetch.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/handlers.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/importer.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/install.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/loader.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/manifest.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/models.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/paths.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/resolve.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/scanner.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/state.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/extensions/tools.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/ffmpeg.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/graph/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/graph/cache.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/graph/descriptor.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/graph/executor.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/graph/loader_runners.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/graph/primitives.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/graph/registry.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/graph/runners.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/graph/schema.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/graph/topo.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/graph/validate.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/media.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/models/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/models/catalog.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/models/loaders.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/models/lora.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/models/requirements.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/models/sampling.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/models/zimage/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/models/zimage/primitives.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/models/zimage/provider.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/models/zimage/requirements.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/models/zimage/runner.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/parallel/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/parallel/config.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/parallel/group.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/parallel/launch.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/parallel/protocol.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/parallel/registry.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/parallel/worker.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/runtime/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/runtime/context.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/runtime/file_store.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/runtime/progress.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/runtime/run.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/runtime/store.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/sampling/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/sampling/batch.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/server/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/server/__main__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/server/assets.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/server/bootstrap.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/server/frontend.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/server/manager.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/server/run_store.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/server/serialize.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/assets.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/config.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/fal.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/frames.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/generation.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/graph_build.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/models.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/peaks.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/store.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/timeline/__init__.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/timeline/compose.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/timeline/ffmpeg.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/timeline/render.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/studio/timeline/resolve.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/src/inline_core/takes.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/helpers.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_cache.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_catalog.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_config.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_device_detect.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_executor.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_extension_api.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_extension_install.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_extension_manifest.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_extension_resolve.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_extension_scanner.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_extension_spine.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_extension_state.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_file_store.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_frontend_serving.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_hidden_nodes.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_loader_runners.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_loaders.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_lora.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_memory_policy.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_model_requirements.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_parallel_group.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_primitives.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_rpc_bridge.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_run_store.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_sampling.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_schema.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_server.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_studio_assets.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_studio_fal.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_studio_frames.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_studio_generation.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_studio_models.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_studio_moodboard.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_studio_peaks.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_studio_rpc.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_studio_schema.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_studio_store.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_studio_timeline.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_take_bytes.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_topo.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_validate.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_xfuser_sampler.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_zimage_primitives.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_zimage_resolve.py +0 -0
- {inline_core-1.2.31 → inline_core-1.2.41}/tests/test_zimage_runner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: inline-core
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.41
|
|
4
4
|
Summary: The generation engine behind Inline Studio.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: numpy>=1.26
|
|
@@ -38,6 +38,14 @@ Provides-Extra: server
|
|
|
38
38
|
Requires-Dist: fastapi>=0.110; extra == 'server'
|
|
39
39
|
Requires-Dist: imageio-ffmpeg>=0.4; extra == 'server'
|
|
40
40
|
Requires-Dist: uvicorn[standard]>=0.29; extra == 'server'
|
|
41
|
+
Provides-Extra: training
|
|
42
|
+
Requires-Dist: bitsandbytes>=0.43; (platform_system != 'Darwin') and extra == 'training'
|
|
43
|
+
Requires-Dist: einops>=0.7; extra == 'training'
|
|
44
|
+
Requires-Dist: nvidia-ml-py>=12; extra == 'training'
|
|
45
|
+
Requires-Dist: peft>=0.11; extra == 'training'
|
|
46
|
+
Requires-Dist: pillow>=10; extra == 'training'
|
|
47
|
+
Requires-Dist: psutil>=5.9; extra == 'training'
|
|
48
|
+
Requires-Dist: timm>=1.0; extra == 'training'
|
|
41
49
|
Description-Content-Type: text/markdown
|
|
42
50
|
|
|
43
51
|
# Inline Core
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
# PyPI name; the import package is `inline_core` (src/inline_core).
|
|
3
3
|
name = "inline-core"
|
|
4
|
-
version = "1.2.
|
|
4
|
+
version = "1.2.41"
|
|
5
5
|
description = "The generation engine behind Inline Studio."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
requires-python = ">=3.11"
|
|
@@ -39,6 +39,20 @@ parallel = [
|
|
|
39
39
|
"xfuser>=0.4",
|
|
40
40
|
"nvidia-ml-py>=12",
|
|
41
41
|
]
|
|
42
|
+
# LoRA training (the Trainer tab): PEFT adapter training + local auto-caption + host/GPU telemetry.
|
|
43
|
+
# Install alongside `runtime`: `.[runtime,training]`. Reuses runtime's torch/diffusers/accelerate.
|
|
44
|
+
training = [
|
|
45
|
+
"peft>=0.11",
|
|
46
|
+
# 8-bit Adam keeps optimizer state small; no macOS wheels, so skip it there (AdamW fallback).
|
|
47
|
+
"bitsandbytes>=0.43; platform_system != 'Darwin'",
|
|
48
|
+
"Pillow>=10",
|
|
49
|
+
# Telemetry (CPU/RAM + per-GPU NVML) for the Trainer tab.
|
|
50
|
+
"psutil>=5.9",
|
|
51
|
+
"nvidia-ml-py>=12",
|
|
52
|
+
# Florence-2 auto-captioner is loaded via trust_remote_code and needs these.
|
|
53
|
+
"timm>=1.0",
|
|
54
|
+
"einops>=0.7",
|
|
55
|
+
]
|
|
42
56
|
dev = [
|
|
43
57
|
"pytest>=8",
|
|
44
58
|
"ruff>=0.6",
|
|
@@ -29,6 +29,7 @@ from ..graph.schema import SCHEMA_VERSION, parse_graph
|
|
|
29
29
|
from ..models.catalog import ModelCatalog
|
|
30
30
|
from ..models.requirements import RequirementsRegistry
|
|
31
31
|
from ..runtime.file_store import FileTakeStore
|
|
32
|
+
from ..studio.system_stats import SystemStats
|
|
32
33
|
from .assets import AssetStore
|
|
33
34
|
from .manager import RunConflict, RunManager
|
|
34
35
|
from .rpc import EventBroadcaster, RpcRouter
|
|
@@ -139,13 +140,19 @@ def create_app(
|
|
|
139
140
|
manager = RunManager(registry, cache, policy, store=run_store, takes=FileTakeStore(takes_root))
|
|
140
141
|
rpc = rpc or RpcRouter()
|
|
141
142
|
events = events or EventBroadcaster()
|
|
143
|
+
# Host/GPU telemetry for the Trainer tab; only meaningful with the SPA (studio) backend wired.
|
|
144
|
+
stats = SystemStats(events) if studio_store is not None else None
|
|
142
145
|
|
|
143
146
|
@asynccontextmanager
|
|
144
147
|
async def lifespan(app: FastAPI): # noqa: ANN202
|
|
145
148
|
manager.bind_loop(asyncio.get_running_loop())
|
|
146
149
|
catalog.ensure_dirs()
|
|
147
150
|
catalog.scan()
|
|
151
|
+
if stats is not None:
|
|
152
|
+
stats.start()
|
|
148
153
|
yield
|
|
154
|
+
if stats is not None:
|
|
155
|
+
stats.stop()
|
|
149
156
|
manager.shutdown()
|
|
150
157
|
|
|
151
158
|
app = FastAPI(title="Inline Core", version="0.0.0", lifespan=lifespan)
|
|
@@ -298,6 +305,7 @@ def create_app(
|
|
|
298
305
|
from ..studio.handlers import register_studio_handlers
|
|
299
306
|
from ..studio.models import ModelDownloads
|
|
300
307
|
from ..studio.timeline.render import Timeline
|
|
308
|
+
from ..studio.training import Training
|
|
301
309
|
|
|
302
310
|
def core_models() -> dict[str, Any]:
|
|
303
311
|
return {
|
|
@@ -316,6 +324,7 @@ def create_app(
|
|
|
316
324
|
generation=CoreGeneration(studio_store, manager, events),
|
|
317
325
|
fal_generation=FalGeneration(studio_store, events),
|
|
318
326
|
timeline=Timeline(studio_store, events),
|
|
327
|
+
training=Training(studio_store, events, on_output=catalog.rescan),
|
|
319
328
|
# Explicit model downloads write into models/; rescan so new files bump the registry.
|
|
320
329
|
# The policy lets the requirements popup show a memory fit estimate before a load;
|
|
321
330
|
# the requirements registry says which node types have models at all.
|
|
@@ -56,6 +56,11 @@ class EventBroadcaster:
|
|
|
56
56
|
def __init__(self) -> None:
|
|
57
57
|
self._subscribers: set[asyncio.Queue[dict[str, Any]]] = set()
|
|
58
58
|
|
|
59
|
+
@property
|
|
60
|
+
def subscriber_count(self) -> int:
|
|
61
|
+
"""Open ``/events`` sockets - lets pollers (telemetry) skip work when nobody listens."""
|
|
62
|
+
return len(self._subscribers)
|
|
63
|
+
|
|
59
64
|
def add(self) -> asyncio.Queue[dict[str, Any]]:
|
|
60
65
|
queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
|
|
61
66
|
self._subscribers.add(queue)
|
|
@@ -39,6 +39,7 @@ def register_studio_handlers(
|
|
|
39
39
|
generation: Any = None,
|
|
40
40
|
fal_generation: Any = None,
|
|
41
41
|
timeline: Any = None,
|
|
42
|
+
training: Any = None,
|
|
42
43
|
model_downloads: Any = None,
|
|
43
44
|
app_version: str = "1.0.0",
|
|
44
45
|
) -> None:
|
|
@@ -155,7 +156,9 @@ def register_studio_handlers(
|
|
|
155
156
|
reg("frames:deleteTake", delete_take)
|
|
156
157
|
|
|
157
158
|
# --- moodboard ------------------------------------------------------------------------------
|
|
158
|
-
|
|
159
|
+
# `surface` defaults to the Studio moodboard so existing callers are unchanged; the Trainer tab
|
|
160
|
+
# passes "trainer" to get its own isolated canvas out of the same tables.
|
|
161
|
+
reg("moodboard:list", lambda surface=mb.STUDIO_SURFACE: mb.list_board(conn(), surface))
|
|
159
162
|
reg("moodboard:addAsset", lambda aid, x, y: mb.add_asset(conn(), aid, x, y))
|
|
160
163
|
reg("moodboard:addText", lambda x, y: mb.add_text(conn(), x, y))
|
|
161
164
|
reg("moodboard:addFrameFromAsset", lambda aid, x, y: mb.add_frame_from_asset(conn(), aid, x, y))
|
|
@@ -183,7 +186,18 @@ def register_studio_handlers(
|
|
|
183
186
|
reg("moodboard:setConnectorVolume", lambda cid, vol: mb.set_connector_volume(conn(), cid, vol))
|
|
184
187
|
reg(
|
|
185
188
|
"moodboard:replaceBoard",
|
|
186
|
-
lambda items, connectors: mb.replace_board(
|
|
189
|
+
lambda items, connectors, surface=mb.STUDIO_SURFACE: mb.replace_board(
|
|
190
|
+
conn(), items, connectors, surface
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
# Trainer-canvas nodes (plus the shared read-only resource node, which either canvas can host).
|
|
194
|
+
reg("moodboard:addTrainDataset", lambda x, y: mb.add_train_dataset(conn(), x, y))
|
|
195
|
+
reg("moodboard:addCaption", lambda x, y: mb.add_caption(conn(), x, y))
|
|
196
|
+
reg("moodboard:addTrainer", lambda x, y: mb.add_trainer(conn(), x, y))
|
|
197
|
+
reg("moodboard:addLossGraph", lambda x, y: mb.add_loss_graph(conn(), x, y))
|
|
198
|
+
reg(
|
|
199
|
+
"moodboard:addResource",
|
|
200
|
+
lambda x, y, surface=mb.STUDIO_SURFACE: mb.add_resource(conn(), x, y, surface),
|
|
187
201
|
)
|
|
188
202
|
|
|
189
203
|
# --- generation -----------------------------------------------------------------------------
|
|
@@ -207,6 +221,25 @@ def register_studio_handlers(
|
|
|
207
221
|
reg("generation:cancel", cancel_generation)
|
|
208
222
|
reg("generation:resumePending", lambda: None)
|
|
209
223
|
|
|
224
|
+
# --- LoRA training (dataset CRUD + the training run subprocess) ------------------------------
|
|
225
|
+
if training is not None:
|
|
226
|
+
reg("training:listDatasets", lambda: training.list_datasets())
|
|
227
|
+
reg("training:createDataset", lambda inp: training.create_dataset(inp))
|
|
228
|
+
reg("training:listItems", lambda did: training.list_items(did))
|
|
229
|
+
reg("training:addItems", lambda did, aids: training.add_items(did, aids))
|
|
230
|
+
reg("training:removeItem", lambda iid: training.remove_item(iid))
|
|
231
|
+
reg("training:setCaption", lambda iid, cap: training.set_caption(iid, cap))
|
|
232
|
+
reg("training:autoCaption", lambda did, overwrite: training.auto_caption(did, overwrite))
|
|
233
|
+
reg("training:listRuns", lambda: training.list_runs())
|
|
234
|
+
reg("training:start", lambda did, hp: training.start(did, hp))
|
|
235
|
+
reg("training:resume", lambda rid: training.resume(rid))
|
|
236
|
+
reg("training:cancel", lambda rid: training.cancel(rid))
|
|
237
|
+
reg("training:status", lambda rid: training.status(rid))
|
|
238
|
+
else:
|
|
239
|
+
for ch in ("listDatasets", "createDataset", "listItems", "addItems", "removeItem",
|
|
240
|
+
"setCaption", "autoCaption", "listRuns", "start", "resume", "cancel", "status"):
|
|
241
|
+
reg(f"training:{ch}", not_wired("LoRA training"))
|
|
242
|
+
|
|
210
243
|
# --- fal settings (key stored server-side) --------------------------------------------------
|
|
211
244
|
reg("falSettings:status", store.fal_status)
|
|
212
245
|
reg("falSettings:setApiKey", store.set_fal_key)
|
|
@@ -17,10 +17,13 @@ from typing import Any
|
|
|
17
17
|
from . import frames as fr
|
|
18
18
|
|
|
19
19
|
_ITEM_COLUMNS = (
|
|
20
|
-
"id, project_id, type, asset_id, frame_id, parent_id, data, x, y, width, height,
|
|
21
|
-
"z_index, created_at, updated_at"
|
|
20
|
+
"id, project_id, surface, type, asset_id, frame_id, parent_id, data, x, y, width, height, "
|
|
21
|
+
"rotation, z_index, created_at, updated_at"
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
+
#: The Studio moodboard; the Trainer tab's graph is the other surface (see schema `surface`).
|
|
25
|
+
STUDIO_SURFACE = "studio"
|
|
26
|
+
|
|
24
27
|
_DEFAULT_SIZE = {"image": (320, 180), "video": (360, 203), "audio": (320, 80)}
|
|
25
28
|
|
|
26
29
|
_DEFAULT_TEXT = {
|
|
@@ -63,6 +66,7 @@ def _row_to_item(row: sqlite3.Row) -> dict[str, Any]:
|
|
|
63
66
|
return {
|
|
64
67
|
"id": row["id"],
|
|
65
68
|
"projectId": row["project_id"],
|
|
69
|
+
"surface": row["surface"],
|
|
66
70
|
"type": row["type"],
|
|
67
71
|
"assetId": row["asset_id"],
|
|
68
72
|
"frameId": row["frame_id"],
|
|
@@ -83,6 +87,7 @@ def _row_to_connector(row: sqlite3.Row) -> dict[str, Any]:
|
|
|
83
87
|
return {
|
|
84
88
|
"id": row["id"],
|
|
85
89
|
"projectId": row["project_id"],
|
|
90
|
+
"surface": row["surface"],
|
|
86
91
|
"fromItemId": row["from_item_id"],
|
|
87
92
|
"toItemId": row["to_item_id"],
|
|
88
93
|
"label": row["label"],
|
|
@@ -103,16 +108,28 @@ def list_items(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
|
|
103
108
|
return [_row_to_item(r) for r in rows]
|
|
104
109
|
|
|
105
110
|
|
|
106
|
-
def list_board(conn: sqlite3.Connection) -> dict[str, Any]:
|
|
107
|
-
items
|
|
111
|
+
def list_board(conn: sqlite3.Connection, surface: str = STUDIO_SURFACE) -> dict[str, Any]:
|
|
112
|
+
"""One canvas's items + connectors. Surfaces are isolated: the Trainer graph never leaks into
|
|
113
|
+
the Studio moodboard (and vice versa)."""
|
|
114
|
+
items = [
|
|
115
|
+
_row_to_item(r)
|
|
116
|
+
for r in conn.execute(
|
|
117
|
+
"SELECT * FROM moodboard_items WHERE surface = ?", (surface,)
|
|
118
|
+
).fetchall()
|
|
119
|
+
]
|
|
108
120
|
connectors = [
|
|
109
|
-
_row_to_connector(r)
|
|
121
|
+
_row_to_connector(r)
|
|
122
|
+
for r in conn.execute(
|
|
123
|
+
"SELECT * FROM moodboard_connectors WHERE surface = ?", (surface,)
|
|
124
|
+
).fetchall()
|
|
110
125
|
]
|
|
111
126
|
return {"items": items, "connectors": connectors}
|
|
112
127
|
|
|
113
128
|
|
|
114
|
-
def _next_z(conn: sqlite3.Connection) -> int:
|
|
115
|
-
row = conn.execute(
|
|
129
|
+
def _next_z(conn: sqlite3.Connection, surface: str = STUDIO_SURFACE) -> int:
|
|
130
|
+
row = conn.execute(
|
|
131
|
+
"SELECT MAX(z_index) AS z FROM moodboard_items WHERE surface = ?", (surface,)
|
|
132
|
+
).fetchone()
|
|
116
133
|
return (row["z"] or 0) + 1
|
|
117
134
|
|
|
118
135
|
|
|
@@ -128,11 +145,13 @@ def _insert_item(
|
|
|
128
145
|
asset_id: str | None = None,
|
|
129
146
|
frame_id: str | None = None,
|
|
130
147
|
z_index: int | None = None,
|
|
148
|
+
surface: str = STUDIO_SURFACE,
|
|
131
149
|
) -> dict[str, Any]:
|
|
132
150
|
now = _now()
|
|
133
151
|
item = {
|
|
134
152
|
"id": _uuid(),
|
|
135
153
|
"project_id": _project_id(conn),
|
|
154
|
+
"surface": surface,
|
|
136
155
|
"type": item_type,
|
|
137
156
|
"asset_id": asset_id,
|
|
138
157
|
"frame_id": frame_id,
|
|
@@ -143,14 +162,14 @@ def _insert_item(
|
|
|
143
162
|
"width": width,
|
|
144
163
|
"height": height,
|
|
145
164
|
"rotation": 0,
|
|
146
|
-
"z_index": z_index if z_index is not None else _next_z(conn),
|
|
165
|
+
"z_index": z_index if z_index is not None else _next_z(conn, surface),
|
|
147
166
|
"created_at": now,
|
|
148
167
|
"updated_at": now,
|
|
149
168
|
}
|
|
150
169
|
conn.execute(
|
|
151
170
|
f"INSERT INTO moodboard_items ({_ITEM_COLUMNS}) VALUES "
|
|
152
|
-
"(:id, :project_id, :type, :asset_id, :frame_id, :parent_id, :data, :x, :y,
|
|
153
|
-
":height, :rotation, :z_index, :created_at, :updated_at)",
|
|
171
|
+
"(:id, :project_id, :surface, :type, :asset_id, :frame_id, :parent_id, :data, :x, :y, "
|
|
172
|
+
":width, :height, :rotation, :z_index, :created_at, :updated_at)",
|
|
154
173
|
item,
|
|
155
174
|
)
|
|
156
175
|
return get_item(conn, item["id"])
|
|
@@ -209,6 +228,57 @@ def add_gen_node(
|
|
|
209
228
|
)
|
|
210
229
|
|
|
211
230
|
|
|
231
|
+
#: Trainer-canvas nodes. They only ever exist on the `trainer` surface (the Trainer tab's graph);
|
|
232
|
+
#: `resource` is the exception - a read-only telemetry node usable on either canvas.
|
|
233
|
+
TRAINER_SURFACE = "trainer"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def add_train_dataset(
|
|
237
|
+
conn: sqlite3.Connection, x: float, y: float, surface: str = TRAINER_SURFACE
|
|
238
|
+
) -> dict[str, Any]:
|
|
239
|
+
return _insert_item(
|
|
240
|
+
conn, item_type="trainDataset", x=x, y=y, width=280, height=240,
|
|
241
|
+
data={"datasetId": None}, surface=surface,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def add_caption(
|
|
246
|
+
conn: sqlite3.Connection, x: float, y: float, surface: str = TRAINER_SURFACE
|
|
247
|
+
) -> dict[str, Any]:
|
|
248
|
+
return _insert_item(
|
|
249
|
+
conn, item_type="caption", x=x, y=y, width=280, height=200,
|
|
250
|
+
data={"datasetId": None, "overwrite": False}, surface=surface,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def add_trainer(
|
|
255
|
+
conn: sqlite3.Connection, x: float, y: float, surface: str = TRAINER_SURFACE
|
|
256
|
+
) -> dict[str, Any]:
|
|
257
|
+
return _insert_item(
|
|
258
|
+
# Roomier than the other nodes on purpose: the body is a live log tail.
|
|
259
|
+
conn, item_type="trainer", x=x, y=y, width=420, height=340,
|
|
260
|
+
# `runId` is persisted so the node rebinds to its run after a reload and can still Resume.
|
|
261
|
+
data={"datasetId": None, "runId": None, "hyperparams": {}}, surface=surface,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def add_loss_graph(
|
|
266
|
+
conn: sqlite3.Connection, x: float, y: float, surface: str = TRAINER_SURFACE
|
|
267
|
+
) -> dict[str, Any]:
|
|
268
|
+
return _insert_item(
|
|
269
|
+
conn, item_type="lossGraph", x=x, y=y, width=320, height=220,
|
|
270
|
+
data={"runId": None}, surface=surface,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def add_resource(
|
|
275
|
+
conn: sqlite3.Connection, x: float, y: float, surface: str = STUDIO_SURFACE
|
|
276
|
+
) -> dict[str, Any]:
|
|
277
|
+
return _insert_item(
|
|
278
|
+
conn, item_type="resource", x=x, y=y, width=280, height=170, data={}, surface=surface
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
212
282
|
def add_layer(conn: sqlite3.Connection, x: float, y: float) -> dict[str, Any]:
|
|
213
283
|
return _insert_item(
|
|
214
284
|
conn, item_type="layer", x=x, y=y, width=420, height=300, data={"name": "Layer"}, z_index=0
|
|
@@ -306,20 +376,29 @@ def delete_item(conn: sqlite3.Connection, item_id: str) -> None:
|
|
|
306
376
|
|
|
307
377
|
|
|
308
378
|
def replace_board(
|
|
309
|
-
conn: sqlite3.Connection,
|
|
379
|
+
conn: sqlite3.Connection,
|
|
380
|
+
items: list[dict[str, Any]],
|
|
381
|
+
connectors: list[dict[str, Any]],
|
|
382
|
+
surface: str = STUDIO_SURFACE,
|
|
310
383
|
) -> None:
|
|
311
|
-
"""Replace
|
|
384
|
+
"""Replace ONE surface's board (undo/redo restore), preserving ids, in one transaction. Scoped
|
|
385
|
+
by surface so a Studio undo never wipes the Trainer canvas (they share the table)."""
|
|
312
386
|
pid = _project_id(conn)
|
|
313
|
-
conn.execute(
|
|
314
|
-
|
|
387
|
+
conn.execute(
|
|
388
|
+
"DELETE FROM moodboard_connectors WHERE project_id = ? AND surface = ?", (pid, surface)
|
|
389
|
+
)
|
|
390
|
+
conn.execute(
|
|
391
|
+
"DELETE FROM moodboard_items WHERE project_id = ? AND surface = ?", (pid, surface)
|
|
392
|
+
)
|
|
315
393
|
for it in items:
|
|
316
394
|
conn.execute(
|
|
317
395
|
f"INSERT INTO moodboard_items ({_ITEM_COLUMNS}) VALUES "
|
|
318
|
-
"(:id, :project_id, :type, :asset_id, :frame_id, :parent_id, :data, :x, :y,
|
|
319
|
-
":height, :rotation, :z_index, :created_at, :updated_at)",
|
|
396
|
+
"(:id, :project_id, :surface, :type, :asset_id, :frame_id, :parent_id, :data, :x, :y, "
|
|
397
|
+
":width, :height, :rotation, :z_index, :created_at, :updated_at)",
|
|
320
398
|
{
|
|
321
399
|
"id": it["id"],
|
|
322
400
|
"project_id": pid,
|
|
401
|
+
"surface": surface,
|
|
323
402
|
"type": it["type"],
|
|
324
403
|
"asset_id": it.get("assetId"),
|
|
325
404
|
"frame_id": it.get("frameId"),
|
|
@@ -338,11 +417,12 @@ def replace_board(
|
|
|
338
417
|
for c in connectors:
|
|
339
418
|
conn.execute(
|
|
340
419
|
"INSERT INTO moodboard_connectors "
|
|
341
|
-
"(id, project_id, from_item_id, to_item_id, label, data, created_at) "
|
|
342
|
-
"
|
|
420
|
+
"(id, project_id, surface, from_item_id, to_item_id, label, data, created_at) VALUES "
|
|
421
|
+
"(:id, :project_id, :surface, :from_item_id, :to_item_id, :label, :data, :created_at)",
|
|
343
422
|
{
|
|
344
423
|
"id": c["id"],
|
|
345
424
|
"project_id": pid,
|
|
425
|
+
"surface": surface,
|
|
346
426
|
"from_item_id": c["fromItemId"],
|
|
347
427
|
"to_item_id": c["toItemId"],
|
|
348
428
|
"label": c.get("label"),
|
|
@@ -359,11 +439,14 @@ def create_connector(
|
|
|
359
439
|
source_handle: str | None = None,
|
|
360
440
|
target_handle: str | None = None,
|
|
361
441
|
) -> dict[str, Any]:
|
|
362
|
-
get_item(conn, from_item_id)
|
|
442
|
+
source = get_item(conn, from_item_id)
|
|
363
443
|
get_item(conn, to_item_id)
|
|
364
444
|
connector = {
|
|
365
445
|
"id": _uuid(),
|
|
366
446
|
"projectId": _project_id(conn),
|
|
447
|
+
# A connector lives on the same canvas as the nodes it joins, so it's derived rather than
|
|
448
|
+
# passed in - the caller never has to know which surface it's drawing on.
|
|
449
|
+
"surface": source["surface"],
|
|
367
450
|
"fromItemId": from_item_id,
|
|
368
451
|
"toItemId": to_item_id,
|
|
369
452
|
"label": None,
|
|
@@ -372,11 +455,11 @@ def create_connector(
|
|
|
372
455
|
}
|
|
373
456
|
conn.execute(
|
|
374
457
|
"INSERT INTO moodboard_connectors "
|
|
375
|
-
"(id, project_id, from_item_id, to_item_id, label, data, created_at) "
|
|
376
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
458
|
+
"(id, project_id, surface, from_item_id, to_item_id, label, data, created_at) "
|
|
459
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
377
460
|
(
|
|
378
|
-
connector["id"], connector["projectId"],
|
|
379
|
-
json.dumps(connector["data"]), connector["createdAt"],
|
|
461
|
+
connector["id"], connector["projectId"], connector["surface"], from_item_id,
|
|
462
|
+
to_item_id, None, json.dumps(connector["data"]), connector["createdAt"],
|
|
380
463
|
),
|
|
381
464
|
)
|
|
382
465
|
return connector
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"""SQLite schema for a project's ``project.db`` - a faithful port of the Studio TypeScript
|
|
2
|
-
``electron/main/db/schema.ts`` (SCHEMA_VERSION 14). The DB is the source of truth for a
|
|
3
|
-
"save" is implicit.
|
|
2
|
+
``electron/main/db/schema.ts`` (ported at SCHEMA_VERSION 14). The DB is the source of truth for a
|
|
3
|
+
project; "save" is implicit. Bump ``SCHEMA_VERSION`` + add a migration to evolve the schema.
|
|
4
4
|
|
|
5
5
|
Kept byte-compatible with the Node schema so Core can open existing ``.inlinestudio`` projects: same
|
|
6
6
|
tables, same column names, same ``user_version`` stamping, and the same additive/rename migrations.
|
|
7
|
+
v15 adds the Core-only LoRA training tables (``training_datasets``/``_dataset_items``/``_runs``);
|
|
8
|
+
they are purely additive, so an older project gains them on next open.
|
|
7
9
|
"""
|
|
8
10
|
|
|
9
11
|
from __future__ import annotations
|
|
10
12
|
|
|
11
13
|
import sqlite3
|
|
12
14
|
|
|
13
|
-
SCHEMA_VERSION =
|
|
15
|
+
SCHEMA_VERSION = 16
|
|
14
16
|
|
|
15
17
|
SCHEMA_SQL = """
|
|
16
18
|
CREATE TABLE IF NOT EXISTS project (
|
|
@@ -86,6 +88,9 @@ CREATE TABLE IF NOT EXISTS assets (
|
|
|
86
88
|
CREATE TABLE IF NOT EXISTS moodboard_items (
|
|
87
89
|
id TEXT PRIMARY KEY,
|
|
88
90
|
project_id TEXT NOT NULL,
|
|
91
|
+
-- Which canvas the item lives on: the Studio moodboard or the Trainer tab's graph. One table,
|
|
92
|
+
-- two surfaces, so both canvases reuse the same item/connector plumbing.
|
|
93
|
+
surface TEXT NOT NULL DEFAULT 'studio',
|
|
89
94
|
type TEXT NOT NULL DEFAULT 'asset',
|
|
90
95
|
asset_id TEXT,
|
|
91
96
|
frame_id TEXT,
|
|
@@ -104,6 +109,7 @@ CREATE TABLE IF NOT EXISTS moodboard_items (
|
|
|
104
109
|
CREATE TABLE IF NOT EXISTS moodboard_connectors (
|
|
105
110
|
id TEXT PRIMARY KEY,
|
|
106
111
|
project_id TEXT NOT NULL,
|
|
112
|
+
surface TEXT NOT NULL DEFAULT 'studio',
|
|
107
113
|
from_item_id TEXT NOT NULL,
|
|
108
114
|
to_item_id TEXT NOT NULL,
|
|
109
115
|
label TEXT,
|
|
@@ -137,8 +143,50 @@ CREATE INDEX IF NOT EXISTS idx_frame_inputs_frame ON frame_inputs(frame_id);
|
|
|
137
143
|
CREATE INDEX IF NOT EXISTS idx_assets_project ON assets(project_id);
|
|
138
144
|
CREATE INDEX IF NOT EXISTS idx_assets_folder ON assets(folder_id);
|
|
139
145
|
CREATE INDEX IF NOT EXISTS idx_asset_folders_parent ON asset_folders(parent_id);
|
|
146
|
+
CREATE TABLE IF NOT EXISTS training_datasets (
|
|
147
|
+
id TEXT PRIMARY KEY,
|
|
148
|
+
project_id TEXT NOT NULL,
|
|
149
|
+
name TEXT NOT NULL,
|
|
150
|
+
trigger_word TEXT NOT NULL DEFAULT '',
|
|
151
|
+
created_at INTEGER NOT NULL,
|
|
152
|
+
updated_at INTEGER NOT NULL
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
CREATE TABLE IF NOT EXISTS training_dataset_items (
|
|
156
|
+
id TEXT PRIMARY KEY,
|
|
157
|
+
dataset_id TEXT NOT NULL,
|
|
158
|
+
asset_id TEXT NOT NULL,
|
|
159
|
+
caption TEXT NOT NULL DEFAULT '',
|
|
160
|
+
position INTEGER NOT NULL,
|
|
161
|
+
created_at INTEGER NOT NULL
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
CREATE TABLE IF NOT EXISTS training_runs (
|
|
165
|
+
id TEXT PRIMARY KEY,
|
|
166
|
+
project_id TEXT NOT NULL,
|
|
167
|
+
dataset_id TEXT NOT NULL,
|
|
168
|
+
name TEXT NOT NULL,
|
|
169
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
170
|
+
base_mode TEXT NOT NULL DEFAULT 'turbo_adapter',
|
|
171
|
+
hyperparams TEXT NOT NULL DEFAULT '{}',
|
|
172
|
+
output_lora_path TEXT,
|
|
173
|
+
progress_fraction REAL NOT NULL DEFAULT 0,
|
|
174
|
+
progress_status TEXT,
|
|
175
|
+
step INTEGER NOT NULL DEFAULT 0,
|
|
176
|
+
total_steps INTEGER NOT NULL DEFAULT 0,
|
|
177
|
+
checkpoint_path TEXT,
|
|
178
|
+
gpu_ids TEXT NOT NULL DEFAULT '[]',
|
|
179
|
+
error TEXT,
|
|
180
|
+
created_at INTEGER NOT NULL,
|
|
181
|
+
updated_at INTEGER NOT NULL
|
|
182
|
+
);
|
|
183
|
+
|
|
140
184
|
CREATE INDEX IF NOT EXISTS idx_moodboard_items_project ON moodboard_items(project_id);
|
|
141
185
|
CREATE INDEX IF NOT EXISTS idx_moodboard_connectors_project ON moodboard_connectors(project_id);
|
|
186
|
+
CREATE INDEX IF NOT EXISTS idx_training_datasets_project ON training_datasets(project_id);
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_training_dataset_items_dataset ON training_dataset_items(dataset_id);
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_training_runs_project ON training_runs(project_id);
|
|
189
|
+
CREATE INDEX IF NOT EXISTS idx_training_runs_dataset ON training_runs(dataset_id);
|
|
142
190
|
"""
|
|
143
191
|
|
|
144
192
|
|
|
@@ -187,6 +235,14 @@ def _migrate_columns(conn: sqlite3.Connection) -> None:
|
|
|
187
235
|
|
|
188
236
|
_add_column_if_missing(conn, "moodboard_items", "frame_id", "TEXT")
|
|
189
237
|
_add_column_if_missing(conn, "moodboard_items", "parent_id", "TEXT")
|
|
238
|
+
|
|
239
|
+
# v15 -> v16: the Trainer tab gets its own canvas; existing items stay on the Studio surface.
|
|
240
|
+
_add_column_if_missing(
|
|
241
|
+
conn, "moodboard_items", "surface", "TEXT NOT NULL DEFAULT 'studio'"
|
|
242
|
+
)
|
|
243
|
+
_add_column_if_missing(
|
|
244
|
+
conn, "moodboard_connectors", "surface", "TEXT NOT NULL DEFAULT 'studio'"
|
|
245
|
+
)
|
|
190
246
|
_add_column_if_missing(conn, "frame_inputs", "source_frame_id", "TEXT")
|
|
191
247
|
|
|
192
248
|
_relax_frame_inputs_asset_id(conn) # v8 -> v9: asset_id must be nullable
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Periodic host + GPU telemetry, broadcast on ``events:systemStats`` for the Trainer tab.
|
|
2
|
+
|
|
3
|
+
Samples CPU/RAM (psutil) and per-GPU utilization/VRAM/temp (pynvml) about once a second and fans it
|
|
4
|
+
out to every ``/events`` subscriber. Skips sampling entirely when nobody is listening, so it costs
|
|
5
|
+
nothing with no client connected. GPU stats are NVIDIA-only; an MPS/CPU host reports host metrics
|
|
6
|
+
with an empty ``gpus`` list. Missing psutil/pynvml degrade to zeros rather than erroring.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
_INTERVAL_SECONDS = 1.5
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SystemStats:
|
|
18
|
+
"""A background poller that broadcasts ``SystemStatsEvent`` frames while clients connect."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, events: Any) -> None:
|
|
21
|
+
self._events = events
|
|
22
|
+
self._task: asyncio.Task[None] | None = None
|
|
23
|
+
self._nvml_ready = False
|
|
24
|
+
|
|
25
|
+
def start(self) -> None:
|
|
26
|
+
if self._task is None:
|
|
27
|
+
self._task = asyncio.create_task(self._loop())
|
|
28
|
+
|
|
29
|
+
def stop(self) -> None:
|
|
30
|
+
if self._task is not None:
|
|
31
|
+
self._task.cancel()
|
|
32
|
+
self._task = None
|
|
33
|
+
|
|
34
|
+
async def _loop(self) -> None:
|
|
35
|
+
try:
|
|
36
|
+
while True:
|
|
37
|
+
await asyncio.sleep(_INTERVAL_SECONDS)
|
|
38
|
+
if self._events.subscriber_count == 0:
|
|
39
|
+
continue # nobody watching - don't spend a sample
|
|
40
|
+
stats = await asyncio.to_thread(self._sample)
|
|
41
|
+
self._events.broadcast("events:systemStats", stats)
|
|
42
|
+
except asyncio.CancelledError:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def _sample(self) -> dict[str, Any]:
|
|
46
|
+
cpu, ram_used, ram_total = self._host()
|
|
47
|
+
return {"cpu": cpu, "ramUsed": ram_used, "ramTotal": ram_total, "gpus": self._gpus()}
|
|
48
|
+
|
|
49
|
+
def _host(self) -> tuple[float, int, int]:
|
|
50
|
+
try:
|
|
51
|
+
import psutil
|
|
52
|
+
|
|
53
|
+
vm = psutil.virtual_memory()
|
|
54
|
+
return float(psutil.cpu_percent()), int(vm.used), int(vm.total)
|
|
55
|
+
except Exception: # noqa: BLE001 - psutil optional; telemetry degrades to zeros
|
|
56
|
+
return 0.0, 0, 0
|
|
57
|
+
|
|
58
|
+
def _gpus(self) -> list[dict[str, Any]]:
|
|
59
|
+
try:
|
|
60
|
+
import pynvml
|
|
61
|
+
except Exception: # noqa: BLE001 - no NVML (non-NVIDIA or extra not installed)
|
|
62
|
+
return []
|
|
63
|
+
try:
|
|
64
|
+
if not self._nvml_ready:
|
|
65
|
+
pynvml.nvmlInit()
|
|
66
|
+
self._nvml_ready = True
|
|
67
|
+
out: list[dict[str, Any]] = []
|
|
68
|
+
for i in range(pynvml.nvmlDeviceGetCount()):
|
|
69
|
+
handle = pynvml.nvmlDeviceGetHandleByIndex(i)
|
|
70
|
+
mem = pynvml.nvmlDeviceGetMemoryInfo(handle)
|
|
71
|
+
util = pynvml.nvmlDeviceGetUtilizationRates(handle)
|
|
72
|
+
name = pynvml.nvmlDeviceGetName(handle)
|
|
73
|
+
out.append(
|
|
74
|
+
{
|
|
75
|
+
"index": i,
|
|
76
|
+
"name": name.decode() if isinstance(name, bytes) else name,
|
|
77
|
+
"utilization": float(util.gpu),
|
|
78
|
+
"memoryUsed": int(mem.used),
|
|
79
|
+
"memoryTotal": int(mem.total),
|
|
80
|
+
"temperature": _temperature(pynvml, handle),
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
return out
|
|
84
|
+
except Exception: # noqa: BLE001 - any NVML hiccup: report no GPUs rather than crash
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _temperature(pynvml: Any, handle: Any) -> float | None:
|
|
89
|
+
try:
|
|
90
|
+
return float(pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU))
|
|
91
|
+
except Exception: # noqa: BLE001
|
|
92
|
+
return None
|