inline-core 1.1.1__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.
Files changed (77) hide show
  1. inline_core/__init__.py +7 -0
  2. inline_core/components/__init__.py +1 -0
  3. inline_core/components/conditioning.py +20 -0
  4. inline_core/components/interfaces.py +91 -0
  5. inline_core/config.py +33 -0
  6. inline_core/device/__init__.py +1 -0
  7. inline_core/device/auto.py +35 -0
  8. inline_core/device/detect.py +41 -0
  9. inline_core/device/memory.py +168 -0
  10. inline_core/device/policy.py +82 -0
  11. inline_core/device/types.py +31 -0
  12. inline_core/errors.py +42 -0
  13. inline_core/graph/__init__.py +1 -0
  14. inline_core/graph/cache.py +83 -0
  15. inline_core/graph/descriptor.py +81 -0
  16. inline_core/graph/executor.py +102 -0
  17. inline_core/graph/primitives.py +137 -0
  18. inline_core/graph/registry.py +61 -0
  19. inline_core/graph/runners.py +72 -0
  20. inline_core/graph/schema.py +136 -0
  21. inline_core/graph/topo.py +49 -0
  22. inline_core/graph/validate.py +56 -0
  23. inline_core/importer/__init__.py +1 -0
  24. inline_core/importer/comfy.py +162 -0
  25. inline_core/media.py +11 -0
  26. inline_core/models/__init__.py +8 -0
  27. inline_core/models/catalog.py +98 -0
  28. inline_core/models/zimage/__init__.py +11 -0
  29. inline_core/models/zimage/requirements.py +180 -0
  30. inline_core/models/zimage/runner.py +386 -0
  31. inline_core/parallel/__init__.py +13 -0
  32. inline_core/parallel/config.py +50 -0
  33. inline_core/parallel/group.py +136 -0
  34. inline_core/parallel/launch.py +44 -0
  35. inline_core/parallel/protocol.py +53 -0
  36. inline_core/parallel/registry.py +31 -0
  37. inline_core/parallel/worker.py +75 -0
  38. inline_core/runtime/__init__.py +1 -0
  39. inline_core/runtime/context.py +31 -0
  40. inline_core/runtime/file_store.py +51 -0
  41. inline_core/runtime/progress.py +83 -0
  42. inline_core/runtime/run.py +113 -0
  43. inline_core/runtime/store.py +18 -0
  44. inline_core/sampling/__init__.py +1 -0
  45. inline_core/sampling/batch.py +116 -0
  46. inline_core/server/__init__.py +1 -0
  47. inline_core/server/__main__.py +61 -0
  48. inline_core/server/app.py +327 -0
  49. inline_core/server/assets.py +47 -0
  50. inline_core/server/bootstrap.py +21 -0
  51. inline_core/server/frontend.py +37 -0
  52. inline_core/server/manager.py +195 -0
  53. inline_core/server/rpc.py +60 -0
  54. inline_core/server/run_store.py +155 -0
  55. inline_core/server/serialize.py +144 -0
  56. inline_core/studio/__init__.py +7 -0
  57. inline_core/studio/assets.py +194 -0
  58. inline_core/studio/config.py +29 -0
  59. inline_core/studio/fal.py +237 -0
  60. inline_core/studio/frames.py +552 -0
  61. inline_core/studio/generation.py +136 -0
  62. inline_core/studio/graph_build.py +109 -0
  63. inline_core/studio/handlers.py +236 -0
  64. inline_core/studio/models.py +173 -0
  65. inline_core/studio/moodboard.py +400 -0
  66. inline_core/studio/schema.py +278 -0
  67. inline_core/studio/store.py +291 -0
  68. inline_core/studio/timeline/__init__.py +6 -0
  69. inline_core/studio/timeline/compose.py +130 -0
  70. inline_core/studio/timeline/ffmpeg.py +76 -0
  71. inline_core/studio/timeline/render.py +120 -0
  72. inline_core/studio/timeline/resolve.py +189 -0
  73. inline_core/takes.py +31 -0
  74. inline_core-1.1.1.dist-info/METADATA +35 -0
  75. inline_core-1.1.1.dist-info/RECORD +77 -0
  76. inline_core-1.1.1.dist-info/WHEEL +4 -0
  77. inline_core-1.1.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,31 @@
