comfygit-studio 0.5.0__tar.gz → 0.7.0__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.
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/.gitignore +1 -0
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/PKG-INFO +4 -4
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/runtime.py +164 -1
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/state.py +2 -1
- comfygit_studio-0.7.0/comfygit_studio/static/assets/index-ry5Db1EL.js +17 -0
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/static/index.html +1 -1
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/pyproject.toml +3 -3
- comfygit_studio-0.5.0/comfygit_studio/static/assets/index-BlnDFmNd.js +0 -17
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/README.md +0 -0
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/__init__.py +0 -0
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/api_schema.py +0 -0
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/embedded.py +0 -0
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/executor.py +0 -0
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/openapi/studio-contract-api.v1.json +0 -0
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/static/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/static/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/static/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/static/assets/index-BDmIh9tA.css +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: comfygit-studio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: ComfyGit Studio runtime - contract API and Studio serving adapters
|
|
5
5
|
Project-URL: Documentation, https://docs.comfygit.org/
|
|
6
6
|
Project-URL: Repository, https://github.com/comfygit-ai/comfygit
|
|
7
7
|
Project-URL: Issues, https://github.com/comfygit-ai/comfygit/issues
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
|
-
Requires-Dist: aiohttp>=3.
|
|
10
|
-
Requires-Dist: comfygit-core==0.
|
|
9
|
+
Requires-Dist: aiohttp>=3.14.3
|
|
10
|
+
Requires-Dist: comfygit-core==0.7.0
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
|
|
13
13
|
# ComfyGit Studio Runtime
|
|
@@ -172,6 +172,16 @@ class WorkerCallbackUpload:
|
|
|
172
172
|
body: bytes
|
|
173
173
|
|
|
174
174
|
|
|
175
|
+
@dataclass(frozen=True)
|
|
176
|
+
class WorkerArtifactUploadTarget:
|
|
177
|
+
upload_id: str
|
|
178
|
+
output_name: str
|
|
179
|
+
artifact_index: int
|
|
180
|
+
transport: dict[str, Any]
|
|
181
|
+
storage_ref: dict[str, Any]
|
|
182
|
+
expected_content_type: str | None = None
|
|
183
|
+
|
|
184
|
+
|
|
175
185
|
class ServeState:
|
|
176
186
|
"""Shared state for request handlers."""
|
|
177
187
|
|
|
@@ -833,6 +843,7 @@ async def proxy_run_create_handler(request: web.Request) -> web.Response:
|
|
|
833
843
|
poll_interval_seconds = float(payload.get("poll_interval_seconds", 1))
|
|
834
844
|
cache_token = str(payload.get("cache_token") or uuid.uuid4().hex[:10])
|
|
835
845
|
callback = _proxy_callback_target(payload.get("callback"))
|
|
846
|
+
artifact_upload_targets = _proxy_artifact_upload_targets(payload)
|
|
836
847
|
|
|
837
848
|
async def record_submitted(prompt_id: str) -> None:
|
|
838
849
|
state.proxy_runs[prompt_id] = ProxyRuntimeRun(
|
|
@@ -862,6 +873,7 @@ async def proxy_run_create_handler(request: web.Request) -> web.Response:
|
|
|
862
873
|
timeout_seconds=timeout_seconds,
|
|
863
874
|
poll_interval_seconds=poll_interval_seconds,
|
|
864
875
|
callback=callback,
|
|
876
|
+
artifact_upload_targets=artifact_upload_targets,
|
|
865
877
|
)
|
|
866
878
|
)
|
|
867
879
|
_track_proxy_task(state, task)
|
|
@@ -1752,6 +1764,39 @@ def _proxy_callback_target(value: Any) -> ProxyCallbackTarget | None:
|
|
|
1752
1764
|
)
|
|
1753
1765
|
|
|
1754
1766
|
|
|
1767
|
+
def _proxy_artifact_upload_targets(payload: Mapping[str, Any]) -> tuple[WorkerArtifactUploadTarget, ...]:
|
|
1768
|
+
raw_targets: Any = payload.get("artifact_upload_targets")
|
|
1769
|
+
delivery = payload.get("artifact_delivery")
|
|
1770
|
+
if not isinstance(raw_targets, list) and isinstance(delivery, Mapping):
|
|
1771
|
+
if str(delivery.get("mode") or "") == "direct_upload":
|
|
1772
|
+
raw_targets = delivery.get("targets")
|
|
1773
|
+
if not isinstance(raw_targets, list):
|
|
1774
|
+
return ()
|
|
1775
|
+
|
|
1776
|
+
targets: list[WorkerArtifactUploadTarget] = []
|
|
1777
|
+
for raw_target in raw_targets:
|
|
1778
|
+
if not isinstance(raw_target, Mapping):
|
|
1779
|
+
continue
|
|
1780
|
+
upload_id = str(raw_target.get("upload_id") or "").strip()
|
|
1781
|
+
output_name = str(raw_target.get("output_name") or "").strip()
|
|
1782
|
+
transport = raw_target.get("transport")
|
|
1783
|
+
if not upload_id or not output_name or not isinstance(transport, Mapping):
|
|
1784
|
+
continue
|
|
1785
|
+
storage_ref = raw_target.get("storage_ref")
|
|
1786
|
+
expected_content_type = raw_target.get("expected_content_type")
|
|
1787
|
+
targets.append(
|
|
1788
|
+
WorkerArtifactUploadTarget(
|
|
1789
|
+
upload_id=upload_id,
|
|
1790
|
+
output_name=output_name,
|
|
1791
|
+
artifact_index=max(0, _optional_int(raw_target.get("artifact_index")) or 0),
|
|
1792
|
+
transport={str(key): value for key, value in transport.items()},
|
|
1793
|
+
storage_ref={str(key): value for key, value in storage_ref.items()} if isinstance(storage_ref, Mapping) else {},
|
|
1794
|
+
expected_content_type=str(expected_content_type) if expected_content_type else None,
|
|
1795
|
+
)
|
|
1796
|
+
)
|
|
1797
|
+
return tuple(targets)
|
|
1798
|
+
|
|
1799
|
+
|
|
1755
1800
|
async def _complete_proxy_runtime_run(
|
|
1756
1801
|
state: ServeState,
|
|
1757
1802
|
prompt_id: str,
|
|
@@ -1760,6 +1805,7 @@ async def _complete_proxy_runtime_run(
|
|
|
1760
1805
|
timeout_seconds: float,
|
|
1761
1806
|
poll_interval_seconds: float,
|
|
1762
1807
|
callback: ProxyCallbackTarget | None = None,
|
|
1808
|
+
artifact_upload_targets: tuple[WorkerArtifactUploadTarget, ...] = (),
|
|
1763
1809
|
) -> None:
|
|
1764
1810
|
record = state.proxy_runs.get(prompt_id)
|
|
1765
1811
|
if record is None:
|
|
@@ -1821,7 +1867,11 @@ async def _complete_proxy_runtime_run(
|
|
|
1821
1867
|
}
|
|
1822
1868
|
if callback is not None:
|
|
1823
1869
|
try:
|
|
1824
|
-
callback_outputs, uploads = await _worker_callback_outputs_and_uploads(
|
|
1870
|
+
callback_outputs, uploads = await _worker_callback_outputs_and_uploads(
|
|
1871
|
+
state,
|
|
1872
|
+
execution.outputs,
|
|
1873
|
+
artifact_upload_targets=artifact_upload_targets,
|
|
1874
|
+
)
|
|
1825
1875
|
await _post_worker_completion_callback(
|
|
1826
1876
|
callback,
|
|
1827
1877
|
{
|
|
@@ -1907,6 +1957,8 @@ def _worker_callback_headers(callback: ProxyCallbackTarget) -> dict[str, str]:
|
|
|
1907
1957
|
async def _worker_callback_outputs_and_uploads(
|
|
1908
1958
|
state: ServeState,
|
|
1909
1959
|
outputs: list[dict[str, Any]],
|
|
1960
|
+
*,
|
|
1961
|
+
artifact_upload_targets: tuple[WorkerArtifactUploadTarget, ...] = (),
|
|
1910
1962
|
) -> tuple[list[dict[str, Any]], list[WorkerCallbackUpload]]:
|
|
1911
1963
|
output_payloads = [dict(output) for output in outputs]
|
|
1912
1964
|
uploads: list[WorkerCallbackUpload] = []
|
|
@@ -1935,6 +1987,27 @@ async def _worker_callback_outputs_and_uploads(
|
|
|
1935
1987
|
filename,
|
|
1936
1988
|
fallback=f"{field_name}{_extension_for_content_type(response.content_type)}",
|
|
1937
1989
|
)
|
|
1990
|
+
upload_target = _worker_artifact_upload_target(
|
|
1991
|
+
artifact_upload_targets,
|
|
1992
|
+
output_name=str(output.get("name") or f"output_{output_index + 1}"),
|
|
1993
|
+
artifact_index=artifact_index,
|
|
1994
|
+
)
|
|
1995
|
+
if upload_target is not None:
|
|
1996
|
+
await _upload_worker_artifact_to_target(
|
|
1997
|
+
upload_target,
|
|
1998
|
+
filename=safe_filename,
|
|
1999
|
+
content_type=response.content_type,
|
|
2000
|
+
body=response.body,
|
|
2001
|
+
)
|
|
2002
|
+
artifact["upload_id"] = upload_target.upload_id
|
|
2003
|
+
artifact["storage_ref"] = dict(upload_target.storage_ref)
|
|
2004
|
+
artifact["filename"] = safe_filename
|
|
2005
|
+
artifact["content_type"] = response.content_type
|
|
2006
|
+
artifact["kind"] = output_kind(output_type, safe_filename)
|
|
2007
|
+
artifact["size_bytes"] = len(response.body)
|
|
2008
|
+
artifact["sha256"] = hashlib.sha256(response.body).hexdigest()
|
|
2009
|
+
artifact.pop("upload_field", None)
|
|
2010
|
+
continue
|
|
1938
2011
|
artifact["upload_field"] = field_name
|
|
1939
2012
|
artifact["content_type"] = response.content_type
|
|
1940
2013
|
artifact["kind"] = output_kind(output_type, safe_filename)
|
|
@@ -1949,6 +2022,96 @@ async def _worker_callback_outputs_and_uploads(
|
|
|
1949
2022
|
return output_payloads, uploads
|
|
1950
2023
|
|
|
1951
2024
|
|
|
2025
|
+
def _worker_artifact_upload_target(
|
|
2026
|
+
targets: tuple[WorkerArtifactUploadTarget, ...],
|
|
2027
|
+
*,
|
|
2028
|
+
output_name: str,
|
|
2029
|
+
artifact_index: int,
|
|
2030
|
+
) -> WorkerArtifactUploadTarget | None:
|
|
2031
|
+
for target in targets:
|
|
2032
|
+
if target.output_name == output_name and target.artifact_index == artifact_index:
|
|
2033
|
+
return target
|
|
2034
|
+
return None
|
|
2035
|
+
|
|
2036
|
+
|
|
2037
|
+
async def _upload_worker_artifact_to_target(
|
|
2038
|
+
target: WorkerArtifactUploadTarget,
|
|
2039
|
+
*,
|
|
2040
|
+
filename: str,
|
|
2041
|
+
content_type: str,
|
|
2042
|
+
body: bytes,
|
|
2043
|
+
) -> None:
|
|
2044
|
+
kind = str(target.transport.get("kind") or target.transport.get("transport_kind") or "").lower()
|
|
2045
|
+
if kind == "local_file_put":
|
|
2046
|
+
path_value = target.transport.get("path") or target.storage_ref.get("path")
|
|
2047
|
+
if not path_value:
|
|
2048
|
+
raise ValueError(f"Artifact upload target {target.upload_id} is missing local path.")
|
|
2049
|
+
path = Path(str(path_value))
|
|
2050
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
2051
|
+
temp_path = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
|
|
2052
|
+
temp_path.write_bytes(body)
|
|
2053
|
+
temp_path.replace(path)
|
|
2054
|
+
return
|
|
2055
|
+
|
|
2056
|
+
if kind == "supabase_signed_upload":
|
|
2057
|
+
upload_url = str(target.transport.get("url") or "").strip()
|
|
2058
|
+
if not upload_url:
|
|
2059
|
+
raise ValueError(f"Artifact upload target {target.upload_id} is missing upload url.")
|
|
2060
|
+
headers = _worker_artifact_upload_headers(target.transport)
|
|
2061
|
+
# Supabase signed upload URLs accept multipart form PUT with a single
|
|
2062
|
+
# file part; sending raw bytes yields a successful HTTP request that
|
|
2063
|
+
# stores the wrong object shape.
|
|
2064
|
+
form = aiohttp.FormData()
|
|
2065
|
+
form.add_field("file", body, filename=filename, content_type=content_type)
|
|
2066
|
+
async with aiohttp.ClientSession() as session:
|
|
2067
|
+
async with session.put(
|
|
2068
|
+
upload_url,
|
|
2069
|
+
data=form,
|
|
2070
|
+
headers=headers,
|
|
2071
|
+
timeout=aiohttp.ClientTimeout(total=300),
|
|
2072
|
+
) as response:
|
|
2073
|
+
response.raise_for_status()
|
|
2074
|
+
return
|
|
2075
|
+
|
|
2076
|
+
if kind in {"http_signed_upload", "http_put", "signed_put"}:
|
|
2077
|
+
upload_url = str(target.transport.get("url") or "").strip()
|
|
2078
|
+
if not upload_url:
|
|
2079
|
+
raise ValueError(f"Artifact upload target {target.upload_id} is missing upload url.")
|
|
2080
|
+
method = str(target.transport.get("method") or "PUT").upper()
|
|
2081
|
+
headers = _worker_artifact_upload_headers(target.transport)
|
|
2082
|
+
headers.setdefault("content-type", content_type)
|
|
2083
|
+
async with aiohttp.ClientSession() as session:
|
|
2084
|
+
async with session.request(
|
|
2085
|
+
method,
|
|
2086
|
+
upload_url,
|
|
2087
|
+
data=body,
|
|
2088
|
+
headers=headers,
|
|
2089
|
+
timeout=aiohttp.ClientTimeout(total=300),
|
|
2090
|
+
) as response:
|
|
2091
|
+
response.raise_for_status()
|
|
2092
|
+
return
|
|
2093
|
+
|
|
2094
|
+
raise ValueError(f"Unsupported artifact upload target transport: {kind or 'unknown'}.")
|
|
2095
|
+
|
|
2096
|
+
|
|
2097
|
+
def _worker_artifact_upload_headers(transport: Mapping[str, Any]) -> dict[str, str]:
|
|
2098
|
+
raw_headers = transport.get("headers")
|
|
2099
|
+
if not isinstance(raw_headers, Mapping):
|
|
2100
|
+
return {}
|
|
2101
|
+
return {
|
|
2102
|
+
str(key): str(value)
|
|
2103
|
+
for key, value in raw_headers.items()
|
|
2104
|
+
if value is not None and str(key).lower() != "content-type"
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
|
|
2108
|
+
def _optional_int(value: Any) -> int | None:
|
|
2109
|
+
try:
|
|
2110
|
+
return int(value)
|
|
2111
|
+
except (TypeError, ValueError):
|
|
2112
|
+
return None
|
|
2113
|
+
|
|
2114
|
+
|
|
1952
2115
|
def _record_proxy_run_error(record: ProxyRuntimeRun, payload: dict[str, Any]) -> None:
|
|
1953
2116
|
record.status = "error"
|
|
1954
2117
|
record.error = str(payload.get("message") or payload.get("error") or "Proxy run failed.")
|
|
@@ -12,12 +12,13 @@ import binascii
|
|
|
12
12
|
import json
|
|
13
13
|
import sqlite3
|
|
14
14
|
from dataclasses import dataclass
|
|
15
|
-
from datetime import
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Any
|
|
18
18
|
|
|
19
19
|
SERVE_STATE_SCHEMA_VERSION = 2
|
|
20
20
|
GALLERY_CURSOR_VERSION = 1
|
|
21
|
+
UTC = timezone.utc
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def utc_now() -> str:
|