159 lines
5.6 KiB
Python
159 lines
5.6 KiB
Python
"""GPT Image 2 / 2.5 text-to-image and image-to-image helpers."""
|
|
|
|
import time
|
|
|
|
import torch
|
|
|
|
from .auth import _load_api_key
|
|
from .credits import _log_remaining_credits
|
|
from .http import TransientKieError
|
|
from .images import _download_image, _image_bytes_to_tensor_and_mask
|
|
from .gpt_image_options import DEFAULT_MODEL, MAX_IMAGE_COUNT, build_gpt_image_payload
|
|
from .jobs import _create_task, _poll_task_until_complete
|
|
from .log import _log
|
|
from .results import _extract_result_urls
|
|
from .upload import _image_tensor_to_png_bytes, _truncate_url, _upload_image
|
|
from .validation import _validate_image_tensor_batch
|
|
|
|
|
|
def _run_gpt_image2_payload(
|
|
*,
|
|
payload: dict[str, object],
|
|
poll_interval_s: float,
|
|
timeout_s: int,
|
|
log: bool,
|
|
create_label: str,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
api_key = _load_api_key()
|
|
_log(log, f"Creating {create_label} task...")
|
|
start_time = time.time()
|
|
task_id, create_response_text = _create_task(api_key, payload)
|
|
_log(log, f"createTask response (elapsed={time.time() - start_time:.1f}s): {create_response_text}")
|
|
_log(log, f"Task created with ID {task_id}. Polling for completion...")
|
|
|
|
record_data = _poll_task_until_complete(
|
|
api_key,
|
|
task_id,
|
|
poll_interval_s,
|
|
timeout_s,
|
|
log,
|
|
start_time,
|
|
)
|
|
|
|
result_urls = _extract_result_urls(record_data)
|
|
_log(log, f"Result URLs: {result_urls}")
|
|
_log(log, f"Downloading result image from {result_urls[0]}...")
|
|
image_bytes = _download_image(result_urls[0])
|
|
image_tensor, mask_tensor = _image_bytes_to_tensor_and_mask(image_bytes)
|
|
_log(log, "Image downloaded and decoded.")
|
|
_log_remaining_credits(log, record_data, api_key, _log)
|
|
return image_tensor, mask_tensor
|
|
|
|
|
|
def run_gpt_image2_text_to_image(
|
|
*,
|
|
prompt: str,
|
|
aspect_ratio: str,
|
|
resolution: str,
|
|
poll_interval_s: float = 10.0,
|
|
timeout_s: int = 300,
|
|
log: bool = True,
|
|
retry_on_fail: bool = True,
|
|
max_retries: int = 2,
|
|
retry_backoff_s: float = 3.0,
|
|
model: str = DEFAULT_MODEL,
|
|
background: str = "opaque",
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Run the selected GPT Image text-to-image model; return RGB and mask."""
|
|
payload = build_gpt_image_payload(
|
|
model=model, mode="text-to-image", prompt=prompt,
|
|
aspect_ratio=aspect_ratio, resolution=resolution, background=background,
|
|
)
|
|
|
|
attempts = max_retries + 1 if retry_on_fail else 1
|
|
attempts = max(attempts, 1)
|
|
backoff = retry_backoff_s if retry_backoff_s >= 0 else 0.0
|
|
|
|
for attempt in range(1, attempts + 1):
|
|
try:
|
|
return _run_gpt_image2_payload(
|
|
payload=payload,
|
|
poll_interval_s=poll_interval_s,
|
|
timeout_s=timeout_s,
|
|
log=log,
|
|
create_label=f"{model} text-to-image",
|
|
)
|
|
except TransientKieError:
|
|
if not retry_on_fail or attempt >= attempts:
|
|
raise
|
|
_log(log, f"Retrying (attempt {attempt + 1}/{attempts}) after {backoff}s")
|
|
time.sleep(backoff)
|
|
|
|
raise RuntimeError(f"{model} text-to-image job failed after retry attempts.")
|
|
|
|
|
|
def run_gpt_image2_image_to_image(
|
|
*,
|
|
prompt: str,
|
|
images: torch.Tensor,
|
|
aspect_ratio: str,
|
|
resolution: str,
|
|
poll_interval_s: float = 10.0,
|
|
timeout_s: int = 300,
|
|
log: bool = True,
|
|
retry_on_fail: bool = True,
|
|
max_retries: int = 2,
|
|
retry_backoff_s: float = 3.0,
|
|
model: str = DEFAULT_MODEL,
|
|
background: str = "opaque",
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Run the selected GPT Image editing model; return RGB and mask."""
|
|
payload = build_gpt_image_payload(
|
|
model=model, mode="image-to-image", prompt=prompt,
|
|
aspect_ratio=aspect_ratio, resolution=resolution, background=background,
|
|
)
|
|
images = _validate_image_tensor_batch(images)
|
|
total_images = images.shape[0]
|
|
if total_images > MAX_IMAGE_COUNT and model != DEFAULT_MODEL:
|
|
raise RuntimeError(f"{model} accepts at most {MAX_IMAGE_COUNT} source images; received {total_images}.")
|
|
|
|
attempts = max_retries + 1 if retry_on_fail else 1
|
|
attempts = max(attempts, 1)
|
|
backoff = retry_backoff_s if retry_backoff_s >= 0 else 0.0
|
|
|
|
for attempt in range(1, attempts + 1):
|
|
try:
|
|
api_key = _load_api_key()
|
|
if total_images > MAX_IMAGE_COUNT:
|
|
_log(
|
|
log,
|
|
f"More than {MAX_IMAGE_COUNT} images provided ({total_images}); "
|
|
f"only first {MAX_IMAGE_COUNT} used.",
|
|
)
|
|
|
|
upload_count = min(total_images, MAX_IMAGE_COUNT)
|
|
image_urls: list[str] = []
|
|
_log(log, f"Uploading {upload_count} image(s) for {model} I2I...")
|
|
for idx in range(upload_count):
|
|
png_bytes = _image_tensor_to_png_bytes(images[idx])
|
|
image_url = _upload_image(api_key, png_bytes)
|
|
image_urls.append(image_url)
|
|
_log(log, f"Image {idx + 1} upload success: {_truncate_url(image_url)}")
|
|
|
|
payload["input"]["input_urls"] = image_urls
|
|
|
|
return _run_gpt_image2_payload(
|
|
payload=payload,
|
|
poll_interval_s=poll_interval_s,
|
|
timeout_s=timeout_s,
|
|
log=log,
|
|
create_label=f"{model} image-to-image",
|
|
)
|
|
except TransientKieError:
|
|
if not retry_on_fail or attempt >= attempts:
|
|
raise
|
|
_log(log, f"Retrying (attempt {attempt + 1}/{attempts}) after {backoff}s")
|
|
time.sleep(backoff)
|
|
|
|
raise RuntimeError(f"{model} image-to-image job failed after retry attempts.")
|