-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathutils.py
More file actions
180 lines (146 loc) · 6.05 KB
/
Copy pathutils.py
File metadata and controls
180 lines (146 loc) · 6.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""Shared helpers for Pruna API nodes."""
import os
import time
from io import BytesIO
import numpy as np
import requests
import torch
from PIL import Image
PRUNA_API_BASE = "https://api.pruna.ai"
PREDICTIONS_URL = f"{PRUNA_API_BASE}/v1/predictions"
FILES_URL = f"{PRUNA_API_BASE}/v1/files"
ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"]
VIDEO_ASPECT_RATIOS = ["16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "1:1"]
POLL_INTERVAL = 3 # seconds between status checks
POLL_TIMEOUT = 300 # seconds before giving up
def resolve_api_key(api_key: str) -> str:
"""Return the api_key field value, falling back to the PRUNA_API_KEY env var."""
key = api_key.strip() if api_key else ""
if not key:
key = os.environ.get("PRUNA_API_KEY", "")
if not key:
raise RuntimeError(
"No Pruna API key provided. Set the api_key input or the "
"PRUNA_API_KEY environment variable."
)
return key
def tensor_to_pil(tensor: torch.Tensor) -> Image.Image:
"""Convert a ComfyUI IMAGE tensor [1, H, W, C] or [H, W, C] to a PIL Image."""
if tensor.ndim == 4:
tensor = tensor[0]
arr = (tensor.cpu().numpy() * 255).clip(0, 255).astype(np.uint8)
if arr.shape[-1] == 4:
return Image.fromarray(arr, mode="RGBA").convert("RGB")
return Image.fromarray(arr, mode="RGB")
def upload_image(tensor: torch.Tensor, api_key: str) -> str:
"""Upload a ComfyUI IMAGE tensor to the Pruna files API and return the file URL."""
pil_image = tensor_to_pil(tensor)
buf = BytesIO()
pil_image.save(buf, format="PNG")
buf.seek(0)
response = requests.post(
FILES_URL,
headers={"apikey": api_key},
files={"content": ("image.png", buf, "image/png")},
timeout=60,
)
if response.status_code not in (200, 201):
raise RuntimeError(
f"Pruna file upload failed {response.status_code}: {response.text}"
)
data = response.json()
return data["urls"]["get"]
def call_api_sync(model: str, payload: dict, api_key: str) -> dict:
"""POST a prediction and return the JSON response (synchronous, Try-Sync: true)."""
headers = {
"Content-Type": "application/json",
"apikey": api_key,
"Model": model,
"Try-Sync": "true",
}
response = requests.post(PREDICTIONS_URL, json=payload, headers=headers, timeout=120)
if response.status_code not in (200, 201):
raise RuntimeError(
f"Pruna API error {response.status_code}: {response.text}"
)
return response.json()
def _poll_until_complete(get_url: str, api_key: str) -> dict:
"""Poll a submitted Pruna job by its get_url until it completes or times out."""
start = time.time()
while time.time() - start < POLL_TIMEOUT:
time.sleep(POLL_INTERVAL)
status_response = requests.get(
get_url,
headers={"apikey": api_key},
timeout=30,
)
if status_response.status_code != 200:
raise RuntimeError(
f"Pruna status check failed {status_response.status_code}: "
f"{status_response.text}"
)
status = status_response.json()
state = status.get("status", "")
if state == "succeeded" or status.get("generation_url"):
return status
if state in ("failed", "canceled"):
raise RuntimeError(f"Pruna job {state}: {status}")
raise RuntimeError(
f"Pruna job did not complete within {POLL_TIMEOUT}s. "
f"Check the Pruna developer portal for job status."
)
def call_api_async(model: str, payload: dict, api_key: str) -> dict:
"""POST a prediction and poll until complete. Returns the final status dict."""
headers = {
"Content-Type": "application/json",
"apikey": api_key,
"Model": model,
}
response = requests.post(PREDICTIONS_URL, json=payload, headers=headers, timeout=60)
if response.status_code not in (200, 201):
raise RuntimeError(
f"Pruna API error {response.status_code}: {response.text}"
)
job = response.json()
if job.get("status") == "succeeded" or job.get("generation_url"):
return job
get_url = job.get("get_url")
if not get_url:
raise RuntimeError(f"Pruna API returned no get_url for polling. Response: {job}")
return _poll_until_complete(get_url, api_key)
def call_api_with_fallback(model: str, payload: dict, api_key: str) -> dict:
"""Try sync prediction first; if no generation_url yet, poll the existing job."""
data = call_api_sync(model, payload, api_key)
if data.get("generation_url"):
return data
get_url = data.get("get_url")
if not get_url:
raise RuntimeError(
f"Pruna API returned no generation_url and no get_url. Response: {data}"
)
return _poll_until_complete(get_url, api_key)
def add_hf_token(payload: dict, hf_api_token: str) -> None:
"""Attach a non-empty HuggingFace token to payload['input']."""
token = hf_api_token.strip()
if token:
payload["input"]["hf_api_token"] = token
def resolve_generation_url(url: str) -> str:
"""Normalise a generation_url that may be a relative path."""
if url.startswith("/"):
return f"{PRUNA_API_BASE}{url}"
return url
def generation_url_to_tensor(data: dict) -> torch.Tensor:
"""Download generation_url from a Pruna API response and return an IMAGE tensor."""
generation_url = data.get("generation_url")
if not generation_url:
raise RuntimeError(f"Pruna API returned no generation_url. Response: {data}")
generation_url = resolve_generation_url(generation_url)
img_response = requests.get(generation_url, timeout=60)
if img_response.status_code != 200:
raise RuntimeError(
f"Failed to download image from {generation_url}: "
f"{img_response.status_code}"
)
image = Image.open(BytesIO(img_response.content)).convert("RGB")
image_np = np.array(image).astype(np.float32) / 255.0
return torch.from_numpy(image_np).unsqueeze(0) # [1, H, W, C]