comfygit-studio 0.5.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.
- comfygit_studio/__init__.py +10 -0
- comfygit_studio/api_schema.py +564 -0
- comfygit_studio/embedded.py +295 -0
- comfygit_studio/executor.py +1002 -0
- comfygit_studio/openapi/studio-contract-api.v1.json +1379 -0
- comfygit_studio/runtime.py +3155 -0
- comfygit_studio/state.py +1019 -0
- comfygit_studio/static/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- comfygit_studio/static/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- comfygit_studio/static/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- comfygit_studio/static/assets/index-BDmIh9tA.css +1 -0
- comfygit_studio/static/assets/index-BlnDFmNd.js +17 -0
- comfygit_studio/static/index.html +14 -0
- comfygit_studio-0.5.0.dist-info/METADATA +25 -0
- comfygit_studio-0.5.0.dist-info/RECORD +16 -0
- comfygit_studio-0.5.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Embedded Studio runtime helpers for hosts that already run aiohttp."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import aiohttp
|
|
12
|
+
from aiohttp import web
|
|
13
|
+
from comfygit_core import Environment
|
|
14
|
+
|
|
15
|
+
from .runtime import (
|
|
16
|
+
SERVE_STATE_KEY,
|
|
17
|
+
STUDIO_API_BASE_PATH_KEY,
|
|
18
|
+
STUDIO_STATIC_DIR_KEY,
|
|
19
|
+
ServeConfig,
|
|
20
|
+
ServeState,
|
|
21
|
+
cancel_run_handler,
|
|
22
|
+
contracts_handler,
|
|
23
|
+
favicon_handler,
|
|
24
|
+
gallery_delete_handler,
|
|
25
|
+
gallery_handler,
|
|
26
|
+
health_handler,
|
|
27
|
+
openapi_handler,
|
|
28
|
+
output_view_handler,
|
|
29
|
+
run_contract_handler,
|
|
30
|
+
runs_handler,
|
|
31
|
+
single_contract_handler,
|
|
32
|
+
single_run_handler,
|
|
33
|
+
studio_index_handler,
|
|
34
|
+
upload_prepare_handler,
|
|
35
|
+
upload_put_handler,
|
|
36
|
+
upload_status_handler,
|
|
37
|
+
worker_callback_handler,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
_EMBEDDED_SESSION_KEY = web.AppKey("comfygit_studio_embedded_session", aiohttp.ClientSession)
|
|
41
|
+
_EMBEDDED_ENV_PATH_KEY = web.AppKey("comfygit_studio_embedded_env_path", str)
|
|
42
|
+
_EMBEDDED_URL_KEY = web.AppKey("comfygit_studio_embedded_url", str)
|
|
43
|
+
_EMBEDDED_COMFY_URL_KEY = web.AppKey("comfygit_studio_embedded_comfy_url", str)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class EmbeddedStudioResult:
|
|
48
|
+
"""Browser-facing metadata for an embedded Studio runtime."""
|
|
49
|
+
|
|
50
|
+
status: str
|
|
51
|
+
url: str | None
|
|
52
|
+
env_name: str
|
|
53
|
+
started: bool = False
|
|
54
|
+
reused: bool = False
|
|
55
|
+
comfy_url: str | None = None
|
|
56
|
+
mode: str = "embedded"
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> dict[str, Any]:
|
|
59
|
+
return {
|
|
60
|
+
"status": self.status,
|
|
61
|
+
"url": self.url,
|
|
62
|
+
"env_name": self.env_name,
|
|
63
|
+
"started": self.started,
|
|
64
|
+
"reused": self.reused,
|
|
65
|
+
"comfy_url": self.comfy_url,
|
|
66
|
+
"mode": self.mode,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def register_embedded_studio_routes(
|
|
71
|
+
app: web.Application,
|
|
72
|
+
*,
|
|
73
|
+
route_api_prefix: str,
|
|
74
|
+
route_ui_prefix: str,
|
|
75
|
+
public_api_base_path: str,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Register namespaced Studio UI and API routes on an existing app."""
|
|
78
|
+
|
|
79
|
+
configure_embedded_studio_app(app, public_api_base_path=public_api_base_path)
|
|
80
|
+
app.add_routes(
|
|
81
|
+
create_embedded_studio_routes(
|
|
82
|
+
route_api_prefix=route_api_prefix,
|
|
83
|
+
route_ui_prefix=route_ui_prefix,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
app.on_cleanup.append(close_embedded_studio)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def configure_embedded_studio_app(
|
|
90
|
+
app: web.Application,
|
|
91
|
+
*,
|
|
92
|
+
public_api_base_path: str,
|
|
93
|
+
static_dir: Path | None = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Configure static assets and browser-facing API base path before startup."""
|
|
96
|
+
|
|
97
|
+
app[STUDIO_STATIC_DIR_KEY] = static_dir or _studio_static_dir()
|
|
98
|
+
app[STUDIO_API_BASE_PATH_KEY] = _normalize_public_prefix(public_api_base_path)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def create_embedded_studio_routes(
|
|
102
|
+
*,
|
|
103
|
+
route_api_prefix: str,
|
|
104
|
+
route_ui_prefix: str,
|
|
105
|
+
) -> web.RouteTableDef:
|
|
106
|
+
"""Create namespaced Studio routes for ComfyUI/Manager-style hosts."""
|
|
107
|
+
|
|
108
|
+
routes = web.RouteTableDef()
|
|
109
|
+
api_prefix = _normalize_route_prefix(route_api_prefix)
|
|
110
|
+
ui_prefix = _normalize_route_prefix(route_ui_prefix)
|
|
111
|
+
|
|
112
|
+
def configured(handler):
|
|
113
|
+
async def wrapped(request: web.Request) -> web.StreamResponse:
|
|
114
|
+
return await handler(request)
|
|
115
|
+
|
|
116
|
+
return wrapped
|
|
117
|
+
|
|
118
|
+
routes.get(_route_path(ui_prefix, "/"))(configured(studio_index_handler))
|
|
119
|
+
routes.get(ui_prefix)(configured(studio_index_handler))
|
|
120
|
+
routes.get(_route_path(ui_prefix, "/assets/{tail:.*}"))(configured(_studio_asset_handler))
|
|
121
|
+
routes.get(_route_path(ui_prefix, "/favicon.ico"))(configured(favicon_handler))
|
|
122
|
+
routes.get(_route_path(ui_prefix, "/{tail:.*}"))(configured(studio_index_handler))
|
|
123
|
+
|
|
124
|
+
api_routes = (
|
|
125
|
+
("GET", "/openapi.json", openapi_handler),
|
|
126
|
+
("GET", "/health", health_handler),
|
|
127
|
+
("GET", "/contracts", contracts_handler),
|
|
128
|
+
("GET", "/contracts/{workflow}/{contract}", single_contract_handler),
|
|
129
|
+
("POST", "/uploads/prepare", upload_prepare_handler),
|
|
130
|
+
("PUT", "/uploads/{upload_id}", upload_put_handler),
|
|
131
|
+
("GET", "/uploads/{upload_id}/status", upload_status_handler),
|
|
132
|
+
("GET", "/gallery", gallery_handler),
|
|
133
|
+
("DELETE", "/gallery/{item_id}", gallery_delete_handler),
|
|
134
|
+
("GET", "/runs", runs_handler),
|
|
135
|
+
("GET", "/runs/{run_id}", single_run_handler),
|
|
136
|
+
("POST", "/runs/{run_id}/cancel", cancel_run_handler),
|
|
137
|
+
("POST", "/worker-callback/runs/{run_id}", worker_callback_handler),
|
|
138
|
+
("POST", "/contracts/{workflow}/{contract}/run", run_contract_handler),
|
|
139
|
+
("GET", "/outputs/view", output_view_handler),
|
|
140
|
+
)
|
|
141
|
+
for method, path, handler in api_routes:
|
|
142
|
+
routes.route(method, _route_path(api_prefix, path))(configured(handler))
|
|
143
|
+
|
|
144
|
+
return routes
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def open_embedded_studio(
|
|
148
|
+
app: web.Application,
|
|
149
|
+
env: Environment,
|
|
150
|
+
*,
|
|
151
|
+
public_scheme: str,
|
|
152
|
+
public_host: str,
|
|
153
|
+
public_ui_path: str,
|
|
154
|
+
comfy_url: str,
|
|
155
|
+
max_request_bytes: int = 256 * 1024 * 1024,
|
|
156
|
+
) -> EmbeddedStudioResult:
|
|
157
|
+
"""Create or reuse the embedded Studio runtime for an environment."""
|
|
158
|
+
|
|
159
|
+
env_path = str(Path(env.path).resolve())
|
|
160
|
+
url = _public_url(public_scheme, public_host, public_ui_path)
|
|
161
|
+
existing_env_path = app.get(_EMBEDDED_ENV_PATH_KEY)
|
|
162
|
+
existing_comfy_url = app.get(_EMBEDDED_COMFY_URL_KEY)
|
|
163
|
+
if existing_env_path == env_path and existing_comfy_url == comfy_url and SERVE_STATE_KEY in app:
|
|
164
|
+
app[_EMBEDDED_URL_KEY] = url
|
|
165
|
+
return EmbeddedStudioResult(
|
|
166
|
+
status="running",
|
|
167
|
+
url=url,
|
|
168
|
+
env_name=env.name,
|
|
169
|
+
reused=True,
|
|
170
|
+
comfy_url=comfy_url,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
await _close_current_state(app)
|
|
174
|
+
|
|
175
|
+
session = aiohttp.ClientSession()
|
|
176
|
+
config = ServeConfig(
|
|
177
|
+
host="embedded",
|
|
178
|
+
port=0,
|
|
179
|
+
comfy_url=comfy_url,
|
|
180
|
+
max_request_bytes=max_request_bytes,
|
|
181
|
+
state="local",
|
|
182
|
+
gallery="private",
|
|
183
|
+
state_db=_embedded_state_db(env),
|
|
184
|
+
)
|
|
185
|
+
app[SERVE_STATE_KEY] = ServeState(env, config, session)
|
|
186
|
+
app[_EMBEDDED_SESSION_KEY] = session
|
|
187
|
+
app[_EMBEDDED_ENV_PATH_KEY] = env_path
|
|
188
|
+
app[_EMBEDDED_URL_KEY] = url
|
|
189
|
+
app[_EMBEDDED_COMFY_URL_KEY] = comfy_url
|
|
190
|
+
|
|
191
|
+
return EmbeddedStudioResult(
|
|
192
|
+
status="running",
|
|
193
|
+
url=url,
|
|
194
|
+
env_name=env.name,
|
|
195
|
+
started=True,
|
|
196
|
+
comfy_url=comfy_url,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_embedded_studio_status(
|
|
201
|
+
app: web.Application,
|
|
202
|
+
env: Environment,
|
|
203
|
+
*,
|
|
204
|
+
public_scheme: str,
|
|
205
|
+
public_host: str,
|
|
206
|
+
public_ui_path: str,
|
|
207
|
+
) -> EmbeddedStudioResult:
|
|
208
|
+
env_path = str(Path(env.path).resolve())
|
|
209
|
+
if app.get(_EMBEDDED_ENV_PATH_KEY) == env_path and SERVE_STATE_KEY in app:
|
|
210
|
+
url = app.get(_EMBEDDED_URL_KEY) or _public_url(public_scheme, public_host, public_ui_path)
|
|
211
|
+
comfy_url = app.get(_EMBEDDED_COMFY_URL_KEY)
|
|
212
|
+
return EmbeddedStudioResult(
|
|
213
|
+
status="running",
|
|
214
|
+
url=url,
|
|
215
|
+
env_name=env.name,
|
|
216
|
+
reused=True,
|
|
217
|
+
comfy_url=comfy_url,
|
|
218
|
+
)
|
|
219
|
+
return EmbeddedStudioResult(status="stopped", url=None, env_name=env.name)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def close_embedded_studio(app: web.Application) -> None:
|
|
223
|
+
await _close_current_state(app)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
async def _close_current_state(app: web.Application) -> None:
|
|
227
|
+
state = app.get(SERVE_STATE_KEY)
|
|
228
|
+
tasks: list[asyncio.Task[Any]] = []
|
|
229
|
+
if isinstance(state, ServeState):
|
|
230
|
+
state.state_store.close()
|
|
231
|
+
tasks.extend(set(state.background_tasks))
|
|
232
|
+
tasks.extend(set(state.active_run_tasks.values()))
|
|
233
|
+
for task in tasks:
|
|
234
|
+
task.cancel()
|
|
235
|
+
session = app.get(_EMBEDDED_SESSION_KEY)
|
|
236
|
+
if isinstance(session, aiohttp.ClientSession):
|
|
237
|
+
await session.close()
|
|
238
|
+
if tasks:
|
|
239
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
240
|
+
app.pop(SERVE_STATE_KEY, None)
|
|
241
|
+
app.pop(_EMBEDDED_SESSION_KEY, None)
|
|
242
|
+
app.pop(_EMBEDDED_ENV_PATH_KEY, None)
|
|
243
|
+
app.pop(_EMBEDDED_URL_KEY, None)
|
|
244
|
+
app.pop(_EMBEDDED_COMFY_URL_KEY, None)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _embedded_state_db(env: Environment) -> Path:
|
|
248
|
+
workspace_path = Path(env.workspace.path).resolve()
|
|
249
|
+
state_dir = workspace_path / ".metadata" / "studio"
|
|
250
|
+
return state_dir / f"{_safe_name(env.name)}.sqlite"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _safe_name(value: str) -> str:
|
|
254
|
+
safe = re.sub(r"[^A-Za-z0-9_.-]+", "-", value.strip()).strip("-")
|
|
255
|
+
return safe or "environment"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _public_url(scheme: str, host: str, path: str) -> str:
|
|
259
|
+
normalized_path = "/" + str(path or "").strip("/")
|
|
260
|
+
return f"{scheme}://{host}{normalized_path}/"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
async def _studio_asset_handler(request: web.Request) -> web.StreamResponse:
|
|
264
|
+
assets_root = (request.app[STUDIO_STATIC_DIR_KEY] / "assets").resolve()
|
|
265
|
+
requested = request.match_info.get("tail", "")
|
|
266
|
+
candidate = (assets_root / requested).resolve()
|
|
267
|
+
if not candidate.is_file() or assets_root not in candidate.parents:
|
|
268
|
+
return web.json_response({"error": "not_found"}, status=404)
|
|
269
|
+
return web.FileResponse(candidate)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _studio_static_dir() -> Path:
|
|
273
|
+
from importlib import resources
|
|
274
|
+
|
|
275
|
+
return Path(str(resources.files("comfygit_studio").joinpath("static")))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _normalize_route_prefix(prefix: str) -> str:
|
|
279
|
+
normalized = "/" + str(prefix or "").strip("/")
|
|
280
|
+
return "" if normalized == "/" else normalized
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _normalize_public_prefix(prefix: str) -> str:
|
|
284
|
+
normalized = "/" + str(prefix or "").strip("/")
|
|
285
|
+
return "" if normalized == "/" else normalized
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _route_path(prefix: str, path: str) -> str:
|
|
289
|
+
if not path.startswith("/"):
|
|
290
|
+
path = f"/{path}"
|
|
291
|
+
if not prefix:
|
|
292
|
+
return path
|
|
293
|
+
if path == "/":
|
|
294
|
+
return f"{prefix}/"
|
|
295
|
+
return f"{prefix}{path}"
|