1
+ """One running worker group per parallel config, created lazily and reused across jobs.
2
+
3
+ The server owns a single registry and shuts it down with the app; the parallel sampler asks it for
4
+ the group that matches a job's placement. Keying on the frozen ParallelConfig means the same model
5
+ and split reuse one process group.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .config import ParallelConfig
11
+ from .group import WorkerGroup
12
+ from .launch import Launcher
13
+
14
+
15
+ class GroupRegistry:
16
+ def __init__(self, launcher: Launcher | None = None) -> None:
17
+ self._launcher = launcher
18
+ self._groups: dict[ParallelConfig, WorkerGroup] = {}
19
+
20
+ def get_or_create(self, config: ParallelConfig) -> WorkerGroup:
21
+ group = self._groups.get(config)
22
+ if group is None:
23
+ group = WorkerGroup(config, self._launcher)
24
+ group.start()
25
+ self._groups[config] = group
26
+ return group
27
+
28
+ def shutdown_all(self) -> None:
29
+ while self._groups:
30
+ _, group = self._groups.popitem()
31
+ group.shutdown()
@@ -0,0 +1,75 @@
1
+ """Rank entrypoint for a parallel denoise group. Launched by a Launcher, one process per rank.
2
+
3
+ Rank 0 owns the IPC channel to the manager: it receives jobs, streams progress, and returns results
4
+ through a pluggable handler. The handler decides what a job means: the stub handler (no torch) backs
5
+ the scaffold and the round-trip test; the xfuser handler (loads the sharded pipeline and runs the
6
+ collective denoise across all ranks) lands with the Z-Image runner (C2).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import socket
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
16
+ from .config import ADDR_ENV, CONFIG_ENV, ParallelConfig
17
+ from .protocol import MessageType, recv_message, send_message
18
+
19
+ # report(step, total) streams progress to the manager while a job runs.
20
+ ProgressFn = Callable[[int, int], None]
21
+ # handler(config, payload, report) -> result payload.
22
+ Handler = Callable[[ParallelConfig, dict[str, Any], ProgressFn], dict[str, Any]]
23
+
24
+
25
+ def _stub_handler(
26
+ config: ParallelConfig, payload: dict[str, Any], report: ProgressFn
27
+ ) -> dict[str, Any]:
28
+ total = int(payload.get("steps", 1))
29
+ for step in range(1, total + 1):
30
+ report(step, total)
31
+ return {"echo": payload, "model": config.model, "world_size": config.world_size}
32
+
33
+
34
+ def _select_handler(config: ParallelConfig) -> Handler:
35
+ if config.stub:
36
+ return _stub_handler
37
+ raise NotImplementedError(
38
+ "the xfuser denoise handler lands with the Z-Image runner (C2); "
39
+ "only the stub handler is available today"
40
+ )
41
+
42
+
43
+ def serve(sock: socket.socket, config: ParallelConfig, handler: Handler) -> None:
44
+ send_message(sock, {"type": MessageType.READY, "worldSize": config.world_size})
45
+ while True:
46
+ message = recv_message(sock)
47
+ if message is None or message.get("type") == MessageType.SHUTDOWN:
48
+ return
49
+ if message.get("type") != MessageType.JOB:
50
+ continue
51
+ payload: dict[str, Any] = message.get("payload", {})
52
+
53
+ def report(step: int, total: int) -> None:
54
+ send_message(sock, {"type": MessageType.PROGRESS, "step": step, "total": total})
55
+
56
+ try:
57
+ result = handler(config, payload, report)
58
+ except Exception as exc: # translate into a protocol error; the manager re-raises typed
59
+ send_message(sock, {"type": MessageType.ERROR, "message": str(exc)})
60
+ continue
61
+ send_message(sock, {"type": MessageType.RESULT, "payload": result})
62
+
63
+
64
+ def main() -> None:
65
+ config = ParallelConfig.from_json(os.environ[CONFIG_ENV])
66
+ host, _, port = os.environ[ADDR_ENV].partition(":")
67
+ sock = socket.create_connection((host, int(port)))
68
+ try:
69
+ serve(sock, config, _select_handler(config))
70
+ finally:
71
+ sock.close()
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
@@ -0,0 +1 @@
1
+ """Per-run execution: the progress event bus, run state, and the execution context."""
@@ -0,0 +1,31 @@
1
+ """The per-run execution context threaded through every component call."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from threading import Event
7
+
8
+ from ..device.policy import DevicePolicy
9
+ from .progress import ProgressEmitter
10
+
11
+
12
+ class CancelToken:
13
+ """Cooperative cancellation. The executor checks it between nodes and steps."""
14
+
15
+ def __init__(self) -> None:
16
+ self._event = Event()
17
+
18
+ def cancel(self) -> None:
19
+ self._event.set()
20
+
21
+ @property
22
+ def cancelled(self) -> bool:
23
+ return self._event.is_set()
24
+
25
+
26
+ @dataclass
27
+ class ExecutionContext:
28
+ run_id: str
29
+ policy: DevicePolicy
30
+ emitter: ProgressEmitter
31
+ cancel: CancelToken
@@ -0,0 +1,51 @@
1
+ """A file-backed take store: writes a decoded image to <root>/<take_id>.png and records the take."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from uuid import uuid4
10
+
11
+ from ..media import MediaKind
12
+ from ..takes import Take
13
+ from .store import TakeStore
14
+
15
+
16
+ class FileTakeStore(TakeStore):
17
+ def __init__(self, root: Path) -> None:
18
+ self._root = root
19
+
20
+ def save(self, run_id: str, node_id: str, image: Any, params: dict[str, Any]) -> Take:
21
+ self._root.mkdir(parents=True, exist_ok=True)
22
+ take_id = f"take_{uuid4().hex[:12]}"
23
+ path = self._root / f"{take_id}.png"
24
+ _to_pil(image).save(path, format="PNG")
25
+ data = path.read_bytes()
26
+ return Take(
27
+ id=take_id,
28
+ run_id=run_id,
29
+ node_id=node_id,
30
+ kind=MediaKind.IMAGE,
31
+ uri=str(path),
32
+ hash=f"sha256-{hashlib.sha256(data).hexdigest()}",
33
+ params=dict(params),
34
+ created_at=int(time.time() * 1000),
35
+ )
36
+
37
+
38
+ def _to_pil(image: Any) -> Any:
39
+ from PIL import Image
40
+
41
+ if isinstance(image, Image.Image):
42
+ return image
43
+ if hasattr(image, "detach"): # a torch tensor
44
+ image = image.detach().to("cpu").numpy()
45
+ import numpy as np
46
+
47
+ array = np.asarray(image)
48
+ if array.dtype != np.uint8:
49
+ scaled = array * 255.0 if float(array.max(initial=0.0)) <= 1.0 else array
50
+ array = scaled.clip(0, 255).round().astype(np.uint8)
51
+ return Image.fromarray(array)
@@ -0,0 +1,83 @@
1
+ """Run events and the emitter seam. Mirrors the websocket events in docs/contract.md section 6.
2
+
3
+ Coalescing (the contract's bounded event rate) is a wrapper added at the serving layer; the executor
4
+ and components just emit.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+
13
+ from ..takes import Take
14
+
15
+
16
+ class Phase(str, Enum):
17
+ QUEUED = "queued"
18
+ PREPARING = "preparing"
19
+ LOADING = "loading"
20
+ ENCODE = "encode"
21
+ SAMPLE = "sample"
22
+ DECODE = "decode"
23
+ SAVE = "save"
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class ProgressEvent:
28
+ run_id: str
29
+ node_id: str
30
+ phase: Phase
31
+ fraction: float
32
+ step: int | None = None
33
+ step_count: int | None = None
34
+ eta_ms: int | None = None
35
+ status: str = ""
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class NodeDoneEvent:
40
+ run_id: str
41
+ node_id: str
42
+ cached: bool
43
+ takes: list[Take] = field(default_factory=list)
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class RunDoneEvent:
48
+ run_id: str
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class CancelledEvent:
53
+ run_id: str
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class ErrorEvent:
58
+ run_id: str
59
+ message: str
60
+ node_id: str | None = None
61
+
62
+
63
+ RunEvent = ProgressEvent | NodeDoneEvent | RunDoneEvent | CancelledEvent | ErrorEvent
64
+
65
+
66
+ class ProgressEmitter(ABC):
67
+ @abstractmethod
68
+ def emit(self, event: RunEvent) -> None: ...
69
+
70
+
71
+ class NullEmitter(ProgressEmitter):
72
+ def emit(self, event: RunEvent) -> None:
73
+ return None
74
+
75
+
76
+ class CollectingEmitter(ProgressEmitter):
77
+ """Keeps every event in order. For tests and the snapshot builder."""
78
+
79
+ def __init__(self) -> None:
80
+ self.events: list[RunEvent] = []
81
+
82
+ def emit(self, event: RunEvent) -> None:
83
+ self.events.append(event)
@@ -0,0 +1,113 @@
1
+ """Run state: the durable snapshot GET /v1/runs/{id} serves, kept fresh by applying run events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+
8
+ from ..takes import Take
9
+ from .progress import (
10
+ CancelledEvent,
11
+ ErrorEvent,
12
+ NodeDoneEvent,
13
+ ProgressEmitter,
14
+ ProgressEvent,
15
+ RunDoneEvent,
16
+ RunEvent,
17
+ )
18
+
19
+
20
+ class RunStatus(str, Enum):
21
+ QUEUED = "queued"
22
+ RUNNING = "running"
23
+ DONE = "done"
24
+ ERROR = "error"
25
+ CANCELLED = "cancelled"
26
+
27
+
28
+ class NodeState(str, Enum):
29
+ QUEUED = "queued"
30
+ RUNNING = "running"
31
+ CACHED = "cached"
32
+ DONE = "done"
33
+ ERROR = "error"
34
+
35
+
36
+ @dataclass
37
+ class NodeRuntimeState:
38
+ state: NodeState = NodeState.QUEUED
39
+ phase: str | None = None
40
+ fraction: float = 0.0
41
+ step: int | None = None
42
+ step_count: int | None = None
43
+ status: str = ""
44
+
45
+
46
+ @dataclass
47
+ class RunError:
48
+ message: str
49
+ node_id: str | None = None
50
+
51
+
52
+ @dataclass
53
+ class RunState:
54
+ run_id: str
55
+ target: str
56
+ status: RunStatus = RunStatus.QUEUED
57
+ fraction: float = 0.0
58
+ nodes: dict[str, NodeRuntimeState] = field(default_factory=dict)
59
+ takes: list[Take] = field(default_factory=list)
60
+ error: RunError | None = None
61
+
62
+
63
+ _COMPLETE = (NodeState.DONE, NodeState.CACHED)
64
+
65
+
66
+ def _recompute_fraction(state: RunState) -> None:
67
+ if not state.nodes:
68
+ state.fraction = 0.0
69
+ return
70
+ total = sum(1.0 if n.state in _COMPLETE else n.fraction for n in state.nodes.values())
71
+ state.fraction = total / len(state.nodes)
72
+
73
+
74
+ def apply_event(state: RunState, event: RunEvent) -> None:
75
+ """Fold a run event into the run state. The single event -> snapshot mapping."""
76
+ if isinstance(event, ProgressEvent):
77
+ node = state.nodes.setdefault(event.node_id, NodeRuntimeState())
78
+ node.state = NodeState.RUNNING
79
+ node.phase = event.phase.value
80
+ node.fraction = event.fraction
81
+ node.step = event.step
82
+ node.step_count = event.step_count
83
+ node.status = event.status
84
+ _recompute_fraction(state)
85
+ elif isinstance(event, NodeDoneEvent):
86
+ node = state.nodes.setdefault(event.node_id, NodeRuntimeState())
87
+ node.state = NodeState.CACHED if event.cached else NodeState.DONE
88
+ node.fraction = 1.0
89
+ known = {t.id for t in state.takes}
90
+ state.takes.extend(t for t in event.takes if t.id not in known)
91
+ _recompute_fraction(state)
92
+ elif isinstance(event, RunDoneEvent):
93
+ state.status = RunStatus.DONE
94
+ state.fraction = 1.0
95
+ elif isinstance(event, CancelledEvent):
96
+ state.status = RunStatus.CANCELLED
97
+ elif isinstance(event, ErrorEvent):
98
+ state.status = RunStatus.ERROR
99
+ state.error = RunError(message=event.message, node_id=event.node_id)
100
+ if event.node_id is not None:
101
+ state.nodes.setdefault(event.node_id, NodeRuntimeState()).state = NodeState.ERROR
102
+
103
+
104
+ class StateTrackingEmitter(ProgressEmitter):
105
+ """Applies each event to a RunState, then forwards it to the real emitter."""
106
+
107
+ def __init__(self, delegate: ProgressEmitter, state: RunState) -> None:
108
+ self._delegate = delegate
109
+ self._state = state
110
+
111
+ def emit(self, event: RunEvent) -> None:
112
+ apply_event(self._state, event)
113
+ self._delegate.emit(event)
@@ -0,0 +1,18 @@
1
+ """The take store seam: persist a decoded output as an immutable take (bytes, hash, uri).
2
+
3
+ Phase 1's implementation writes into the project's takes/ folder. Kept behind this interface so a
4
+ fleet object store swaps in without touching the executor.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from abc import ABC, abstractmethod
10
+ from typing import Any
11
+
12
+ from ..takes import Take
13
+
14
+
15
+ class TakeStore(ABC):
16
+ @abstractmethod
17
+ def save(self, run_id: str, node_id: str, image: Any, params: dict[str, Any]) -> Take:
18
+ """Persist a decoded image (PIL, numpy, or tensor) as an immutable take."""
@@ -0,0 +1 @@
1
+ """The batched-sampler boundary: the graph submits jobs, this batches and steps them on the GPU."""
@@ -0,0 +1,116 @@
1
+ """The graph/GPU boundary. The graph never runs the denoise loop inline; it submits a SampleJob.
2
+
3
+ Phase 1 runs one job at a time behind this seam. Phase 5 adds cross-request batching (grouping by
4
+ model family, resolution, and adapter bucket) without changing the interface. Multi-GPU splits one
5
+ job across a worker group (XFuserBatchedSampler) when the device policy asks for a parallel denoise.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from dataclasses import dataclass
12
+ from typing import TYPE_CHECKING, Any, Protocol
13
+
14
+ from ..components.conditioning import Conditioning, Latents
15
+ from ..components.interfaces import Denoiser, Sampler, Scheduler, StepCallback, StepInfo
16
+
17
+ if TYPE_CHECKING:
18
+ from ..device.policy import Placement
19
+ from ..parallel.config import ParallelConfig
20
+ from ..parallel.group import ProgressHandler
21
+ from ..parallel.registry import GroupRegistry
22
+ from ..runtime.context import ExecutionContext
23
+
24
+
25
+ @dataclass
26
+ class SampleJob:
27
+ """One request to sample. Compatible jobs are grouped and stepped together."""
28
+
29
+ denoiser: Denoiser
30
+ scheduler: Scheduler
31
+ sampler: Sampler
32
+ latents: Latents
33
+ conditioning: Conditioning
34
+ steps: int
35
+ on_step: StepCallback | None = None
36
+
37
+
38
+ class BatchedSampler(ABC):
39
+ @abstractmethod
40
+ def submit(self, job: SampleJob, ctx: ExecutionContext) -> Latents: ...
41
+
42
+
43
+ class InlineBatchedSampler(BatchedSampler):
44
+ """Phase 1: no batching. Run the job through its sampler directly."""
45
+
46
+ def submit(self, job: SampleJob, ctx: ExecutionContext) -> Latents:
47
+ return job.sampler.sample(
48
+ job.denoiser,
49
+ job.scheduler,
50
+ job.latents,
51
+ job.conditioning,
52
+ steps=job.steps,
53
+ ctx=ctx,
54
+ on_step=job.on_step,
55
+ )
56
+
57
+
58
+ class ParallelCodec(Protocol):
59
+ """Bridges a SampleJob and its worker group: which group, request out, latents back.
60
+
61
+ The real codec moves torch tensors over a side channel and builds the xfuser request; it lives
62
+ with the model runner (C2) so this module stays torch-free and the group stays mockable.
63
+ """
64
+
65
+ def config(self, placement: Placement) -> ParallelConfig:
66
+ """The group identity (model + split) for this placement."""
67
+ ...
68
+
69
+ def to_request(self, job: SampleJob) -> dict[str, Any]:
70
+ """The serializable request the worker runs against its resident pipeline."""
71
+ ...
72
+
73
+ def from_result(self, result: dict[str, Any], job: SampleJob) -> Latents:
74
+ """The sampled latents the worker returned."""
75
+ ...
76
+
77
+
78
+ class XFuserBatchedSampler(BatchedSampler):
79
+ """Routes a parallel-placement denoise to the worker group; everything else runs inline.
80
+
81
+ The device policy decides: a single-GPU or CPU run has no parallel placement, so it keeps the
82
+ in-process path with zero overhead. Only a multi-GPU denoiser placement crosses to the group.
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ registry: GroupRegistry,
88
+ codec: ParallelCodec,
89
+ inline: BatchedSampler | None = None,
90
+ ) -> None:
91
+ self._registry = registry
92
+ self._codec = codec
93
+ self._inline = inline or InlineBatchedSampler()
94
+
95
+ def submit(self, job: SampleJob, ctx: ExecutionContext) -> Latents:
96
+ placement = ctx.policy.placement("denoiser")
97
+ if placement.parallel is None:
98
+ return self._inline.submit(job, ctx)
99
+ group = self._registry.get_or_create(self._codec.config(placement))
100
+ result = group.submit(self._codec.to_request(job), _forward_progress(job))
101
+ return self._codec.from_result(result, job)
102
+
103
+ def close(self) -> None:
104
+ self._registry.shutdown_all()
105
+
106
+
107
+ def _forward_progress(job: SampleJob) -> ProgressHandler | None:
108
+ """Feed worker step ticks into the job's on_step callback, so streaming stays unchanged."""
109
+ on_step = job.on_step
110
+ if on_step is None:
111
+ return None
112
+
113
+ def report(step: int, total: int) -> None:
114
+ on_step(StepInfo(step=step, total=total))
115
+
116
+ return report
@@ -0,0 +1 @@
1
+ """The /v1 HTTP and websocket serving layer. A thin shell over the graph engine (see contract)."""
@@ -0,0 +1,61 @@
1
+ """Run the engine: `python -m inline_core.server`. Registers models whose deps are installed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uvicorn
6
+
7
+ from ..config import data_dir, server_host, server_port
8
+ from ..device.memory import MemoryPolicy
9
+ from ..graph.cache import InMemoryCache
10
+ from ..graph.registry import build_default_registry
11
+ from ..runtime.file_store import FileTakeStore
12
+ from ..studio import config as studio_config
13
+ from ..studio.store import StudioStore
14
+ from .app import create_app
15
+ from .bootstrap import register_models
16
+ from .frontend import resolve_frontend_root
17
+ from .rpc import EventBroadcaster, RpcRouter
18
+ from .run_store import SqliteRunStore
19
+
20
+
21
+ def main() -> None:
22
+ policy = MemoryPolicy()
23
+ registry = build_default_registry()
24
+ data = data_dir()
25
+ takes = data / "takes"
26
+ store = FileTakeStore(takes)
27
+ run_store = SqliteRunStore(data / "runs.db")
28
+ registered = register_models(registry, store, policy)
29
+ print(f"Registered models: {registered or 'none (source nodes only)'}")
30
+ frontend_root = resolve_frontend_root()
31
+ fe = frontend_root or "none (API only); use --front-end-root or install the frontend package"
32
+ print(f"Frontend: {fe}")
33
+ # The Studio app-backend: Core is the sole native backend (projects, frames, moodboard, assets,
34
+ # generation, fal, timeline). Every InlineStudioApi channel is handled here.
35
+ rpc = RpcRouter()
36
+ events = EventBroadcaster()
37
+ store = StudioStore(
38
+ studio_config.data_dir(),
39
+ studio_config.workspace_dir(),
40
+ default_comfy_url=studio_config.DEFAULT_COMFY_URL,
41
+ default_core_url=studio_config.DEFAULT_CORE_URL,
42
+ )
43
+ print(f"Studio data: {studio_config.data_dir()} | workspace: {studio_config.workspace_dir()}")
44
+ app = create_app(
45
+ registry=registry,
46
+ cache=InMemoryCache(),
47
+ policy=policy,
48
+ run_store=run_store,
49
+ takes_dir=str(takes),
50
+ frontend_root=frontend_root,
51
+ rpc=rpc,
52
+ events=events,
53
+ studio_store=store,
54
+ )
55
+ host, port = server_host(), server_port()
56
+ print(f"Serving on http://{host}:{port}")
57
+ uvicorn.run(app, host=host, port=port)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()